semiotic 3.7.0 → 3.7.1

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 (63) hide show
  1. package/CLAUDE.md +3 -2
  2. package/README.md +30 -7
  3. package/ai/dist/mcp-server.js +417 -5
  4. package/ai/schema.json +1 -1
  5. package/ai/system-prompt.md +3 -3
  6. package/dist/components/AccessibleNavTree.d.ts +2 -1
  7. package/dist/components/Annotation.d.ts +1 -1
  8. package/dist/components/CategoryColors.d.ts +1 -1
  9. package/dist/components/ChartGrid.d.ts +1 -1
  10. package/dist/components/ContextLayout.d.ts +1 -1
  11. package/dist/components/DataSummaryContext.d.ts +1 -1
  12. package/dist/components/DetailsPanel.d.ts +1 -1
  13. package/dist/components/Legend.d.ts +3 -2
  14. package/dist/components/LinkedCharts.d.ts +1 -1
  15. package/dist/components/ThemeProvider.d.ts +1 -1
  16. package/dist/components/Tooltip/FlippingTooltip.d.ts +1 -1
  17. package/dist/components/Tooltip/Tooltip.d.ts +2 -2
  18. package/dist/components/ai/qualityScorecard.d.ts +11 -0
  19. package/dist/components/charts/geo/ChoroplethMap.d.ts +2 -1
  20. package/dist/components/charts/network/CirclePack.d.ts +2 -1
  21. package/dist/components/charts/network/OrbitDiagram.d.ts +1 -1
  22. package/dist/components/charts/network/TreeDiagram.d.ts +2 -1
  23. package/dist/components/charts/network/Treemap.d.ts +2 -1
  24. package/dist/components/charts/realtime/RealtimeHistogram.d.ts +1 -1
  25. package/dist/components/charts/shared/ChartError.d.ts +2 -1
  26. package/dist/components/charts/shared/withChartWrapper.d.ts +1 -1
  27. package/dist/components/charts/xy/MinimapChart.d.ts +2 -1
  28. package/dist/components/charts/xy/ScatterplotMatrix.d.ts +2 -1
  29. package/dist/components/semiotic-server.d.ts +2 -1
  30. package/dist/components/server/renderEvidence.d.ts +92 -0
  31. package/dist/components/server/renderToStaticSVG.d.ts +14 -1
  32. package/dist/components/stream/AccessibleDataTable.d.ts +5 -5
  33. package/dist/components/stream/FocusRing.d.ts +2 -1
  34. package/dist/components/stream/MarginalGraphics.d.ts +2 -1
  35. package/dist/components/stream/NetworkSVGOverlay.d.ts +2 -1
  36. package/dist/components/stream/OrdinalBrushOverlay.d.ts +19 -1
  37. package/dist/components/stream/OrdinalSVGOverlay.d.ts +2 -2
  38. package/dist/components/stream/SVGOverlay.d.ts +3 -2
  39. package/dist/components/stream/XYBrushOverlay.d.ts +21 -1
  40. package/dist/geo.min.js +1 -1
  41. package/dist/geo.module.min.js +1 -1
  42. package/dist/network.min.js +1 -1
  43. package/dist/network.module.min.js +1 -1
  44. package/dist/ordinal.min.js +1 -1
  45. package/dist/ordinal.module.min.js +1 -1
  46. package/dist/realtime.min.js +1 -1
  47. package/dist/realtime.module.min.js +1 -1
  48. package/dist/semiotic-ai.min.js +1 -1
  49. package/dist/semiotic-ai.module.min.js +1 -1
  50. package/dist/semiotic-server.d.ts +2 -1
  51. package/dist/semiotic-themes.min.js +1 -1
  52. package/dist/semiotic-themes.module.min.js +1 -1
  53. package/dist/semiotic-utils.min.js +1 -1
  54. package/dist/semiotic-utils.module.min.js +1 -1
  55. package/dist/semiotic-value.min.js +1 -1
  56. package/dist/semiotic-value.module.min.js +1 -1
  57. package/dist/semiotic.min.js +1 -1
  58. package/dist/semiotic.module.min.js +1 -1
  59. package/dist/server.min.js +1 -1
  60. package/dist/server.module.min.js +1 -1
  61. package/dist/xy.min.js +1 -1
  62. package/dist/xy.module.min.js +1 -1
  63. package/package.json +2 -2
package/CLAUDE.md CHANGED
@@ -4,7 +4,7 @@
4
4
  - Install: `npm install semiotic`
5
5
  <!-- semiotic-bundle-sizes:start -->
6
6
  <!-- Auto-generated by scripts/sync-bundle-sizes.mjs — do not edit by hand. -->
7
- - **Use sub-path imports** — `semiotic/xy` (90KB gz), `semiotic/ordinal` (74KB gz), `semiotic/network` (68KB gz), `semiotic/geo` (55KB gz), `semiotic/realtime` (95KB gz), `semiotic/server` (127KB gz), `semiotic/utils` (37KB gz), `semiotic/recipes` (9KB gz), `semiotic/themes` (4KB gz), `semiotic/data` (3KB gz), `semiotic/value` (6KB gz), `semiotic/ai` (246KB gz). Full `semiotic` is 203KB gz.
7
+ - **Use sub-path imports** — `semiotic/xy` (90KB gz), `semiotic/ordinal` (74KB gz), `semiotic/network` (68KB gz), `semiotic/geo` (55KB gz), `semiotic/realtime` (95KB gz), `semiotic/server` (128KB gz), `semiotic/utils` (38KB gz), `semiotic/recipes` (9KB gz), `semiotic/themes` (4KB gz), `semiotic/data` (3KB gz), `semiotic/value` (6KB gz), `semiotic/ai` (250KB gz). Full `semiotic` is 203KB gz.
8
8
  <!-- semiotic-bundle-sizes:end -->
9
9
  - CLI: `npx semiotic-ai [--schema|--compact|--examples|--doctor|--audit-a11y]` · MCP: `npx semiotic-mcp`
