@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.
Files changed (153) hide show
  1. package/LICENSE +193 -0
  2. package/README.md +154 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +109 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/codegen/index.d.ts +14 -0
  7. package/dist/codegen/index.js +346 -0
  8. package/dist/codegen/index.js.map +1 -0
  9. package/dist/env/bbox.d.ts +13 -0
  10. package/dist/env/bbox.js +335 -0
  11. package/dist/env/bbox.js.map +1 -0
  12. package/dist/env/canvas.d.ts +9 -0
  13. package/dist/env/canvas.js +55 -0
  14. package/dist/env/canvas.js.map +1 -0
  15. package/dist/env/computed-style.d.ts +38 -0
  16. package/dist/env/computed-style.js +186 -0
  17. package/dist/env/computed-style.js.map +1 -0
  18. package/dist/env/fonts.d.ts +3 -0
  19. package/dist/env/fonts.js +103 -0
  20. package/dist/env/fonts.js.map +1 -0
  21. package/dist/env/index.d.ts +13 -0
  22. package/dist/env/index.js +121 -0
  23. package/dist/env/index.js.map +1 -0
  24. package/dist/env/raf-queue.d.ts +23 -0
  25. package/dist/env/raf-queue.js +51 -0
  26. package/dist/env/raf-queue.js.map +1 -0
  27. package/dist/env/size.d.ts +9 -0
  28. package/dist/env/size.js +30 -0
  29. package/dist/env/size.js.map +1 -0
  30. package/dist/html/document.d.ts +16 -0
  31. package/dist/html/document.js +95 -0
  32. package/dist/html/document.js.map +1 -0
  33. package/dist/index.d.ts +25 -0
  34. package/dist/index.js +18 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/recipes/area.d.ts +66 -0
  37. package/dist/recipes/area.js +76 -0
  38. package/dist/recipes/area.js.map +1 -0
  39. package/dist/recipes/bar.d.ts +67 -0
  40. package/dist/recipes/bar.js +69 -0
  41. package/dist/recipes/bar.js.map +1 -0
  42. package/dist/recipes/boxplot.d.ts +64 -0
  43. package/dist/recipes/boxplot.js +89 -0
  44. package/dist/recipes/boxplot.js.map +1 -0
  45. package/dist/recipes/chord.d.ts +41 -0
  46. package/dist/recipes/chord.js +72 -0
  47. package/dist/recipes/chord.js.map +1 -0
  48. package/dist/recipes/choropleth-regions.d.ts +49 -0
  49. package/dist/recipes/choropleth-regions.js +521 -0
  50. package/dist/recipes/choropleth-regions.js.map +1 -0
  51. package/dist/recipes/choropleth.d.ts +27 -0
  52. package/dist/recipes/choropleth.js +86 -0
  53. package/dist/recipes/choropleth.js.map +1 -0
  54. package/dist/recipes/donut.d.ts +25 -0
  55. package/dist/recipes/donut.js +51 -0
  56. package/dist/recipes/donut.js.map +1 -0
  57. package/dist/recipes/graph.d.ts +55 -0
  58. package/dist/recipes/graph.js +132 -0
  59. package/dist/recipes/graph.js.map +1 -0
  60. package/dist/recipes/heatmap.d.ts +21 -0
  61. package/dist/recipes/heatmap.js +79 -0
  62. package/dist/recipes/heatmap.js.map +1 -0
  63. package/dist/recipes/index.d.ts +5 -0
  64. package/dist/recipes/index.js +35 -0
  65. package/dist/recipes/index.js.map +1 -0
  66. package/dist/recipes/line.d.ts +69 -0
  67. package/dist/recipes/line.js +64 -0
  68. package/dist/recipes/line.js.map +1 -0
  69. package/dist/recipes/nested-donut.d.ts +22 -0
  70. package/dist/recipes/nested-donut.js +47 -0
  71. package/dist/recipes/nested-donut.js.map +1 -0
  72. package/dist/recipes/radial-bar.d.ts +25 -0
  73. package/dist/recipes/radial-bar.js +53 -0
  74. package/dist/recipes/radial-bar.js.map +1 -0
  75. package/dist/recipes/sankey.d.ts +41 -0
  76. package/dist/recipes/sankey.js +61 -0
  77. package/dist/recipes/sankey.js.map +1 -0
  78. package/dist/recipes/scatter.d.ts +68 -0
  79. package/dist/recipes/scatter.js +75 -0
  80. package/dist/recipes/scatter.js.map +1 -0
  81. package/dist/recipes/shared.d.ts +144 -0
  82. package/dist/recipes/shared.js +212 -0
  83. package/dist/recipes/shared.js.map +1 -0
  84. package/dist/recipes/timeline.d.ts +31 -0
  85. package/dist/recipes/timeline.js +123 -0
  86. package/dist/recipes/timeline.js.map +1 -0
  87. package/dist/recipes/treemap.d.ts +19 -0
  88. package/dist/recipes/treemap.js +43 -0
  89. package/dist/recipes/treemap.js.map +1 -0
  90. package/dist/recipes/types.d.ts +16 -0
  91. package/dist/recipes/types.js +2 -0
  92. package/dist/recipes/types.js.map +1 -0
  93. package/dist/render/headless.d.ts +62 -0
  94. package/dist/render/headless.js +158 -0
  95. package/dist/render/headless.js.map +1 -0
  96. package/dist/render/materialize.d.ts +35 -0
  97. package/dist/render/materialize.js +160 -0
  98. package/dist/render/materialize.js.map +1 -0
  99. package/dist/render/mutex.d.ts +9 -0
  100. package/dist/render/mutex.js +14 -0
  101. package/dist/render/mutex.js.map +1 -0
  102. package/dist/render/rasterize.d.ts +10 -0
  103. package/dist/render/rasterize.js +29 -0
  104. package/dist/render/rasterize.js.map +1 -0
  105. package/dist/render/renderer.d.ts +21 -0
  106. package/dist/render/renderer.js +66 -0
  107. package/dist/render/renderer.js.map +1 -0
  108. package/dist/render/spec.d.ts +95 -0
  109. package/dist/render/spec.js +15 -0
  110. package/dist/render/spec.js.map +1 -0
  111. package/dist/server.d.ts +4 -0
  112. package/dist/server.js +44 -0
  113. package/dist/server.js.map +1 -0
  114. package/dist/svg/collect-css.d.ts +14 -0
  115. package/dist/svg/collect-css.js +53 -0
  116. package/dist/svg/collect-css.js.map +1 -0
  117. package/dist/svg/css-vars.d.ts +17 -0
  118. package/dist/svg/css-vars.js +64 -0
  119. package/dist/svg/css-vars.js.map +1 -0
  120. package/dist/svg/header.d.ts +8 -0
  121. package/dist/svg/header.js +81 -0
  122. package/dist/svg/header.js.map +1 -0
  123. package/dist/svg/ids.d.ts +12 -0
  124. package/dist/svg/ids.js +94 -0
  125. package/dist/svg/ids.js.map +1 -0
  126. package/dist/svg/postprocess.d.ts +41 -0
  127. package/dist/svg/postprocess.js +135 -0
  128. package/dist/svg/postprocess.js.map +1 -0
  129. package/dist/tools/register.d.ts +12 -0
  130. package/dist/tools/register.js +127 -0
  131. package/dist/tools/register.js.map +1 -0
  132. package/dist/widget/bundle.js +1777 -0
  133. package/dist/widget/bundle.meta.json +3 -0
  134. package/dist/widget/entry.d.ts +34 -0
  135. package/dist/widget/entry.js +118 -0
  136. package/dist/widget/entry.js.map +1 -0
  137. package/dist/widget/interactions.d.ts +14 -0
  138. package/dist/widget/interactions.js +151 -0
  139. package/dist/widget/interactions.js.map +1 -0
  140. package/dist/widget/unovis-slim.d.ts +33 -0
  141. package/dist/widget/unovis-slim.js +34 -0
  142. package/dist/widget/unovis-slim.js.map +1 -0
  143. package/docs/README.md +56 -0
  144. package/docs/architecture.md +158 -0
  145. package/docs/chart-spec.md +148 -0
  146. package/docs/getting-started.md +175 -0
  147. package/docs/interactive.md +125 -0
  148. package/docs/output-types.md +170 -0
  149. package/docs/programmatic.md +234 -0
  150. package/docs/tools.md +273 -0
  151. package/docs/troubleshooting.md +140 -0
  152. package/fonts/README.md +19 -0
  153. 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.