@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,148 @@
|
|
|
1
|
+
# Chart spec
|
|
2
|
+
|
|
3
|
+
Every chart in this package is described by one JSON object — the `ChartSpec`.
|
|
4
|
+
Tools build it from their simplified inputs; the headless renderer, the browser
|
|
5
|
+
widget and the code generator all consume it.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
tool input ──(recipe)──► ChartSpec ──┬──► headless renderer ──► SVG / PNG
|
|
9
|
+
├──► browser widget ──► HTML / inline widget
|
|
10
|
+
└──► code generator ──► framework source
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Because the spec is plain JSON, you can obtain one with
|
|
14
|
+
`outputType: "config"`, edit it, and render it directly — which is how you
|
|
15
|
+
reach Unovis options the tool schemas deliberately don't expose.
|
|
16
|
+
|
|
17
|
+
## Shape
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
interface ChartSpec {
|
|
21
|
+
container: 'xy' | 'single'
|
|
22
|
+
width: number
|
|
23
|
+
height: number
|
|
24
|
+
theme: 'light' | 'dark'
|
|
25
|
+
title?: string
|
|
26
|
+
containerConfig?: Record<string, unknown> // extra container options
|
|
27
|
+
components: { type: string; config: Record<string, unknown> }[]
|
|
28
|
+
xAxis?: Record<string, unknown> // XY containers only
|
|
29
|
+
yAxis?: Record<string, unknown>
|
|
30
|
+
colors?: string[]
|
|
31
|
+
legend?: { name: string; color?: string; paletteIndex?: number }[]
|
|
32
|
+
data: unknown
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- **`container: 'xy'`** holds any number of XY components (line, area, bars,
|
|
37
|
+
scatter, timeline, boxplot, plus `Plotband`/`Plotline` decorations) on shared,
|
|
38
|
+
domain-synchronized scales, plus optional axes.
|
|
39
|
+
- **`container: 'single'`** holds exactly one component (donut, sankey, heatmap,
|
|
40
|
+
treemap, chord, graph, map, …).
|
|
41
|
+
- `components` renders in array order, so later entries draw on top.
|
|
42
|
+
|
|
43
|
+
## A complete example
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"container": "xy",
|
|
48
|
+
"width": 800,
|
|
49
|
+
"height": 480,
|
|
50
|
+
"theme": "light",
|
|
51
|
+
"title": "Revenue vs Target",
|
|
52
|
+
"components": [
|
|
53
|
+
{
|
|
54
|
+
"type": "GroupedBar",
|
|
55
|
+
"config": {
|
|
56
|
+
"x": { "$index": true },
|
|
57
|
+
"y": { "$field": "revenue", "as": "number" },
|
|
58
|
+
"roundedCorners": 3
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"type": "Line",
|
|
63
|
+
"config": {
|
|
64
|
+
"x": { "$index": true },
|
|
65
|
+
"y": { "$field": "target", "as": "number" },
|
|
66
|
+
"lineWidth": 3,
|
|
67
|
+
"color": "var(--vis-color1)"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"xAxis": { "tickFormat": { "$lookup": ["Jan", "Feb", "Mar"] }, "tickValues": [0, 1, 2] },
|
|
72
|
+
"yAxis": { "gridLine": true, "tickFormat": { "$numTickFormat": true } },
|
|
73
|
+
"legend": [
|
|
74
|
+
{ "name": "Revenue", "paletteIndex": 0 },
|
|
75
|
+
{ "name": "Target", "paletteIndex": 1 }
|
|
76
|
+
],
|
|
77
|
+
"data": [
|
|
78
|
+
{ "revenue": 420, "target": 430 },
|
|
79
|
+
{ "revenue": 510, "target": 460 },
|
|
80
|
+
{ "revenue": 470, "target": 490 }
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
That spec is a bar + line combo chart — something no single tool emits today,
|
|
86
|
+
but which the spec layer has always supported.
|
|
87
|
+
|
|
88
|
+
## Accessor references
|
|
89
|
+
|
|
90
|
+
Unovis accessors are functions, which JSON can't hold, so the spec uses
|
|
91
|
+
descriptors that the renderer converts into real functions. **Nothing in a spec
|
|
92
|
+
is ever evaluated as code** — field names are looked up as properties, and
|
|
93
|
+
that's the whole mechanism.
|
|
94
|
+
|
|
95
|
+
| Descriptor | Becomes | Use |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `{ "$field": "sales" }` | `d => d.sales` | Read a field |
|
|
98
|
+
| `{ "$field": "sales", "as": "number" }` | numeric coercion | Numeric fields arriving as strings |
|
|
99
|
+
| `{ "$field": "date", "as": "date" }` | epoch milliseconds | Time axes |
|
|
100
|
+
| `{ "$index": true }` | `(d, i) => i` | Categorical x positions |
|
|
101
|
+
| `{ "$const": 5 }` | `() => 5` | Constant value |
|
|
102
|
+
| `{ "$lookup": ["Jan", "Feb"] }` | index → label | Category tick labels |
|
|
103
|
+
| `{ "$numTickFormat": true }` | thousands separators | Numeric axis labels |
|
|
104
|
+
| `{ "$dateTickFormat": true }` | short date labels | Time axis labels |
|
|
105
|
+
| `{ "$format": { "field": "value", "prefix": "$", "suffix": " USD" } }` | formatted string | Sankey sub-labels, etc. |
|
|
106
|
+
| `{ "$mapField": { "field": "group", "mapping": { "a": "#4D8CFD" }, "fallback": "#ccc" } }` | category → value | Color by category |
|
|
107
|
+
| `{ "$unovisMap": "WorldMapTopoJSON" }` | the topojson payload | Map charts |
|
|
108
|
+
| `{ "$mapProjection": "AlbersUsa" }` | `MapProjection.AlbersUsa()` | Map projections |
|
|
109
|
+
|
|
110
|
+
Descriptors can appear anywhere in a component config, including nested inside
|
|
111
|
+
objects and arrays. `y: [{ "$field": "a" }, { "$field": "b" }]` is a two-series
|
|
112
|
+
chart.
|
|
113
|
+
|
|
114
|
+
## Component types
|
|
115
|
+
|
|
116
|
+
XY components (`container: "xy"`): `Line`, `Area`, `GroupedBar`, `StackedBar`,
|
|
117
|
+
`Scatter`, `Timeline`, `Boxplot`, `XYLabels`, `Plotband`, `Plotline`.
|
|
118
|
+
|
|
119
|
+
Single components (`container: "single"`): `Donut`, `NestedDonut`, `RadialBar`,
|
|
120
|
+
`Sankey`, `Heatmap`, `Treemap`, `ChordDiagram`, `Graph`, `TopoJSONMap`.
|
|
121
|
+
|
|
122
|
+
Any option from the corresponding Unovis config passes straight through, as
|
|
123
|
+
long as it's JSON-serializable or expressible as a descriptor. Interaction
|
|
124
|
+
options (events, tooltips, brushes) are meaningless for static output and are
|
|
125
|
+
ignored; the interactive widget adds its own.
|
|
126
|
+
|
|
127
|
+
## Rendering a spec yourself
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { renderChart } from '@unovis/mcp'
|
|
131
|
+
|
|
132
|
+
const { svg, width, height, warnings } = await renderChart(spec)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Charts always render with `duration: 0` so output is deterministic, and the
|
|
136
|
+
renderer waits for the container's `onRenderComplete` — plus the component's own
|
|
137
|
+
completion signal for asynchronous layouts like force-directed graphs.
|
|
138
|
+
|
|
139
|
+
## Deliberate limits
|
|
140
|
+
|
|
141
|
+
- **Functions can't be smuggled in.** If you need a bespoke callback, use the
|
|
142
|
+
library directly with [`renderToSvg`](./programmatic.md#rendertosvg).
|
|
143
|
+
- **One coordinate system per chart.** Components in an XY container share
|
|
144
|
+
synchronized domains, so mixing wildly different units (revenue and
|
|
145
|
+
percentages) will flatten one of them. True dual axes aren't supported yet.
|
|
146
|
+
- **`Graph` layouts** are limited to `force`, `circular` and `concentric` in the
|
|
147
|
+
tools; `dagre` and `elk` work through a hand-written spec in Node but not in
|
|
148
|
+
the browser widget (see [Troubleshooting](./troubleshooting.md)).
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
- **Node.js 20 or newer.** Native dependencies ship as prebuilt binaries
|
|
6
|
+
(`@napi-rs/canvas`, `@resvg/resvg-js`), so there is no compile step and no
|
|
7
|
+
`node-gyp`.
|
|
8
|
+
- Nothing else. No browser download, no database, no API keys.
|
|
9
|
+
|
|
10
|
+
## Connect a client
|
|
11
|
+
|
|
12
|
+
The server speaks stdio by default, which is what every MCP client uses for
|
|
13
|
+
local servers. You register it once; the client starts and stops it for you.
|
|
14
|
+
|
|
15
|
+
### Claude Code
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
claude mcp add unovis -- npx -y @unovis/mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Claude Desktop
|
|
22
|
+
|
|
23
|
+
Add to `claude_desktop_config.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"unovis": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["-y", "@unovis/mcp"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Cursor
|
|
37
|
+
|
|
38
|
+
Add to `.cursor/mcp.json` in your project (or the global equivalent):
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"unovis": {
|
|
44
|
+
"command": "npx",
|
|
45
|
+
"args": ["-y", "@unovis/mcp"]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Codex CLI
|
|
52
|
+
|
|
53
|
+
Add to `~/.codex/config.toml`:
|
|
54
|
+
|
|
55
|
+
```toml
|
|
56
|
+
[mcp_servers.unovis]
|
|
57
|
+
command = "npx"
|
|
58
|
+
args = ["-y", "@unovis/mcp"]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Codex runs in a terminal and cannot display inline images, so prefer
|
|
62
|
+
`outputType: "svg"` (plain text) or `outputPath` (writes a file, returns the
|
|
63
|
+
path). See [Output types](./output-types.md).
|
|
64
|
+
|
|
65
|
+
### VS Code
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"servers": {
|
|
70
|
+
"unovis": {
|
|
71
|
+
"type": "stdio",
|
|
72
|
+
"command": "npx",
|
|
73
|
+
"args": ["-y", "@unovis/mcp"]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Any other MCP client
|
|
80
|
+
|
|
81
|
+
There is nothing client-specific in the server: standard stdio JSON-RPC,
|
|
82
|
+
`tools/list` + `tools/call`, plus one optional UI resource. If your client
|
|
83
|
+
speaks MCP, point it at `npx -y @unovis/mcp`.
|
|
84
|
+
|
|
85
|
+
## Verify the installation
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx @unovis/mcp --version
|
|
89
|
+
npx @modelcontextprotocol/inspector npx -y @unovis/mcp
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The Inspector lists every tool with its schema and lets you call one
|
|
93
|
+
interactively — the fastest way to confirm the server works before wiring it
|
|
94
|
+
into an agent.
|
|
95
|
+
|
|
96
|
+
## Remote / shared deployments
|
|
97
|
+
|
|
98
|
+
For a team server, use the streamable HTTP transport:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx @unovis/mcp --transport http --host 0.0.0.0 --port 3737 --endpoint /mcp
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
claude mcp add --transport http unovis https://charts.example.com/mcp
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Two things to know before exposing it:
|
|
109
|
+
|
|
110
|
+
- **There is no built-in authentication.** Keep it on a private network or put
|
|
111
|
+
a reverse proxy with a bearer-token check in front.
|
|
112
|
+
- **Renders are serialized per process** (one shared jsdom document). The
|
|
113
|
+
server is stateless, so scale by running several replicas behind a load
|
|
114
|
+
balancer rather than expecting concurrency from one instance.
|
|
115
|
+
|
|
116
|
+
A container image needs nothing unusual:
|
|
117
|
+
|
|
118
|
+
```dockerfile
|
|
119
|
+
FROM node:24-slim
|
|
120
|
+
RUN npm install -g @unovis/mcp
|
|
121
|
+
# Pre-provision fonts so containers don't download them on first render
|
|
122
|
+
ENV UNOVIS_MCP_FONTS_DIR=/fonts
|
|
123
|
+
COPY fonts/ /fonts/
|
|
124
|
+
EXPOSE 3737
|
|
125
|
+
CMD ["unovis-mcp", "--transport", "http", "--host", "0.0.0.0"]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## CLI options
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
--transport <stdio|http> Transport (default: stdio)
|
|
132
|
+
--host <host> HTTP bind address (default: 127.0.0.1)
|
|
133
|
+
--port <port> HTTP port (default: 3737)
|
|
134
|
+
--endpoint <path> HTTP endpoint path (default: /mcp)
|
|
135
|
+
--tools <a,b,...> Expose only these tools
|
|
136
|
+
--version Print the version
|
|
137
|
+
--help Show usage
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Diagnostics go to stderr; stdout carries only protocol traffic.
|
|
141
|
+
|
|
142
|
+
## Environment variables
|
|
143
|
+
|
|
144
|
+
| Variable | Purpose |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `DISABLED_TOOLS` | Comma-separated tool names to hide |
|
|
147
|
+
| `UNOVIS_MCP_FONTS_DIR` | Directory of font files to use for text measurement |
|
|
148
|
+
| `UNOVIS_MCP_NO_DOWNLOAD` | Set to `1` to skip the one-time Inter download and use system fonts |
|
|
149
|
+
|
|
150
|
+
## Limiting the tool surface
|
|
151
|
+
|
|
152
|
+
Some clients degrade with large tool counts, and you may simply not want maps
|
|
153
|
+
or graphs. Both directions are supported:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# Allowlist
|
|
157
|
+
unovis-mcp --tools generate_line_chart,generate_bar_chart,generate_donut_chart
|
|
158
|
+
|
|
159
|
+
# Denylist
|
|
160
|
+
DISABLED_TOOLS=generate_choropleth_map,generate_network_graph unovis-mcp
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`get_unovis_info` always reports the tools that are actually active, so an
|
|
164
|
+
agent can discover the trimmed surface at runtime.
|
|
165
|
+
|
|
166
|
+
## First render
|
|
167
|
+
|
|
168
|
+
Ask your agent something like:
|
|
169
|
+
|
|
170
|
+
> Chart the deploy counts in `deploys.csv` by week as a bar chart, and save it
|
|
171
|
+
> to `docs/deploys.svg`
|
|
172
|
+
|
|
173
|
+
A well-behaved agent reads the file, calls `generate_bar_chart` with the parsed
|
|
174
|
+
rows and `outputPath: "/abs/path/docs/deploys.svg"`, and tells you where the
|
|
175
|
+
file landed.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Interactive charts
|
|
2
|
+
|
|
3
|
+
Static SVG and PNG are the universal baseline. When you want hover, tooltips and
|
|
4
|
+
zoom, the same chart spec can be delivered as a live chart instead — driven by a
|
|
5
|
+
browser bundle that shares the spec layer with the headless renderer.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
ChartSpec ──┬──► headless renderer ──► SVG / PNG (works everywhere)
|
|
9
|
+
├──► widget bundle ──► .html file (any browser)
|
|
10
|
+
└──► widget bundle ──► ui:// resource (MCP UI clients)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What you get
|
|
14
|
+
|
|
15
|
+
- **Hover tooltips** tailored per chart type — bars and points show their
|
|
16
|
+
record, sankey nodes show totals, map areas show their value, graph links show
|
|
17
|
+
their endpoints.
|
|
18
|
+
- **A crosshair** on line and area charts, with a shared readout of every series
|
|
19
|
+
at the hovered x position.
|
|
20
|
+
- **Unovis's real HTML legend** (`BulletLegend`) — impossible in standalone SVG,
|
|
21
|
+
because it isn't SVG.
|
|
22
|
+
- **Component interactions** that come free with a live chart: graph drag and
|
|
23
|
+
zoom, map panning (when enabled), label collision handling.
|
|
24
|
+
- **Responsive layout**: the chart fills its container and re-renders on resize.
|
|
25
|
+
|
|
26
|
+
## Self-contained HTML
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{ "outputType": "html", "outputPath": "/abs/path/revenue.html" }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
or programmatically:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { buildChartDocument } from '@unovis/mcp'
|
|
36
|
+
|
|
37
|
+
const html = buildChartDocument(spec, {
|
|
38
|
+
duration: 400, // animation ms; 0 renders immediately
|
|
39
|
+
documentTitle: 'Q3 revenue',
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
One file, nothing external: the widget bundle (~665kB), the spec, and the styles
|
|
44
|
+
are all inlined. It opens offline, survives being emailed, and can be committed
|
|
45
|
+
to a repo.
|
|
46
|
+
|
|
47
|
+
## Inline in the conversation
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{ "outputType": "interactive" }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The tool returns the spec as structured content plus a reference to the
|
|
54
|
+
`ui://unovis/chart` resource, which is the widget in embed mode. Clients that
|
|
55
|
+
implement MCP UI resources render it inline; the others fall back to the text
|
|
56
|
+
summary.
|
|
57
|
+
|
|
58
|
+
This is **experimental** — the conventions are still moving, so don't build a
|
|
59
|
+
product on it yet. `html` is the dependable path.
|
|
60
|
+
|
|
61
|
+
## Embedding the widget in your own page
|
|
62
|
+
|
|
63
|
+
The widget doubles as a plain iframe component, so any web app can render Unovis
|
|
64
|
+
specs without bundling Unovis. Serve the embed document (it's what the `ui://`
|
|
65
|
+
resource returns, and `buildEmbedDocument()` produces it), point an iframe at it
|
|
66
|
+
with the `#embed` hash, and post it a spec:
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<iframe id="chart" src="/unovis-widget.html#embed" style="width:100%;border:0"></iframe>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
const frame = document.getElementById('chart')
|
|
73
|
+
|
|
74
|
+
window.addEventListener('message', (event) => {
|
|
75
|
+
if (event.data?.type === 'unovis:ready') {
|
|
76
|
+
// The widget is loaded and waiting for a spec
|
|
77
|
+
frame.contentWindow.postMessage({ type: 'unovis:render', spec, options: { duration: 400 } }, '*')
|
|
78
|
+
}
|
|
79
|
+
if (event.data?.type === 'unovis:size') {
|
|
80
|
+
// Grow the iframe to fit its content
|
|
81
|
+
frame.style.height = `${event.data.height}px`
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
</script>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Protocol
|
|
88
|
+
|
|
89
|
+
| Direction | Message | Meaning |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| widget → host | `{ type: 'unovis:ready' }` | Loaded, waiting for a spec |
|
|
92
|
+
| host → widget | `{ type: 'unovis:render', spec, options }` | Render this spec (replaces any previous chart) |
|
|
93
|
+
| widget → host | `{ type: 'unovis:size', width, height }` | Content size after a render |
|
|
94
|
+
|
|
95
|
+
Send `unovis:render` as often as you like — each one tears down the previous
|
|
96
|
+
chart. `options` accepts `duration` and `showTitle`.
|
|
97
|
+
|
|
98
|
+
## Using the widget API directly
|
|
99
|
+
|
|
100
|
+
If you'd rather not use an iframe, the bundle exposes a global:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const handle = window.UnovisChart.render(spec, document.getElementById('chart'), { duration: 400 })
|
|
104
|
+
// …later
|
|
105
|
+
handle.destroy()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`window.UnovisChart.unovis` is the full Unovis namespace from the bundle, if you
|
|
109
|
+
want to build charts by hand in the same page.
|
|
110
|
+
|
|
111
|
+
## Limits
|
|
112
|
+
|
|
113
|
+
- **Graph layouts**: `force`, `circular`, `concentric` and `dagre` all work. Only
|
|
114
|
+
`elk` is excluded — at 1.4MB its engine would have to be inlined into every
|
|
115
|
+
generated file.
|
|
116
|
+
- **A background tab renders nothing until you look at it.** Browsers pause
|
|
117
|
+
`requestAnimationFrame` for hidden pages, and Unovis schedules its rendering
|
|
118
|
+
through it. The chart appears as soon as the tab becomes visible. This is
|
|
119
|
+
ordinary browser behavior, but it surprises people (and it will make an
|
|
120
|
+
automated screenshot of a hidden page come out blank).
|
|
121
|
+
- **Fonts** come from the viewer's machine. The document asks for the Inter
|
|
122
|
+
stack and falls back to system UI fonts, so text can be a few pixels wider or
|
|
123
|
+
narrower than in the SVG output, which measures with bundled Inter.
|
|
124
|
+
- **Map charts** embed their topojson in the file, which makes world maps
|
|
125
|
+
noticeably larger than other charts.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Output types
|
|
2
|
+
|
|
3
|
+
Every chart tool accepts the same `outputType`, so you pick a chart once and
|
|
4
|
+
decide separately how you want it delivered.
|
|
5
|
+
|
|
6
|
+
| `outputType` | Returns | Best for |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| `svg` (default) | Standalone SVG markup as text | Embedding in docs, READMEs, anything; works in every client |
|
|
9
|
+
| `png` | A rendered PNG image (base64) | Chat clients that display images; slides; anywhere SVG isn't accepted |
|
|
10
|
+
| `html` | Path to a self-contained interactive file | Exploration: tooltips, crosshair, hover |
|
|
11
|
+
| `interactive` | The chart spec + a widget reference | Clients that render MCP UI resources inline |
|
|
12
|
+
| `config` | The resolved chart spec as JSON | Iterating on a chart before rendering; feeding another tool |
|
|
13
|
+
| `code` | Ready-to-paste Unovis source | Putting the chart into your own app |
|
|
14
|
+
|
|
15
|
+
## Shared options
|
|
16
|
+
|
|
17
|
+
These exist on every chart tool:
|
|
18
|
+
|
|
19
|
+
| Option | Type | Default | Notes |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| `width` | number | `800` | Final image width in pixels |
|
|
22
|
+
| `height` | number | `480` | Final image height, before the title/legend header is added |
|
|
23
|
+
| `theme` | `light` \| `dark` | `light` | Dark theme bakes Unovis's dark palette and a dark background |
|
|
24
|
+
| `title` | string | — | Rendered as a heading above the chart |
|
|
25
|
+
| `colors` | string[] | — | Hex palette replacing the default one, in order |
|
|
26
|
+
| `outputType` | see above | `svg` | |
|
|
27
|
+
| `outputPath` | string | — | Absolute path with an extension matching `outputType` (`.svg`, `.png`, `.html`) |
|
|
28
|
+
| `scale` | 1–4 | `2` | PNG pixel density only |
|
|
29
|
+
| `framework` | `ts` \| `react` \| `svelte` \| `vue` \| `angular` \| `solid` | `ts` | `code` output only |
|
|
30
|
+
|
|
31
|
+
`width` and `height` describe the **image**, not the plot area. The chart is
|
|
32
|
+
drawn inside a 16px frame (12px at the top) so nothing touches the edges, and
|
|
33
|
+
the title/legend header adds height on top.
|
|
34
|
+
|
|
35
|
+
## `svg`
|
|
36
|
+
|
|
37
|
+
The default, and the most portable. The returned markup is genuinely
|
|
38
|
+
standalone:
|
|
39
|
+
|
|
40
|
+
- stylesheet rules are inlined as presentation attributes, and the emotion
|
|
41
|
+
classes are stripped
|
|
42
|
+
- `var(--vis-*)` theme references are baked to literal colors for the requested
|
|
43
|
+
theme
|
|
44
|
+
- element ids are rewritten with a per-render prefix, so several charts can
|
|
45
|
+
coexist in one document without clashing
|
|
46
|
+
- no external references at all — no fonts, stylesheets, or images to fetch
|
|
47
|
+
|
|
48
|
+
That means it renders correctly inside `<img>`, in GitHub markdown, in Figma,
|
|
49
|
+
and in rasterizers that don't implement CSS custom properties.
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{ "outputType": "svg" }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With `outputPath`, the file gets an XML declaration and the tool returns the
|
|
56
|
+
path instead of the markup — useful for large charts you don't want in the
|
|
57
|
+
conversation.
|
|
58
|
+
|
|
59
|
+
## `png`
|
|
60
|
+
|
|
61
|
+
Rasterized locally with [resvg](https://github.com/thx/resvg-js) from the very
|
|
62
|
+
same SVG, using the same fonts that were used to measure the text. `scale`
|
|
63
|
+
multiplies the pixel dimensions (`2` gives a retina-quality image).
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{ "outputType": "png", "scale": 2, "width": 800, "height": 480 }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Returned as base64 image content, which most graphical clients render inline.
|
|
70
|
+
Terminal clients generally can't — use `outputPath` there.
|
|
71
|
+
|
|
72
|
+
## `html`
|
|
73
|
+
|
|
74
|
+
Writes a **self-contained interactive** document and returns its path. Opening
|
|
75
|
+
it gives you the real chart: hover tooltips, a crosshair readout on line and
|
|
76
|
+
area charts, hover highlighting, drag/zoom on graphs, and Unovis's real HTML
|
|
77
|
+
legend. Everything (the widget bundle, the spec, the styles) is inlined, so it
|
|
78
|
+
works offline and can be emailed or committed.
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{ "outputType": "html", "outputPath": "/abs/path/revenue.html" }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Without `outputPath` the file goes to a temp directory. It is always written to
|
|
85
|
+
disk rather than returned inline — the document is ~600kB.
|
|
86
|
+
|
|
87
|
+
See [Interactive charts](./interactive.md) for what's supported and how the
|
|
88
|
+
widget works.
|
|
89
|
+
|
|
90
|
+
## `interactive`
|
|
91
|
+
|
|
92
|
+
Returns the chart spec as structured content plus a reference to the
|
|
93
|
+
`ui://unovis/chart` widget resource, letting clients that support MCP UI
|
|
94
|
+
resources render the chart **inside the conversation**. The text content is a
|
|
95
|
+
short summary, which is what clients without widget support will show.
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{ "outputType": "interactive" }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This is experimental: the conventions for embedded UI in MCP are still
|
|
102
|
+
settling, so treat client support as best-effort and prefer `html` when you
|
|
103
|
+
need a guaranteed result.
|
|
104
|
+
|
|
105
|
+
## `config`
|
|
106
|
+
|
|
107
|
+
Returns the resolved chart spec as JSON without rendering anything. Two good
|
|
108
|
+
uses: letting an agent inspect and adjust a chart before spending time on
|
|
109
|
+
pixels, and hand-editing a spec to reach options the tool schemas don't expose.
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{ "outputType": "config" }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The result can be fed straight to `renderChart()` — see
|
|
116
|
+
[Chart spec](./chart-spec.md) and [Programmatic use](./programmatic.md).
|
|
117
|
+
|
|
118
|
+
## `code`
|
|
119
|
+
|
|
120
|
+
Emits ready-to-paste Unovis source for your framework, with the data included
|
|
121
|
+
so it runs as-is.
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{ "outputType": "code", "framework": "react" }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import React from 'react'
|
|
129
|
+
import { VisAxis, VisGroupedBar, VisXYContainer } from '@unovis/react'
|
|
130
|
+
|
|
131
|
+
const data = [
|
|
132
|
+
{ q: 'Q1', rev: 42 },
|
|
133
|
+
{ q: 'Q2', rev: 51 },
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
const categories = [
|
|
137
|
+
'Q1',
|
|
138
|
+
'Q2'
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
const formatNumber = (value: number | Date): string =>
|
|
142
|
+
Number(value).toLocaleString('en-US', { maximumFractionDigits: 6 })
|
|
143
|
+
|
|
144
|
+
export default function Chart (): JSX.Element {
|
|
145
|
+
return (
|
|
146
|
+
<>
|
|
147
|
+
<VisXYContainer height={480}>
|
|
148
|
+
<VisGroupedBar data={data} x={(d, i) => i} y={[d => d.rev]} orientation="vertical" roundedCorners={true} />
|
|
149
|
+
<VisAxis type="x" gridLine={false} tickFormat={(index: number | Date) => categories[Math.round(Number(index))]} tickValues={[0, 1]} />
|
|
150
|
+
<VisAxis type="y" gridLine={true} tickFormat={formatNumber} />
|
|
151
|
+
</VisXYContainer>
|
|
152
|
+
</>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Notes on the generated code:
|
|
158
|
+
|
|
159
|
+
- Each framework gets its real syntax: `Vis*` components for React, Solid,
|
|
160
|
+
Svelte and Vue; `<vis-*>` elements for Angular. Angular returns **two files**
|
|
161
|
+
(template plus component class), because Angular templates can't contain
|
|
162
|
+
arrow functions — accessors become typed class fields.
|
|
163
|
+
- Enum-typed props emit the enum member and its import (for example
|
|
164
|
+
`curveType={CurveType.MonotoneX}` from `@unovis/ts`) rather than a bare
|
|
165
|
+
string, which wouldn't type-check.
|
|
166
|
+
- Map charts import their topojson (`import { WorldMapTopoJSON } from
|
|
167
|
+
'@unovis/ts/maps'`) instead of inlining megabytes of geometry.
|
|
168
|
+
- The vanilla-TypeScript target is type-checked against `@unovis/ts` in this
|
|
169
|
+
package's test suite, so the emitted imports, generics and prop names are
|
|
170
|
+
known to be real.
|