10
10
 
@@ -160,9 +160,10 @@ Also: **ScatterplotMatrix**, **ChartContainer** (`title`, `subtitle`, `actions`)
160
160
  HOC charts render SVG automatically in server environments. For standalone generation:
161
161
 
162
162
  ```ts
163
- import { renderChart, renderToImage, renderToAnimatedGif, renderDashboard } from "semiotic/server"
163
+ import { renderChart, renderChartWithEvidence, renderToImage, renderToAnimatedGif, renderDashboard } from "semiotic/server"
164
164
 
165
165
  const svg = renderChart("BarChart", { data, categoryAccessor, valueAccessor, theme: "tufte", showLegend, showGrid, annotations })
166
+ const { svg: svg2, evidence } = renderChartWithEvidence("BarChart", { data, categoryAccessor, valueAccessor }) // evidence: { markCount, markCountByType, empty, xDomain?, yDomain?, categories?, nodeCount?, edgeCount?, annotationCount, ariaLabel, warnings } — ground truth from the rendered scene; check `evidence.empty`/`markCount` instead of parsing SVG. MCP renderChart returns the same block.
166
167
  const png = await renderToImage("LineChart", { ... }, { format: "png", scale: 2 }) // requires sharp
167
168
  const gif = await renderToAnimatedGif("line", data, { xAccessor, yAccessor, theme: "dark" }, { fps: 12, transitionFrames: 4, decay: { type: "linear" } }) // requires sharp + gifenc
168
169
  const dashboard = renderDashboard([{ component: "BarChart", props }, { component: "PieChart", colSpan: 2, props }], { title, theme, layout: { columns: 2 } })
package/README.md CHANGED
@@ -35,11 +35,11 @@ generate correct code without examples.
35
35
  Semiotic ships with everything an AI coding assistant needs to generate
36
36
  correct visualizations without trial and error:
37
37
 
38
- - **`semiotic/ai`** — a single import with the 47-chart capability catalog (XY, ordinal, network, realtime, geo, value), optimized for LLM code generation. Prefer family subpaths such as `semiotic/xy`, `semiotic/geo`, and `semiotic/value` when bundle size matters.
38
+ - **`semiotic/ai`** — a single import with the 47-chart capability catalog (XY, ordinal, network, realtime, geo, value), optimized for LLM code generation. Note: the published entry files are pre-bundled, so importing one chart from `semiotic/ai` still ships most of the bundle — treat it as a codegen/tooling surface and use family subpaths (`semiotic/xy`, `semiotic/geo`, `semiotic/value`, …) in production code, at roughly half the single-chart cost.
39
39
  - **`ai/schema.json`** — machine-readable prop schemas for every component
40
40
  - **`npx semiotic-mcp`** — an MCP server for tool-based chart rendering in any MCP client
41
41
  - **`npx semiotic-ai --doctor`** — validate component + props JSON from the command line with typo suggestions and anti-pattern detection
42
- - **`diagnoseConfig(component, props)`** — programmatic anti-pattern detector with 12 checks and actionable fixes
42
+ - **`diagnoseConfig(component, props)`** — programmatic anti-pattern detector with actionable fixes, spanning validation, encoding, accessibility, and misleading-design (deception) checks
43
43
  - **`CLAUDE.md`** — instruction files auto-synced for Claude, Cursor, Copilot, Windsurf, and Cline
44
44
  - **`llms.txt`** — machine-readable documentation following the emerging standard
45
45
 
@@ -320,13 +320,13 @@ Semiotic ships 12 entry points. **Don't import from `"semiotic"` unless you need
320
320
  | `semiotic/network` | **68 KB** | ForceDirectedGraph, SankeyDiagram, ProcessSankey, Treemap, + 4 more |
321
321
  | `semiotic/geo` | **55 KB** | ChoroplethMap, FlowMap, DistanceCartogram, ProportionalSymbolMap |
322
322
  | `semiotic/realtime` | **95 KB** | RealtimeLineChart, RealtimeHistogram, + 4 streaming charts |
323
- | `semiotic/server` | **127 KB** | renderChart, renderDashboard, renderToImage, renderToAnimatedGif |
324
- | `semiotic/utils` | **37 KB** | ThemeProvider, validators, serialization — no chart components |
323
+ | `semiotic/server` | **128 KB** | renderChart, renderDashboard, renderToImage, renderToAnimatedGif |
324
+ | `semiotic/utils` | **38 KB** | ThemeProvider, validators, serialization — no chart components |
325
325
  | `semiotic/recipes` | **9 KB** | Pure layout functions (waffle, marimekko, flextree, dagre, …) |
326
326
  | `semiotic/themes` | **4 KB** | Theme presets only (tufte, carbon, etc.) |
327
327
  | `semiotic/data` | **3 KB** | bin, rollup, groupBy, pivot, fromVegaLite |
328
328
  | `semiotic/value` | **6 KB** | BigNumber — focal-value KPI / scorecard (SingleValueFrame POC) |
329
- | `semiotic/ai` | **246 KB** | All 47 schema-backed charts + validation — optimized for LLM code generation |
329
+ | `semiotic/ai` | **250 KB** | All 47 schema-backed charts + validation — optimized for LLM code generation |
330
330
  | `semiotic` | **203 KB** | Everything below (full bundle) |
331
331
 
332
332
  <!-- semiotic-bundle-sizes:end -->
@@ -411,13 +411,16 @@ Add to your MCP client config (e.g. `claude_desktop_config.json` for Claude Desk
411
411
  }
412
412
  ```
413
413
 
414
- No API keys or authentication required. The server runs locally via stdio. HTTP mode is also available for inspectors and web clients: `npx semiotic-mcp --http --port 3001`.
414
+ No API keys or authentication required. The server runs locally via stdio. HTTP mode is also available for inspectors, web clients, and ChatGPT Apps SDK experiments: `npx semiotic-mcp --http --port 3001`.
415
+
416
+ For ChatGPT developer mode, expose the HTTP endpoint over HTTPS with a tunnel and create a connector that points at `https://<your-tunnel>/mcp`. The experimental Apps SDK surface is `renderInteractiveChart`, which returns a `text/html;profile=mcp-app` widget template plus a hidden SVG payload rendered by Semiotic on the MCP server.
415
417
 
416
418
  ### Tools
417
419
 
418
420
  | Tool | Description |
419
421
  |------|-------------|
420
- | **`renderChart`** | Render a Semiotic chart to static SVG. Supports the components returned by `getSchema` that are marked `[renderable]`. Pass `{ component: "LineChart", props: { data: [...], xAccessor: "x", yAccessor: "y" } }`. Returns SVG string or validation errors with fix suggestions. |
422
+ | **`renderChart`** | Render a Semiotic chart to static SVG. Supports the components returned by `getSchema` that are marked `[renderable]`. Pass `{ component: "LineChart", props: { data: [...], xAccessor: "x", yAccessor: "y" } }`. Returns SVG string plus a "Render evidence" JSON block (mark counts by scene type, resolved axis domains, empty flag, annotation count, accessible name) so agents can verify the chart drew data marks, or validation errors with fix suggestions. |
423
+ | **`renderInteractiveChart`** | Render a static-data chart as a ChatGPT Apps widget. Uses the same Semiotic server render path as `renderChart`, then hydrates an iframe UI with fit, zoom, data, hover, and render-evidence controls. |
421
424
  | **`getSchema`** | Return the prop schema for a specific component. Pass `{ component: "LineChart" }` to get its props, or omit `component` to list all 47 chart schemas. Components marked `[renderable]` are available through `renderChart`; realtime charts require a browser/live environment. |
422
425
  | **`suggestChart`** | Legacy sample-row recommender. Pass `{ data: [{...}, ...] }` with 1–5 sample objects plus optional broad intent/capability filters. |
423
426
  | **`suggestCharts`** | Capability-based recommender for bounded row data. Returns ranked chart suggestions with scores, reasons, caveats, import paths, and ready-to-use props. |
@@ -439,6 +442,7 @@ No API keys or authentication required. The server runs locally via stdio. HTTP
439
442
  | **`semiotic://behavior-contracts`** | Agent-visible semantic rules for color precedence, required prop combinations, push refs, and renderability. |
440
443
  | **`semiotic://system-prompt`** | Compact AI instructions with import rules, chart props, SSR guidance, and pitfalls. |
441
444
  | **`semiotic://examples`** | Copy-paste chart examples by data shape. |
445
+ | **`ui://semiotic/chart-widget.html`** | ChatGPT Apps / MCP Apps widget template used by `renderInteractiveChart`. |
442
446
 
443
447
  ### Prompts
444
448
 
@@ -490,6 +494,25 @@ Args: {
490
494
  → Returns: <svg>...</svg>
491
495
  ```
492
496
 
497
+ ### Example: render a ChatGPT Apps widget
498
+
499
+ ```
500
+ Tool: renderInteractiveChart
501
+ Args: {
502
+ "component": "BarChart",
503
+ "props": {
504
+ "title": "Revenue by Quarter",
505
+ "data": [
506
+ { "quarter": "Q1", "revenue": 120 },
507
+ { "quarter": "Q2", "revenue": 180 }
508
+ ],
509
+ "categoryAccessor": "quarter",
510
+ "valueAccessor": "revenue"
511
+ }
512
+ }
513
+ → Returns: structured chart summary for the model + hidden SVG/widget metadata for ChatGPT.
514
+ ```
515
+
493
516
  ### Example: diagnose a broken config
494
517
 
495
518
  ```
@@ -32433,6 +32433,7 @@ ${errors.join("\n")}`
32433
32433
  }
32434
32434
 
32435
32435
  // ai/mcp-server.ts
32436
+ var import_server2 = require("semiotic/server");
32436
32437
  var import_ai3 = require("semiotic/ai");
32437
32438
  var {
32438
32439
  componentIndexFromSchema,
@@ -32458,6 +32459,8 @@ for (const tool of schema.tools) {
32458
32459
  var allComponentNames = Object.keys(schemaByComponent).sort();
32459
32460
  var componentNames = Object.keys(COMPONENT_REGISTRY).sort();
32460
32461
  var REPO = "nteract/semiotic";
32462
+ var SEMIOTIC_CHART_WIDGET_URI = "ui://semiotic/chart-widget.html";
32463
+ var MCP_APP_MIME_TYPE = "text/html;profile=mcp-app";
32461
32464
  function aiFilePath(fileName) {
32462
32465
  return path.resolve(__dirname, "..", fileName);
32463
32466
  }
@@ -32476,6 +32479,30 @@ function textResource(uri, mimeType, text) {
32476
32479
  }]
32477
32480
  };
32478
32481
  }
32482
+ function appResource(uri, text) {
32483
+ return {
32484
+ contents: [{
32485
+ uri: uri.href,
32486
+ mimeType: MCP_APP_MIME_TYPE,
32487
+ text,
32488
+ _meta: {
32489
+ ui: {
32490
+ prefersBorder: true,
32491
+ csp: {
32492
+ connectDomains: [],
32493
+ resourceDomains: []
32494
+ }
32495
+ },
32496
+ "openai/widgetDescription": "Interactive Semiotic chart preview rendered by the semiotic-mcp server.",
32497
+ "openai/widgetPrefersBorder": true,
32498
+ "openai/widgetCSP": {
32499
+ connect_domains: [],
32500
+ resource_domains: []
32501
+ }
32502
+ }
32503
+ }]
32504
+ };
32505
+ }
32479
32506
  function promptMessage(text) {
32480
32507
  return {
32481
32508
  messages: [{
@@ -32487,6 +32514,279 @@ function promptMessage(text) {
32487
32514
  }]
32488
32515
  };
32489
32516
  }
32517
+ function stripUnsafeSvg(svg) {
32518
+ return svg.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "").replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "").replace(/\s(href|xlink:href)\s*=\s*(["'])\s*javascript:[^"']*\2/gi, "");
32519
+ }
32520
+ function parseRenderEvidence(result) {
32521
+ const evidenceText = result.content.find((block) => block.text.startsWith("Render evidence:\n"))?.text;
32522
+ if (!evidenceText) return null;
32523
+ try {
32524
+ return JSON.parse(evidenceText.replace(/^Render evidence:\n/, ""));
32525
+ } catch {
32526
+ return null;
32527
+ }
32528
+ }
32529
+ function chartTitleFromProps(component, props) {
32530
+ return typeof props.title === "string" && props.title.trim() ? props.title.trim() : component;
32531
+ }
32532
+ function chartDatumCount(props) {
32533
+ if (Array.isArray(props.data)) return props.data.length;
32534
+ if (Array.isArray(props.nodes)) return props.nodes.length;
32535
+ if (Array.isArray(props.edges)) return props.edges.length;
32536
+ if (Array.isArray(props.links)) return props.links.length;
32537
+ return null;
32538
+ }
32539
+ function renderSemioticChartWidgetHTML() {
32540
+ return `
32541
+ <!doctype html>
32542
+ <html lang="en">
32543
+ <head>
32544
+ <meta charset="utf-8" />
32545
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
32546
+ <style>
32547
+ :root {
32548
+ color-scheme: light dark;
32549
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
32550
+ --bg: Canvas;
32551
+ --fg: CanvasText;
32552
+ --muted: color-mix(in srgb, CanvasText 62%, Canvas 38%);
32553
+ --border: color-mix(in srgb, CanvasText 16%, Canvas 84%);
32554
+ --panel: color-mix(in srgb, Canvas 94%, CanvasText 6%);
32555
+ --accent: #2f6fed;
32556
+ }
32557
+ * { box-sizing: border-box; }
32558
+ body { margin: 0; background: var(--bg); color: var(--fg); }
32559
+ main { display: grid; gap: 10px; padding: 12px; min-height: 100vh; }
32560
+ header { display: flex; align-items: start; justify-content: space-between; gap: 10px; }
32561
+ h1 { font-size: 16px; line-height: 1.25; margin: 0; font-weight: 650; }
32562
+ .summary { margin-top: 3px; color: var(--muted); font-size: 12px; line-height: 1.35; }
32563
+ .toolbar { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex-wrap: wrap; }
32564
+ button {
32565
+ border: 1px solid var(--border);
32566
+ background: var(--panel);
32567
+ color: var(--fg);
32568
+ border-radius: 6px;
32569
+ font: inherit;
32570
+ font-size: 12px;
32571
+ padding: 6px 8px;
32572
+ cursor: pointer;
32573
+ }
32574
+ button[aria-pressed="true"] {
32575
+ border-color: var(--accent);
32576
+ color: var(--accent);
32577
+ }
32578
+ label { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 12px; }
32579
+ input[type="range"] { width: 92px; }
32580
+ .chart-shell {
32581
+ overflow: auto;
32582
+ border: 1px solid var(--border);
32583
+ border-radius: 8px;
32584
+ min-height: 260px;
32585
+ background: white;
32586
+ }
32587
+ .chart-shell.fit svg { width: 100%; height: auto; }
32588
+ .chart {
32589
+ min-width: 360px;
32590
+ padding: 10px;
32591
+ transform-origin: top left;
32592
+ }
32593
+ .chart svg { display: block; max-width: none; }
32594
+ .empty {
32595
+ min-height: 240px;
32596
+ display: grid;
32597
+ place-items: center;
32598
+ color: var(--muted);
32599
+ text-align: center;
32600
+ padding: 24px;
32601
+ }
32602
+ .drawer {
32603
+ display: none;
32604
+ border: 1px solid var(--border);
32605
+ border-radius: 8px;
32606
+ overflow: auto;
32607
+ max-height: 220px;
32608
+ }
32609
+ .drawer.open { display: block; }
32610
+ pre {
32611
+ margin: 0;
32612
+ padding: 10px;
32613
+ font-size: 12px;
32614
+ white-space: pre-wrap;
32615
+ overflow-wrap: anywhere;
32616
+ }
32617
+ table { width: 100%; border-collapse: collapse; font-size: 12px; }
32618
+ th, td { border-bottom: 1px solid var(--border); padding: 6px 8px; text-align: left; vertical-align: top; }
32619
+ th { position: sticky; top: 0; background: var(--panel); }
32620
+ .hover {
32621
+ position: fixed;
32622
+ pointer-events: none;
32623
+ z-index: 10;
32624
+ max-width: 280px;
32625
+ padding: 6px 8px;
32626
+ border-radius: 6px;
32627
+ border: 1px solid var(--border);
32628
+ background: var(--bg);
32629
+ color: var(--fg);
32630
+ box-shadow: 0 8px 24px rgb(0 0 0 / 18%);
32631
+ font-size: 12px;
32632
+ display: none;
32633
+ }
32634
+ @media (max-width: 520px) {
32635
+ main { padding: 10px; }
32636
+ header { display: grid; }
32637
+ .toolbar { justify-content: start; }
32638
+ .chart { min-width: 300px; }
32639
+ }
32640
+ </style>
32641
+ </head>
32642
+ <body>
32643
+ <main>
32644
+ <header>
32645
+ <div>
32646
+ <h1 id="title">Semiotic chart</h1>
32647
+ <div class="summary" id="summary">Waiting for a tool result...</div>
32648
+ </div>
32649
+ <div class="toolbar" aria-label="Chart controls">
32650
+ <button id="fit" type="button" aria-pressed="true">Fit</button>
32651
+ <button id="data" type="button" aria-pressed="false">Data</button>
32652
+ <button id="evidence" type="button" aria-pressed="false">Evidence</button>
32653
+ <label>Zoom <input id="zoom" type="range" min="60" max="180" value="100" /></label>
32654
+ </div>
32655
+ </header>
32656
+ <section id="chartShell" class="chart-shell fit" aria-label="Rendered Semiotic chart">
32657
+ <div id="chart" class="chart"><div class="empty">Ask ChatGPT to render a Semiotic chart.</div></div>
32658
+ </section>
32659
+ <section id="dataDrawer" class="drawer" aria-label="Chart data"></section>
32660
+ <section id="evidenceDrawer" class="drawer" aria-label="Render evidence"><pre id="evidenceText">{}</pre></section>
32661
+ </main>
32662
+ <div id="hover" class="hover" role="status" aria-live="polite"></div>
32663
+ <script>
32664
+ const state = { output: null, meta: null };
32665
+ const titleEl = document.getElementById("title");
32666
+ const summaryEl = document.getElementById("summary");
32667
+ const chartEl = document.getElementById("chart");
32668
+ const chartShell = document.getElementById("chartShell");
32669
+ const dataDrawer = document.getElementById("dataDrawer");
32670
+ const evidenceDrawer = document.getElementById("evidenceDrawer");
32671
+ const evidenceText = document.getElementById("evidenceText");
32672
+ const hover = document.getElementById("hover");
32673
+ const fitButton = document.getElementById("fit");
32674
+ const dataButton = document.getElementById("data");
32675
+ const evidenceButton = document.getElementById("evidence");
32676
+ const zoom = document.getElementById("zoom");
32677
+
32678
+ function html(value) {
32679
+ return String(value ?? "").replace(/[&<>"']/g, (char) => ({
32680
+ "&": "&amp;",
32681
+ "<": "&lt;",
32682
+ ">": "&gt;",
32683
+ '"': "&quot;",
32684
+ "'": "&#39;"
32685
+ })[char]);
32686
+ }
32687
+
32688
+ function currentPayload() {
32689
+ const openai = window.openai || {};
32690
+ const output = state.output || openai.toolOutput || null;
32691
+ const meta = state.meta || openai.toolResultMetadata || openai.toolResponseMetadata || openai._meta || null;
32692
+ return { output, meta };
32693
+ }
32694
+
32695
+ function sampleRows(meta) {
32696
+ const props = meta?.props || {};
32697
+ if (Array.isArray(props.data)) return props.data.slice(0, 50);
32698
+ if (Array.isArray(props.nodes)) return props.nodes.slice(0, 50);
32699
+ if (Array.isArray(props.edges)) return props.edges.slice(0, 50);
32700
+ if (Array.isArray(props.links)) return props.links.slice(0, 50);
32701
+ return [];
32702
+ }
32703
+
32704
+ function renderTable(rows) {
32705
+ if (!rows.length) return '<pre>No row data was provided in the widget metadata.</pre>';
32706
+ const columns = Array.from(rows.reduce((set, row) => {
32707
+ Object.keys(row || {}).forEach((key) => set.add(key));
32708
+ return set;
32709
+ }, new Set()));
32710
+ return '<table><thead><tr>' + columns.map((col) => '<th>' + html(col) + '</th>').join('') +
32711
+ '</tr></thead><tbody>' + rows.map((row) => '<tr>' + columns.map((col) => '<td>' + html(row?.[col]) + '</td>').join('') + '</tr>').join('') + '</tbody></table>';
32712
+ }
32713
+
32714
+ function render(output, meta) {
32715
+ const payload = output || {};
32716
+ const hidden = meta || {};
32717
+ titleEl.textContent = payload.title || payload.component || "Semiotic chart";
32718
+ summaryEl.textContent = payload.summary || "Rendered by semiotic-mcp.";
32719
+ const svg = hidden.svg || payload.svg;
32720
+ if (svg) {
32721
+ chartEl.innerHTML = svg;
32722
+ } else {
32723
+ chartEl.innerHTML = '<div class="empty">No SVG payload received. The model-visible chart summary is still available above.</div>';
32724
+ }
32725
+ const rows = sampleRows(hidden);
32726
+ dataDrawer.innerHTML = renderTable(rows);
32727
+ evidenceText.textContent = JSON.stringify(payload.evidence || hidden.evidence || {}, null, 2);
32728
+ }
32729
+
32730
+ function rerenderFromGlobals() {
32731
+ const payload = currentPayload();
32732
+ render(payload.output, payload.meta);
32733
+ }
32734
+
32735
+ fitButton.addEventListener("click", () => {
32736
+ const enabled = !chartShell.classList.contains("fit");
32737
+ chartShell.classList.toggle("fit", enabled);
32738
+ fitButton.setAttribute("aria-pressed", String(enabled));
32739
+ });
32740
+ dataButton.addEventListener("click", () => {
32741
+ const open = !dataDrawer.classList.contains("open");
32742
+ dataDrawer.classList.toggle("open", open);
32743
+ dataButton.setAttribute("aria-pressed", String(open));
32744
+ });
32745
+ evidenceButton.addEventListener("click", () => {
32746
+ const open = !evidenceDrawer.classList.contains("open");
32747
+ evidenceDrawer.classList.toggle("open", open);
32748
+ evidenceButton.setAttribute("aria-pressed", String(open));
32749
+ });
32750
+ zoom.addEventListener("input", () => {
32751
+ chartEl.style.transform = 'scale(' + Number(zoom.value) / 100 + ')';
32752
+ chartEl.style.width = (10000 / Number(zoom.value)) + '%';
32753
+ });
32754
+ chartEl.addEventListener("mousemove", (event) => {
32755
+ const target = event.target;
32756
+ if (!(target instanceof Element) || target === chartEl) {
32757
+ hover.style.display = "none";
32758
+ return;
32759
+ }
32760
+ const label = target.getAttribute("aria-label") || target.textContent?.trim() || target.tagName.toLowerCase();
32761
+ hover.textContent = label.slice(0, 180);
32762
+ hover.style.left = Math.min(event.clientX + 12, window.innerWidth - 300) + "px";
32763
+ hover.style.top = Math.min(event.clientY + 12, window.innerHeight - 70) + "px";
32764
+ hover.style.display = "block";
32765
+ });
32766
+ chartEl.addEventListener("mouseleave", () => {
32767
+ hover.style.display = "none";
32768
+ });
32769
+ window.addEventListener("message", (event) => {
32770
+ if (event.source !== window.parent) return;
32771
+ const message = event.data;
32772
+ if (!message || message.jsonrpc !== "2.0") return;
32773
+ if (message.method === "ui/notifications/tool-result") {
32774
+ state.output = message.params?.structuredContent || null;
32775
+ state.meta = message.params?._meta || null;
32776
+ render(state.output, state.meta);
32777
+ }
32778
+ }, { passive: true });
32779
+ window.addEventListener("openai:set_globals", (event) => {
32780
+ const globals = event.detail?.globals || {};
32781
+ state.output = globals.toolOutput || state.output;
32782
+ state.meta = globals.toolResultMetadata || globals.toolResponseMetadata || globals._meta || state.meta;
32783
+ rerenderFromGlobals();
32784
+ }, { passive: true });
32785
+ rerenderFromGlobals();
32786
+ </script>
32787
+ </body>
32788
+ </html>`.trim();
32789
+ }
32490
32790
  async function getSchemaHandler(args) {
32491
32791
  const component = args.component;
32492
32792
  if (!component) {
@@ -32563,6 +32863,17 @@ async function renderChartHandler(args) {
32563
32863
  };
32564
32864
  }
32565
32865
  let svg = result.svg;
32866
+ let evidenceBlock = null;
32867
+ try {
32868
+ const { svg: evidenceSvg, evidence } = (0, import_server2.renderChartWithEvidence)(component, props);
32869
+ svg = evidenceSvg;
32870
+ evidenceBlock = {
32871
+ type: "text",
32872
+ text: `Render evidence:
32873
+ ${JSON.stringify(evidence, null, 2)}`
32874
+ };
32875
+ } catch {
32876
+ }
32566
32877
  if (theme && Object.keys(theme).length > 0) {
32567
32878
  const validVars = Object.entries(theme).filter(([k]) => k.startsWith("--semiotic-")).map(([k, v]) => `${k}: ${v}`).join("; ");
32568
32879
  if (validVars) {
@@ -32576,7 +32887,10 @@ async function renderChartHandler(args) {
32576
32887
  const pngBuffer = await sharpFn(Buffer.from(svg)).png().toBuffer();
32577
32888
  const base643 = pngBuffer.toString("base64");
32578
32889
  return {
32579
- content: [{ type: "text", text: `data:image/png;base64,${base643}` }]
32890
+ content: [
32891
+ { type: "text", text: `data:image/png;base64,${base643}` },
32892
+ ...evidenceBlock ? [evidenceBlock] : []
32893
+ ]
32580
32894
  };
32581
32895
  } catch (err) {
32582
32896
  if (err.code === "MODULE_NOT_FOUND" || err.code === "ERR_MODULE_NOT_FOUND") {
@@ -32599,7 +32913,52 @@ ${svg}` }],
32599
32913
  }
32600
32914
  }
32601
32915
  return {
32602
- content: [{ type: "text", text: svg }]
32916
+ content: [
32917
+ { type: "text", text: svg },
32918
+ ...evidenceBlock ? [evidenceBlock] : []
32919
+ ]
32920
+ };
32921
+ }
32922
+ async function renderInteractiveChartHandler(args) {
32923
+ const component = args.component;
32924
+ const props = args.props ?? {};
32925
+ const rendered = await renderChartHandler({
32926
+ component,
32927
+ props,
32928
+ theme: args.theme,
32929
+ format: "svg"
32930
+ });
32931
+ if (rendered.isError) return rendered;
32932
+ const svg = stripUnsafeSvg(rendered.content[0]?.text ?? "");
32933
+ const evidence = parseRenderEvidence(rendered);
32934
+ const title = chartTitleFromProps(component || "Semiotic chart", props);
32935
+ const datumCount = chartDatumCount(props);
32936
+ const summary = [
32937
+ `Rendered ${title} with ${component}.`,
32938
+ datumCount == null ? "No row count was inferred from props." : `${datumCount} input row${datumCount === 1 ? "" : "s"} available in the widget data drawer.`,
32939
+ "Use the widget controls to zoom, fit width, inspect data, and inspect render evidence."
32940
+ ].join(" ");
32941
+ return {
32942
+ content: [{
32943
+ type: "text",
32944
+ text: `Rendered ${title} (${component}) as an interactive ChatGPT Apps widget.`
32945
+ }],
32946
+ structuredContent: {
32947
+ component: component ?? "SemioticChart",
32948
+ title,
32949
+ summary,
32950
+ datumCount,
32951
+ evidence
32952
+ },
32953
+ _meta: {
32954
+ component,
32955
+ title,
32956
+ props,
32957
+ theme: args.theme ?? null,
32958
+ svg,
32959
+ evidence,
32960
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
32961
+ }
32603
32962
  };
32604
32963
  }
32605
32964
  function filterUsageModeDiagnoses(component, usageMode, diagnoses) {
@@ -33102,6 +33461,27 @@ function createServer2() {
33102
33461
  },
33103
33462
  (uri) => textResource(uri, "text/markdown", readAIFile("examples.md"))
33104
33463
  );
33464
+ srv.registerResource(
33465
+ "semiotic-chatgpt-chart-widget",
33466
+ SEMIOTIC_CHART_WIDGET_URI,
33467
+ {
33468
+ title: "Semiotic ChatGPT Chart Widget",
33469
+ description: "MCP Apps widget template for interactive Semiotic chart previews inside ChatGPT.",
33470
+ mimeType: MCP_APP_MIME_TYPE,
33471
+ _meta: {
33472
+ ui: {
33473
+ prefersBorder: true,
33474
+ csp: {
33475
+ connectDomains: [],
33476
+ resourceDomains: []
33477
+ }
33478
+ },
33479
+ "openai/widgetDescription": "Interactive Semiotic chart preview rendered by the semiotic-mcp server.",
33480
+ "openai/widgetPrefersBorder": true
33481
+ }
33482
+ },
33483
+ (uri) => appResource(uri, renderSemioticChartWidgetHTML())
33484
+ );
33105
33485
  srv.registerPrompt(
33106
33486
  "build-semiotic-chart",
33107
33487
  {
@@ -33191,7 +33571,7 @@ function createServer2() {
33191
33571
  );
33192
33572
  srv.tool(
33193
33573
  "renderChart",
33194
- `Render a Semiotic chart to static SVG or PNG. This is a static snapshot path: props must include data immediately, and ref/push-mode charts cannot be rendered through this tool. Returns SVG string (default) or Base64-encoded PNG image. Optionally pass theme CSS custom properties (--semiotic-bg, --semiotic-text, etc.) to style the output. PNG requires the 'sharp' package to be installed. Available components: ${componentNames.join(", ")}.`,
33574
+ `Render a Semiotic chart to static SVG or PNG. This is a static snapshot path: props must include data immediately, and ref/push-mode charts cannot be rendered through this tool. Returns SVG string (default) or Base64-encoded PNG image, plus a "Render evidence" JSON block (mark counts by type, resolved axis domains, empty flag, annotation count, accessible name) \u2014 read the evidence instead of parsing the SVG to verify the chart actually rendered data marks. Optionally pass theme CSS custom properties (--semiotic-bg, --semiotic-text, etc.) to style the output. PNG requires the 'sharp' package to be installed. Available components: ${componentNames.join(", ")}.`,
33195
33575
  {
33196
33576
  component: external_exports3.string().describe("Chart component name, e.g. 'LineChart', 'BarChart'"),
33197
33577
  props: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Chart props object, e.g. { data: [...], xAccessor: 'x' }."),
@@ -33200,6 +33580,38 @@ function createServer2() {
33200
33580
  },
33201
33581
  renderChartHandler
33202
33582
  );
33583
+ srv.registerTool(
33584
+ "renderInteractiveChart",
33585
+ {
33586
+ title: "Render interactive Semiotic chart",
33587
+ description: `Render a static-data Semiotic chart as a ChatGPT Apps widget. Use this after suggestCharts/getSchema/diagnoseConfig when the user wants to see an interactive chart inside ChatGPT. The server renders Semiotic to SVG and the widget adds fit, zoom, data, hover, and render-evidence controls. Available components: ${componentNames.join(", ")}.`,
33588
+ inputSchema: {
33589
+ component: external_exports3.string().describe("Renderable chart component name, e.g. 'LineChart', 'BarChart', 'GaugeChart'."),
33590
+ props: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Static Semiotic chart props, including data/accessors where required."),
33591
+ theme: external_exports3.record(external_exports3.string(), external_exports3.string()).optional().describe("CSS custom properties such as { '--semiotic-bg': '#fff', '--semiotic-text': '#111' }. Only --semiotic-* variables are applied.")
33592
+ },
33593
+ outputSchema: {
33594
+ component: external_exports3.string(),
33595
+ title: external_exports3.string(),
33596
+ summary: external_exports3.string(),
33597
+ datumCount: external_exports3.number().nullable(),
33598
+ evidence: external_exports3.record(external_exports3.string(), external_exports3.unknown()).nullable()
33599
+ },
33600
+ annotations: {
33601
+ readOnlyHint: true,
33602
+ destructiveHint: false,
33603
+ idempotentHint: true,
33604
+ openWorldHint: false
33605
+ },
33606
+ _meta: {
33607
+ ui: { resourceUri: SEMIOTIC_CHART_WIDGET_URI },
33608
+ "openai/outputTemplate": SEMIOTIC_CHART_WIDGET_URI,
33609
+ "openai/toolInvocation/invoking": "Rendering Semiotic chart...",
33610
+ "openai/toolInvocation/invoked": "Rendered Semiotic chart."
33611
+ }
33612
+ },
33613
+ renderInteractiveChartHandler
33614
+ );
33203
33615
  srv.tool(
33204
33616
  "diagnoseConfig",
33205
33617
  "Diagnose a Semiotic chart configuration for common problems (empty data, bad dimensions, missing accessors, wrong data shape, color contrast issues, etc). Pass usageMode='push' for ref-based React HOCs that intentionally omit data; omit usageMode or pass 'static' for renderChart/MCP/server configs where data is required. Checks WCAG color contrast ratios and suggests COLOR_BLIND_SAFE_CATEGORICAL for accessibility. Returns a human-readable diagnostic report with actionable fixes.",
@@ -33423,8 +33835,8 @@ async function main() {
33423
33835
  });
33424
33836
  httpServer.listen(port, () => {
33425
33837
  console.error(`Semiotic MCP server (HTTP) listening on http://localhost:${port}`);
33426
- console.error("Tools: getSchema, suggestChart, suggestCharts, proposeChartVariants, suggestStreamCharts, suggestDashboard, suggestStretchCharts, repairChartConfig, renderChart, interrogateChart, groundChart, diagnoseConfig, auditAccessibility, reportIssue, applyTheme");
33427
- console.error("Resources: semiotic://schema, semiotic://components, semiotic://behavior-contracts, semiotic://system-prompt, semiotic://examples");
33838
+ console.error("Tools: getSchema, suggestChart, suggestCharts, proposeChartVariants, suggestStreamCharts, suggestDashboard, suggestStretchCharts, repairChartConfig, renderChart, renderInteractiveChart, interrogateChart, groundChart, diagnoseConfig, auditAccessibility, reportIssue, applyTheme");
33839
+ console.error("Resources: semiotic://schema, semiotic://components, semiotic://behavior-contracts, semiotic://system-prompt, semiotic://examples, ui://semiotic/chart-widget.html");
33428
33840
  });
33429
33841
  } else {
33430
33842
  const srv = createServer2();
package/ai/schema.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "name": "semiotic",
4
- "version": "3.7.0",
4
+ "version": "3.7.1",
5
5
  "description": "React data visualization library for charts, networks, and beyond",
6
6
  "tools": [
7
7
  {
@@ -2,7 +2,7 @@
2
2
 
3
3
  <!-- semiotic-bundle-sizes:start -->
4
4
  <!-- Auto-generated by scripts/sync-bundle-sizes.mjs — do not edit by hand. -->
5
- **Use sub-path imports** — `semiotic/xy` (90KB gz), `semiotic/ordinal` (74KB gz), `semiotic/network` (68KB gz), `semiotic/geo` (55KB gz), `semiotic/realtime` (95KB gz), `semiotic/server` (127KB gz), `semiotic/utils` (37KB gz), `semiotic/recipes` (9KB gz), `semiotic/themes` (4KB gz), `semiotic/data` (3KB gz), `semiotic/value` (6KB gz), `semiotic/ai` (246KB gz). Full `semiotic` is 203KB gz.
5
+ **Use sub-path imports** — `semiotic/xy` (90KB gz), `semiotic/ordinal` (74KB gz), `semiotic/network` (68KB gz), `semiotic/geo` (55KB gz), `semiotic/realtime` (95KB gz), `semiotic/server` (128KB gz), `semiotic/utils` (38KB gz), `semiotic/recipes` (9KB gz), `semiotic/themes` (4KB gz), `semiotic/data` (3KB gz), `semiotic/value` (6KB gz), `semiotic/ai` (250KB gz). Full `semiotic` is 203KB gz.
6
6
  <!-- semiotic-bundle-sizes:end -->
7
7
 
8
8
  ## Flat Array Data (`data: object[]`)
@@ -140,7 +140,7 @@ These rules are generated from `ai/behaviorContracts.cjs` and are consumed by `s
140
140
 
141
141
  ## Key Patterns
142
142
  - **Percentile band + main line**: Layer `<AreaChart yAccessor="p95" y0Accessor="p5" showLine={false} />` + `<LineChart yAccessor="p50" />`. AreaChart's `showLine` only draws the top edge, NOT a separate main line.
143
- - **SSR**: `renderChart("BarChart", props)` from `semiotic/server` — uses HOC names. Also `"Sparkline"` (no axes, 2px margins). `renderToImage()` (PNG), `renderToAnimatedGif()` (GIF), `renderDashboard()` (multi-chart). All accept `theme`. Required props: StackedBarChart needs `stackBy`, GroupedBarChart needs `groupBy`, StackedAreaChart needs `areaBy`, BubbleChart needs `sizeBy`, FunnelChart uses `stepAccessor`, GaugeChart needs `value` (`thresholds` optional).
143
+ - **SSR**: `renderChart("BarChart", props)` from `semiotic/server` — uses HOC names. Also `"Sparkline"` (no axes, 2px margins). `renderChartWithEvidence()` returns `{ svg, evidence }` (mark counts by scene type, axis domains, empty flag, annotation count, accessible name) so agents can verify the render drew data marks. `renderToImage()` (PNG), `renderToAnimatedGif()` (GIF), `renderDashboard()` (multi-chart). All accept `theme`. Required props: StackedBarChart needs `stackBy`, GroupedBarChart needs `groupBy`, StackedAreaChart needs `areaBy`, BubbleChart needs `sizeBy`, FunnelChart uses `stepAccessor`, GaugeChart needs `value` (`thresholds` optional).
144
144
  - **CLI**: `npx semiotic-ai --list` shows components/import paths/renderability; `npx semiotic-ai --schema GaugeChart` prints one component schema with metadata; `--doctor` validates props JSON and behavior contracts.
145
- - **MCP**: `npx semiotic-mcp` exposes tools (`getSchema`, `suggestChart`, `diagnoseConfig`, `renderChart`, `applyTheme`, `reportIssue`), resources (`semiotic://schema`, `semiotic://components`, `semiotic://behavior-contracts`, `semiotic://system-prompt`, `semiotic://examples`), and prompts (`build-semiotic-chart`, `debug-semiotic-chart`).
145
+ - **MCP**: `npx semiotic-mcp` exposes schema, suggestion, diagnosis, accessibility, grounding, issue, theme, static render (`renderChart`), and ChatGPT Apps render (`renderInteractiveChart`) tools. Resources include `semiotic://schema`, `semiotic://components`, `semiotic://behavior-contracts`, `semiotic://system-prompt`, `semiotic://examples`, and the widget template `ui://semiotic/chart-widget.html`. Prompts: `build-semiotic-chart`, `debug-semiotic-chart`.
146
146
  - **exportChart**: Pass the wrapper div, not the SVG element: `exportChart(wrapperDiv, { format: "png" })`. It finds canvas+SVG internally.
@@ -1,3 +1,4 @@
1
+ import * as React from "react";
1
2
  import { type NavTreeNode } from "./ai/navigationTree";
2
3
  export interface AccessibleNavTreeProps {
3
4
  tree: NavTreeNode;
@@ -22,4 +23,4 @@ export interface AccessibleNavTreeProps {
22
23
  */
23
24
  chartId?: string;
24
25
  }
25
- export declare function AccessibleNavTree({ tree, label, visible, className, onActiveChange, activeId: controlledActiveId, chartId }: AccessibleNavTreeProps): import("react/jsx-runtime").JSX.Element;
26
+ export declare function AccessibleNavTree({ tree, label, visible, className, onActiveChange, activeId: controlledActiveId, chartId }: AccessibleNavTreeProps): React.JSX.Element;
@@ -65,5 +65,5 @@ export interface AnnotationProps {
65
65
  [key: string]: unknown;
66
66
  };
67
67
  }
68
- export default function SemioticAnnotation(props: AnnotationProps): import("react/jsx-runtime").JSX.Element;
68
+ export default function SemioticAnnotation(props: AnnotationProps): React.JSX.Element;
69
69
  export {};