semiotic 3.1.1 → 3.2.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/CLAUDE.md +134 -216
- package/LICENSE +197 -10
- package/README.md +1 -0
- package/ai/dist/componentRegistry.js +6 -0
- package/ai/dist/mcp-server.js +232 -65
- package/ai/dist/renderHOCToSVG.js +5 -3
- package/ai/examples.md +93 -0
- package/ai/schema.json +3916 -878
- package/ai/system-prompt.md +27 -0
- package/dist/components/ThemeProvider.d.ts +5 -3
- package/dist/components/charts/geo/ChoroplethMap.d.ts +1 -1
- package/dist/components/charts/index.d.ts +8 -1
- package/dist/components/charts/ordinal/BarChart.d.ts +1 -0
- package/dist/components/charts/ordinal/BoxPlot.d.ts +1 -0
- package/dist/components/charts/ordinal/DonutChart.d.ts +1 -0
- package/dist/components/charts/ordinal/DotPlot.d.ts +1 -0
- package/dist/components/charts/ordinal/FunnelChart.d.ts +55 -0
- package/dist/components/charts/ordinal/GroupedBarChart.d.ts +1 -0
- package/dist/components/charts/ordinal/Histogram.d.ts +1 -0
- package/dist/components/charts/ordinal/PieChart.d.ts +1 -0
- package/dist/components/charts/ordinal/RidgelinePlot.d.ts +1 -0
- package/dist/components/charts/ordinal/StackedBarChart.d.ts +1 -0
- package/dist/components/charts/ordinal/SwarmPlot.d.ts +1 -0
- package/dist/components/charts/ordinal/ViolinPlot.d.ts +1 -0
- package/dist/components/charts/shared/colorManipulation.d.ts +15 -0
- package/dist/components/charts/shared/formatUtils.d.ts +28 -0
- package/dist/components/charts/shared/hatchPattern.d.ts +35 -0
- package/dist/components/charts/shared/hooks.d.ts +16 -1
- package/dist/components/charts/shared/legendUtils.d.ts +2 -1
- package/dist/components/charts/shared/selectionUtils.d.ts +11 -0
- package/dist/components/charts/shared/statisticalOverlays.d.ts +49 -5
- package/dist/components/charts/shared/types.d.ts +4 -1
- package/dist/components/charts/xy/Heatmap.d.ts +1 -1
- package/dist/components/charts/xy/MultiAxisLineChart.d.ts +71 -0
- package/dist/components/realtime/types.d.ts +2 -0
- package/dist/components/semiotic-ai.d.ts +3 -0
- package/dist/components/semiotic-ordinal.d.ts +3 -0
- package/dist/components/semiotic-themes.d.ts +64 -0
- package/dist/components/semiotic-xy.d.ts +1 -0
- package/dist/components/semiotic.d.ts +11 -5
- package/dist/components/store/ThemeStore.d.ts +22 -2
- package/dist/components/stream/OrdinalSVGOverlay.d.ts +1 -0
- package/dist/components/stream/PipelineStore.d.ts +2 -0
- package/dist/components/stream/SVGOverlay.d.ts +5 -3
- package/dist/components/stream/accessorUtils.d.ts +14 -0
- package/dist/components/stream/networkTypes.d.ts +2 -0
- package/dist/components/stream/ordinalSceneBuilders/barFunnelScene.d.ts +27 -0
- package/dist/components/stream/ordinalSceneBuilders/funnelScene.d.ts +26 -0
- package/dist/components/stream/ordinalTypes.d.ts +16 -2
- package/dist/components/stream/renderers/barFunnelCanvasRenderer.d.ts +12 -0
- package/dist/components/stream/renderers/trapezoidCanvasRenderer.d.ts +15 -0
- package/dist/components/stream/sceneUtils.d.ts +10 -0
- package/dist/components/stream/types.d.ts +10 -3
- package/dist/geo.min.js +1 -1
- package/dist/geo.module.min.js +1 -1
- package/dist/network.min.js +1 -1
- package/dist/network.module.min.js +1 -1
- package/dist/ordinal.min.js +1 -1
- package/dist/ordinal.module.min.js +1 -1
- package/dist/realtime.min.js +1 -1
- package/dist/realtime.module.min.js +1 -1
- package/dist/semiotic-ai-statisticalOverlays-C2PPlmXv.js +1 -0
- package/dist/semiotic-ai.d.ts +3 -0
- package/dist/semiotic-ai.min.js +1 -1
- package/dist/semiotic-ai.module.min.js +1 -1
- package/dist/semiotic-ordinal.d.ts +3 -0
- package/dist/semiotic-statisticalOverlays-D8LhSbQt.js +1 -0
- package/dist/semiotic-themes.d.ts +64 -0
- package/dist/semiotic-themes.min.js +1 -0
- package/dist/semiotic-themes.module.min.js +1 -0
- package/dist/semiotic-xy.d.ts +1 -0
- package/dist/semiotic.d.ts +11 -5
- package/dist/semiotic.min.js +1 -1
- package/dist/semiotic.module.min.js +1 -1
- package/dist/server.min.js +1 -1
- package/dist/server.module.min.js +1 -1
- package/dist/xy-statisticalOverlays-C2PPlmXv.js +1 -0
- package/dist/xy.min.js +1 -1
- package/dist/xy.module.min.js +1 -1
- package/package.json +24 -5
- package/dist/semiotic-ai-statisticalOverlays-C1f7TYyD.js +0 -1
- package/dist/semiotic-statisticalOverlays-C1f7TYyD.js +0 -1
- package/dist/xy-statisticalOverlays-C1f7TYyD.js +0 -1
package/ai/dist/mcp-server.js
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* }
|
|
20
20
|
* }
|
|
21
21
|
* }
|
|
22
|
+
*
|
|
23
|
+
* HTTP mode (for remote inspectors / web clients):
|
|
24
|
+
* npx semiotic-mcp --http --port 3001
|
|
22
25
|
*/
|
|
23
26
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
24
27
|
if (k2 === undefined) k2 = k;
|
|
@@ -56,34 +59,32 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
56
59
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
60
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
58
61
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
62
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
63
|
+
const zod_1 = require("zod");
|
|
59
64
|
const fs = __importStar(require("fs"));
|
|
60
65
|
const path = __importStar(require("path"));
|
|
66
|
+
const http = __importStar(require("http"));
|
|
61
67
|
const renderHOCToSVG_1 = require("./renderHOCToSVG");
|
|
62
68
|
const componentRegistry_1 = require("./componentRegistry");
|
|
63
69
|
const ai_1 = require("semiotic/ai");
|
|
64
70
|
// Load schema.json for version info
|
|
65
71
|
const schemaPath = path.resolve(__dirname, "../schema.json");
|
|
66
72
|
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
|
|
67
|
-
// Build MCP server
|
|
68
|
-
const server = new mcp_js_1.McpServer({
|
|
69
|
-
name: "semiotic",
|
|
70
|
-
version: schema.version || "3.0.0",
|
|
71
|
-
});
|
|
72
73
|
// Build component name → schema lookup from schema.json
|
|
73
74
|
const schemaByComponent = {};
|
|
74
75
|
for (const tool of schema.tools) {
|
|
75
76
|
schemaByComponent[tool.function.name] = tool.function;
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const componentNames = Object.keys(componentRegistry_1.COMPONENT_REGISTRY).sort();
|
|
79
|
+
const REPO = "nteract/semiotic";
|
|
80
|
+
async function getSchemaHandler(args) {
|
|
80
81
|
const component = args.component;
|
|
81
82
|
if (!component) {
|
|
82
83
|
const all = Object.keys(schemaByComponent).sort();
|
|
83
84
|
const renderable = new Set(Object.keys(componentRegistry_1.COMPONENT_REGISTRY));
|
|
84
85
|
const list = all.map(name => renderable.has(name) ? `${name} [renderable]` : name);
|
|
85
86
|
return {
|
|
86
|
-
content: [{ type: "text", text: `Available components (${all.length}):\n${list.join(", ")}\n\nComponents marked [renderable] can be rendered to SVG via renderChart. Others (Realtime*) require a browser environment.\n\nPass { component: '<name>' } to get the prop schema for a specific component.` }],
|
|
87
|
+
content: [{ type: "text", text: `Available components (${all.length}):\n${list.join(", ")}\n\nComponents marked [renderable] can be rendered to SVG via renderChart (pass theme parameter for styled output). Others (Realtime*) require a browser environment.\n\nAll charts support CSS custom properties for theming (--semiotic-bg, --semiotic-text, --semiotic-grid, etc.) and <ThemeProvider>. Use COLOR_BLIND_SAFE_CATEGORICAL (import from semiotic) for accessible color palettes.\n\nPass { component: '<name>' } to get the prop schema for a specific component.` }],
|
|
87
88
|
};
|
|
88
89
|
}
|
|
89
90
|
const entry = schemaByComponent[component];
|
|
@@ -94,14 +95,12 @@ server.tool("getSchema", `Return the prop schema for a Semiotic chart component.
|
|
|
94
95
|
isError: true,
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
|
-
const
|
|
98
|
+
const renderableNote = componentRegistry_1.COMPONENT_REGISTRY[component] ? "This component can be rendered to SVG via renderChart." : "This component requires a browser environment and cannot be rendered via renderChart.";
|
|
98
99
|
return {
|
|
99
|
-
content: [{ type: "text", text: `${
|
|
100
|
+
content: [{ type: "text", text: `${renderableNote}\n\n${JSON.stringify(entry, null, 2)}` }],
|
|
100
101
|
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Analyzes a data sample and recommends appropriate chart types.
|
|
104
|
-
server.tool("suggestChart", "Recommend Semiotic chart types for a given data sample. Pass { data: [...] } with 1-5 sample objects. Optionally pass { intent: 'comparison' | 'trend' | 'distribution' | 'relationship' | 'composition' | 'geographic' | 'network' | 'hierarchy' } to narrow suggestions. Returns ranked recommendations with example props.", {}, async (args) => {
|
|
102
|
+
}
|
|
103
|
+
async function suggestChartHandler(args) {
|
|
105
104
|
const data = args.data;
|
|
106
105
|
const intent = args.intent;
|
|
107
106
|
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
@@ -135,8 +134,6 @@ server.tool("suggestChart", "Recommend Semiotic chart types for a given data sam
|
|
|
135
134
|
numericFields.push(key);
|
|
136
135
|
}
|
|
137
136
|
else if (typeof first === "string") {
|
|
138
|
-
// Check for dates — require ISO-like pattern (YYYY-MM or YYYY/MM or YYYY-MM-DD, etc.)
|
|
139
|
-
// to avoid false positives on 4-digit IDs like "1234"
|
|
140
137
|
if (/^\d{4}[-/]\d{2}/.test(first) && !isNaN(Date.parse(first))) {
|
|
141
138
|
dateFields.push(key);
|
|
142
139
|
}
|
|
@@ -144,20 +141,17 @@ server.tool("suggestChart", "Recommend Semiotic chart types for a given data sam
|
|
|
144
141
|
stringFields.push(key);
|
|
145
142
|
}
|
|
146
143
|
}
|
|
147
|
-
// Detect geo fields
|
|
148
144
|
const kl = key.toLowerCase();
|
|
149
145
|
if (kl === "lat" || kl === "latitude")
|
|
150
146
|
geoFields.lat = key;
|
|
151
147
|
if (kl === "lon" || kl === "lng" || kl === "longitude")
|
|
152
148
|
geoFields.lon = key;
|
|
153
|
-
// Detect network fields
|
|
154
149
|
if (kl === "source" || kl === "from")
|
|
155
150
|
networkFields.source = key;
|
|
156
151
|
if (kl === "target" || kl === "to")
|
|
157
152
|
networkFields.target = key;
|
|
158
153
|
if (kl === "value" || kl === "weight" || kl === "amount")
|
|
159
154
|
networkFields.value = key;
|
|
160
|
-
// Detect hierarchy fields
|
|
161
155
|
if (kl === "children" || kl === "values")
|
|
162
156
|
hierarchyFields.children = key;
|
|
163
157
|
if (kl === "parent")
|
|
@@ -252,11 +246,11 @@ server.tool("suggestChart", "Recommend Semiotic chart types for a given data sam
|
|
|
252
246
|
props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"`, stackBy: `"${stringFields[1]}"` },
|
|
253
247
|
});
|
|
254
248
|
}
|
|
255
|
-
if (
|
|
249
|
+
if (!intent || intent === "distribution") {
|
|
256
250
|
suggestions.push({
|
|
257
251
|
component: "Histogram",
|
|
258
252
|
confidence: "medium",
|
|
259
|
-
reason:
|
|
253
|
+
reason: `Numeric distribution of ${valField} — histogram shows value spread`,
|
|
260
254
|
props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"` },
|
|
261
255
|
});
|
|
262
256
|
}
|
|
@@ -311,25 +305,17 @@ server.tool("suggestChart", "Recommend Semiotic chart types for a given data sam
|
|
|
311
305
|
const propsStr = Object.entries(s.props).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
312
306
|
return `${i + 1}. **${s.component}** (${s.confidence} confidence)\n ${s.reason}\n \`<${s.component} ${propsStr} />\``;
|
|
313
307
|
});
|
|
308
|
+
// Theming guidance appended to every recommendation
|
|
309
|
+
const themingTip = `\n---\n**Styling**: All charts respond to CSS custom properties on any ancestor element:\n\`\`\`css\n.my-theme {\n --semiotic-bg: #fff; /* chart background */\n --semiotic-text: #333; /* primary text */\n --semiotic-text-secondary: #666; /* tick labels */\n --semiotic-grid: #e0e0e0; /* grid lines */\n --semiotic-border: #e0e0e0; /* axis lines, borders */\n --semiotic-font-family: sans-serif;\n --semiotic-tooltip-bg: rgba(0,0,0,0.85);\n --semiotic-tooltip-text: white;\n --semiotic-tooltip-radius: 6px;\n}\n\`\`\`\nOr use \`<ThemeProvider theme="dark">\` / \`<ThemeProvider theme={{ colors: {...}, typography: {...} }}>\`.\nFor accessibility, use \`colorScheme={COLOR_BLIND_SAFE_CATEGORICAL}\` (import from \`semiotic\`) — 8-color palette safe for all forms of color blindness.`;
|
|
314
310
|
return {
|
|
315
|
-
content: [{ type: "text", text: lines.join("\n\n") }],
|
|
311
|
+
content: [{ type: "text", text: lines.join("\n\n") + themingTip }],
|
|
316
312
|
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Generic tool that renders any Semiotic HOC chart to static SVG.
|
|
320
|
-
// Accepts { component, props } — the single entry point for all chart rendering.
|
|
321
|
-
const componentNames = Object.keys(componentRegistry_1.COMPONENT_REGISTRY).sort();
|
|
322
|
-
server.tool("renderChart", `Render any Semiotic chart to static SVG. Pass { component: '<name>', props: { ... } }. Returns SVG string or validation errors. Available components: ${componentNames.join(", ")}.`, {}, async (args) => {
|
|
313
|
+
}
|
|
314
|
+
async function renderChartHandler(args) {
|
|
323
315
|
const component = args.component;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
// Flatten shape: { component, data, ... } — strip component before forwarding
|
|
330
|
-
const { component: _, ...rest } = args;
|
|
331
|
-
props = rest;
|
|
332
|
-
}
|
|
316
|
+
const props = args.props ?? {};
|
|
317
|
+
const theme = args.theme;
|
|
318
|
+
const format = args.format || "svg";
|
|
333
319
|
if (!component) {
|
|
334
320
|
return {
|
|
335
321
|
content: [{ type: "text", text: `Missing 'component' field. Provide { component: '<name>', props: { ... } }. Available: ${componentNames.join(", ")}` }],
|
|
@@ -349,23 +335,51 @@ server.tool("renderChart", `Render any Semiotic chart to static SVG. Pass { comp
|
|
|
349
335
|
isError: true,
|
|
350
336
|
};
|
|
351
337
|
}
|
|
338
|
+
let svg = result.svg;
|
|
339
|
+
// Inject theme CSS custom properties into the SVG root element.
|
|
340
|
+
// We add a <style> block inside the SVG rather than wrapping in a <div>,
|
|
341
|
+
// because sharp requires pure SVG input for PNG rasterization.
|
|
342
|
+
if (theme && Object.keys(theme).length > 0) {
|
|
343
|
+
const validVars = Object.entries(theme)
|
|
344
|
+
.filter(([k]) => k.startsWith("--semiotic-"))
|
|
345
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
346
|
+
.join("; ");
|
|
347
|
+
if (validVars) {
|
|
348
|
+
svg = svg.replace(/<svg([^>]*)>/, `<svg$1><style>:root { ${validVars} }</style>`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// PNG rasterization via sharp (optional dependency)
|
|
352
|
+
if (format === "png") {
|
|
353
|
+
try {
|
|
354
|
+
// Dynamic import — sharp is an optional dependency
|
|
355
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
356
|
+
const sharpMod = await Function('return import("sharp")')();
|
|
357
|
+
const sharpFn = sharpMod.default || sharpMod;
|
|
358
|
+
const pngBuffer = await sharpFn(Buffer.from(svg)).png().toBuffer();
|
|
359
|
+
const base64 = pngBuffer.toString("base64");
|
|
360
|
+
return {
|
|
361
|
+
content: [{ type: "text", text: `data:image/png;base64,${base64}` }],
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
if (err.code === "MODULE_NOT_FOUND" || err.code === "ERR_MODULE_NOT_FOUND") {
|
|
366
|
+
return {
|
|
367
|
+
content: [{ type: "text", text: `PNG output requires the 'sharp' package. Install it with: npm install sharp\n\nFalling back to SVG output:\n\n${svg}` }],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
content: [{ type: "text", text: `PNG conversion failed: ${err.message}\n\nSVG output:\n\n${svg}` }],
|
|
372
|
+
isError: true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
352
376
|
return {
|
|
353
|
-
content: [{ type: "text", text:
|
|
377
|
+
content: [{ type: "text", text: svg }],
|
|
354
378
|
};
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Anti-pattern detector: checks for common failure modes and returns
|
|
358
|
-
// actionable fix instructions.
|
|
359
|
-
server.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for common problems (empty data, bad dimensions, missing accessors, wrong data shape, etc). Pass { component: 'LineChart', props: { ... } }. Returns structured diagnoses with fix instructions.", {}, async (args) => {
|
|
379
|
+
}
|
|
380
|
+
async function diagnoseConfigHandler(args) {
|
|
360
381
|
const component = args.component;
|
|
361
|
-
|
|
362
|
-
if (args.props) {
|
|
363
|
-
props = args.props;
|
|
364
|
-
}
|
|
365
|
-
else {
|
|
366
|
-
const { component: _, ...rest } = args;
|
|
367
|
-
props = rest;
|
|
368
|
-
}
|
|
382
|
+
const props = args.props ?? {};
|
|
369
383
|
if (!component) {
|
|
370
384
|
return {
|
|
371
385
|
content: [{ type: "text", text: "Missing 'component' field. Provide { component: 'LineChart', props: { ... } }." }],
|
|
@@ -374,13 +388,13 @@ server.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for commo
|
|
|
374
388
|
}
|
|
375
389
|
const result = (0, ai_1.diagnoseConfig)(component, props);
|
|
376
390
|
if (result.ok) {
|
|
377
|
-
const warnings = result.diagnoses.filter(d => d.severity === "warning");
|
|
391
|
+
const warnings = result.diagnoses.filter((d) => d.severity === "warning");
|
|
378
392
|
const msg = warnings.length > 0
|
|
379
|
-
? `Configuration looks good with ${warnings.length} warning(s):\n${warnings.map(w => `⚠ [${w.code}] ${w.message}\n Fix: ${w.fix}`).join("\n")}`
|
|
393
|
+
? `Configuration looks good with ${warnings.length} warning(s):\n${warnings.map((w) => `⚠ [${w.code}] ${w.message}\n Fix: ${w.fix}`).join("\n")}`
|
|
380
394
|
: `✓ Configuration looks good — no issues detected.`;
|
|
381
395
|
return { content: [{ type: "text", text: msg }] };
|
|
382
396
|
}
|
|
383
|
-
const lines = result.diagnoses.map(d => {
|
|
397
|
+
const lines = result.diagnoses.map((d) => {
|
|
384
398
|
const icon = d.severity === "error" ? "✗" : "⚠";
|
|
385
399
|
const fixLine = d.fix ? `\n Fix: ${d.fix}` : "";
|
|
386
400
|
return `${icon} [${d.code}] ${d.message}${fixLine}`;
|
|
@@ -389,12 +403,8 @@ server.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for commo
|
|
|
389
403
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
390
404
|
isError: true,
|
|
391
405
|
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Generates a pre-filled GitHub issue URL for bug reports or feature requests.
|
|
395
|
-
// The user (or AI agent) can open the URL to submit — no auth needed.
|
|
396
|
-
const REPO = "nteract/semiotic";
|
|
397
|
-
server.tool("reportIssue", "Generate a GitHub issue URL for Semiotic bug reports or feature requests. Pass { title, body, labels? }. Returns a URL the user can open to submit. For rendering bugs, include the component name, props summary, and any diagnoseConfig output in the body.", {}, async (args) => {
|
|
406
|
+
}
|
|
407
|
+
async function reportIssueHandler(args) {
|
|
398
408
|
const title = args.title;
|
|
399
409
|
const body = args.body;
|
|
400
410
|
const labels = args.labels;
|
|
@@ -416,11 +426,168 @@ server.tool("reportIssue", "Generate a GitHub issue URL for Semiotic bug reports
|
|
|
416
426
|
return {
|
|
417
427
|
content: [{ type: "text", text: `Open this URL to submit the issue:\n\n${url}` }],
|
|
418
428
|
};
|
|
419
|
-
}
|
|
420
|
-
//
|
|
429
|
+
}
|
|
430
|
+
// Named theme presets (inlined to avoid runtime dependency on semiotic-themes bundle)
|
|
431
|
+
const THEME_PRESET_NAMES = [
|
|
432
|
+
"light", "dark", "high-contrast",
|
|
433
|
+
"pastels", "pastels-dark",
|
|
434
|
+
"bi-tool", "bi-tool-dark",
|
|
435
|
+
"italian", "italian-dark",
|
|
436
|
+
"tufte", "tufte-dark",
|
|
437
|
+
"journalist", "journalist-dark",
|
|
438
|
+
"playful", "playful-dark",
|
|
439
|
+
];
|
|
440
|
+
async function applyThemeHandler(args) {
|
|
441
|
+
const name = args.name;
|
|
442
|
+
if (!name) {
|
|
443
|
+
return {
|
|
444
|
+
content: [{ type: "text", text: `Available theme presets:\n${THEME_PRESET_NAMES.join(", ")}\n\nPass { name: "tufte" } to get the CSS custom properties and ThemeProvider usage for that theme.\n\nLight-mode presets: ${THEME_PRESET_NAMES.filter(n => !n.includes("dark")).join(", ")}\nDark-mode presets: ${THEME_PRESET_NAMES.filter(n => n.includes("dark")).join(", ")}` }],
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (!THEME_PRESET_NAMES.includes(name)) {
|
|
448
|
+
return {
|
|
449
|
+
content: [{ type: "text", text: `Unknown theme "${name}". Available: ${THEME_PRESET_NAMES.join(", ")}` }],
|
|
450
|
+
isError: true,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const usage = [
|
|
454
|
+
`## Theme: "${name}"`,
|
|
455
|
+
"",
|
|
456
|
+
"### Option 1: ThemeProvider (recommended)",
|
|
457
|
+
"```jsx",
|
|
458
|
+
`import { ThemeProvider } from "semiotic"`,
|
|
459
|
+
`<ThemeProvider theme="${name}">`,
|
|
460
|
+
` <LineChart ... />`,
|
|
461
|
+
`</ThemeProvider>`,
|
|
462
|
+
"```",
|
|
463
|
+
"",
|
|
464
|
+
"### Option 2: Import the theme object",
|
|
465
|
+
"```jsx",
|
|
466
|
+
`import { ${name.replace(/-./g, c => c[1].toUpperCase()).replace(/^./, c => c.toUpperCase()).replace(/Dark$/, '_DARK').replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()} } from "semiotic/themes"`,
|
|
467
|
+
`<ThemeProvider theme={themeObject}>`,
|
|
468
|
+
` <BarChart ... />`,
|
|
469
|
+
`</ThemeProvider>`,
|
|
470
|
+
"```",
|
|
471
|
+
"",
|
|
472
|
+
"### Option 3: CSS custom properties (no React required)",
|
|
473
|
+
"```jsx",
|
|
474
|
+
`import { themeToCSS } from "semiotic/themes"`,
|
|
475
|
+
`import { ${name.replace(/-./g, c => c[1].toUpperCase()).replace(/^./, c => c.toUpperCase()).replace(/Dark$/, '_DARK').replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()} } from "semiotic/themes"`,
|
|
476
|
+
`const css = themeToCSS(themeObject, ".my-charts")`,
|
|
477
|
+
"// Outputs CSS custom properties string for embedding in a stylesheet",
|
|
478
|
+
"```",
|
|
479
|
+
"",
|
|
480
|
+
"### Option 4: Design tokens JSON",
|
|
481
|
+
"```jsx",
|
|
482
|
+
`import { themeToTokens } from "semiotic/themes"`,
|
|
483
|
+
`const tokens = themeToTokens(themeObject)`,
|
|
484
|
+
"// Style Dictionary / DTCG-compatible token format",
|
|
485
|
+
"```",
|
|
486
|
+
"",
|
|
487
|
+
"For accessibility, consider `\"high-contrast\"` which uses `COLOR_BLIND_SAFE_CATEGORICAL` (Wong 2011 palette).",
|
|
488
|
+
];
|
|
489
|
+
return {
|
|
490
|
+
content: [{ type: "text", text: usage.join("\n") }],
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
// ── Server factory ───────────────────────────────────────────────────────
|
|
494
|
+
// Creates a fresh McpServer with all tools registered.
|
|
495
|
+
// HTTP mode needs one instance per session (McpServer can only connect to one transport).
|
|
496
|
+
// Stdio mode uses a single instance.
|
|
497
|
+
function createServer() {
|
|
498
|
+
const srv = new mcp_js_1.McpServer({
|
|
499
|
+
name: "semiotic",
|
|
500
|
+
version: schema.version || "3.0.0",
|
|
501
|
+
});
|
|
502
|
+
srv.tool("getSchema", `Return the prop schema for a Semiotic chart component. Pass { component: '<name>' } to get its props, or omit component to list all available components. Components marked [renderable] can be passed to renderChart for static SVG output.`, { component: zod_1.z.string().optional().describe("Component name, e.g. 'LineChart'. Omit to list all.") }, getSchemaHandler);
|
|
503
|
+
srv.tool("suggestChart", "Recommend Semiotic chart types for a given data sample. Pass { data: [...] } with 1-5 sample objects. Optionally pass intent to narrow suggestions. Returns ranked recommendations with example props.", {
|
|
504
|
+
data: zod_1.z.array(zod_1.z.record(zod_1.z.string(), zod_1.z.unknown())).min(1).max(5).describe("1-5 sample data objects"),
|
|
505
|
+
intent: zod_1.z.enum(["comparison", "trend", "distribution", "relationship", "composition", "geographic", "network", "hierarchy"]).optional().describe("Visualization intent to narrow suggestions"),
|
|
506
|
+
}, suggestChartHandler);
|
|
507
|
+
srv.tool("renderChart", `Render a Semiotic chart to static SVG or PNG. 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(", ")}.`, {
|
|
508
|
+
component: zod_1.z.string().describe("Chart component name, e.g. 'LineChart', 'BarChart'"),
|
|
509
|
+
props: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional().describe("Chart props object, e.g. { data: [...], xAccessor: 'x' }."),
|
|
510
|
+
theme: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional().describe("CSS custom properties for theming, e.g. { '--semiotic-bg': '#1a1a2e', '--semiotic-text': '#ededed' }. Only --semiotic-* variables are applied."),
|
|
511
|
+
format: zod_1.z.enum(["svg", "png"]).optional().describe("Output format: 'svg' (default) returns SVG markup, 'png' returns a Base64-encoded PNG image. PNG requires the 'sharp' package."),
|
|
512
|
+
}, renderChartHandler);
|
|
513
|
+
srv.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for common problems (empty data, bad dimensions, missing accessors, wrong data shape, color contrast issues, etc). Checks WCAG color contrast ratios and suggests COLOR_BLIND_SAFE_CATEGORICAL for accessibility. Returns a human-readable diagnostic report with actionable fixes.", {
|
|
514
|
+
component: zod_1.z.string().describe("Chart component name, e.g. 'LineChart'"),
|
|
515
|
+
props: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional().describe("Chart props object, e.g. { data: [...], xAccessor: 'x' }."),
|
|
516
|
+
}, diagnoseConfigHandler);
|
|
517
|
+
srv.tool("reportIssue", "Generate a GitHub issue URL for Semiotic bug reports or feature requests. Returns a URL the user can open to submit. For rendering bugs, include the component name, props summary, and any diagnoseConfig output in the body.", {
|
|
518
|
+
title: zod_1.z.string().describe("Issue title, e.g. 'Bug: BarChart tooltip shows undefined'"),
|
|
519
|
+
body: zod_1.z.string().optional().describe("Issue body with details, reproduction steps, diagnoseConfig output"),
|
|
520
|
+
labels: zod_1.z.union([zod_1.z.array(zod_1.z.string()), zod_1.z.string()]).optional().describe("GitHub labels, e.g. ['bug'] or 'bug'"),
|
|
521
|
+
}, reportIssueHandler);
|
|
522
|
+
srv.tool("applyTheme", `Get usage instructions for a named Semiotic theme preset. Returns ThemeProvider examples, CSS custom properties, and design token export patterns. Available themes: ${THEME_PRESET_NAMES.join(", ")}.`, {
|
|
523
|
+
name: zod_1.z.string().optional().describe("Theme preset name, e.g. 'tufte', 'pastels-dark', 'bi-tool'. Omit to list all available themes."),
|
|
524
|
+
}, applyThemeHandler);
|
|
525
|
+
return srv;
|
|
526
|
+
}
|
|
527
|
+
// ── Startup ──────────────────────────────────────────────────────────────
|
|
528
|
+
const cliArgs = process.argv.slice(2);
|
|
529
|
+
const httpMode = cliArgs.includes("--http");
|
|
530
|
+
const portFlagIndex = cliArgs.indexOf("--port");
|
|
531
|
+
const parsedPort = portFlagIndex !== -1 && cliArgs[portFlagIndex + 1] != null
|
|
532
|
+
? parseInt(cliArgs[portFlagIndex + 1], 10)
|
|
533
|
+
: NaN;
|
|
534
|
+
const port = Number.isFinite(parsedPort) ? parsedPort : 3001;
|
|
421
535
|
async function main() {
|
|
422
|
-
|
|
423
|
-
|
|
536
|
+
if (httpMode) {
|
|
537
|
+
// HTTP mode — session-based, one server+transport per session
|
|
538
|
+
const sessions = new Map();
|
|
539
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
540
|
+
// CORS headers for browser-based inspectors
|
|
541
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
542
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
543
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
|
|
544
|
+
res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
|
|
545
|
+
if (req.method === "OPTIONS") {
|
|
546
|
+
res.writeHead(204);
|
|
547
|
+
res.end();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
551
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
552
|
+
// Existing session — route to its transport
|
|
553
|
+
const session = sessions.get(sessionId);
|
|
554
|
+
await session.transport.handleRequest(req, res);
|
|
555
|
+
}
|
|
556
|
+
else if (!sessionId) {
|
|
557
|
+
// New session — create server + transport
|
|
558
|
+
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
559
|
+
sessionIdGenerator: () => `semiotic-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
560
|
+
});
|
|
561
|
+
const srv = createServer();
|
|
562
|
+
await srv.connect(transport);
|
|
563
|
+
transport.onclose = () => {
|
|
564
|
+
const sid = transport.sessionId;
|
|
565
|
+
if (sid)
|
|
566
|
+
sessions.delete(sid);
|
|
567
|
+
};
|
|
568
|
+
await transport.handleRequest(req, res);
|
|
569
|
+
const sid = transport.sessionId;
|
|
570
|
+
if (sid) {
|
|
571
|
+
sessions.set(sid, { server: srv, transport });
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
// Session ID provided but not found — stale session
|
|
576
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
577
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Unknown session. Send a request without mcp-session-id to start a new session." }, id: null }));
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
httpServer.listen(port, () => {
|
|
581
|
+
console.error(`Semiotic MCP server (HTTP) listening on http://localhost:${port}`);
|
|
582
|
+
console.error("Tools: getSchema, suggestChart, renderChart, diagnoseConfig, reportIssue");
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
// Default: stdio mode for Claude Desktop, Claude Code, Cursor, etc.
|
|
587
|
+
const srv = createServer();
|
|
588
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
589
|
+
await srv.connect(transport);
|
|
590
|
+
}
|
|
424
591
|
}
|
|
425
592
|
main().catch((err) => {
|
|
426
593
|
console.error("MCP server error:", err);
|
|
@@ -53,12 +53,14 @@ function renderHOCToSVG(componentName, props) {
|
|
|
53
53
|
error: `Unknown component "${componentName}". Available: ${Object.keys(componentRegistry_1.COMPONENT_REGISTRY).join(", ")}`,
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
-
// Validate props
|
|
56
|
+
// Validate props (skip for components not in the validation map, e.g. geo)
|
|
57
57
|
const validation = (0, ai_1.validateProps)(componentName, props);
|
|
58
|
-
|
|
58
|
+
const errors = validation.errors ?? [];
|
|
59
|
+
const isUnknownComponentOnly = errors.length === 1 && errors[0].startsWith("Unknown component");
|
|
60
|
+
if (!validation.valid && !isUnknownComponentOnly) {
|
|
59
61
|
return {
|
|
60
62
|
svg: null,
|
|
61
|
-
error: `Validation errors:\n${
|
|
63
|
+
error: `Validation errors:\n${errors.join("\n")}`,
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
66
|
// Disable hover (not useful in static SVG)
|
package/ai/examples.md
CHANGED
|
@@ -902,3 +902,96 @@ const containerRef = useRef<HTMLDivElement>(null)
|
|
|
902
902
|
```
|
|
903
903
|
|
|
904
904
|
Key: `exportChart(wrapperDiv, { format, filename, scale, background })`. It queries the wrapper for canvas and SVG elements internally. Default format: PNG with 2x scale.
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
908
|
+
## Theming & Brand Styling
|
|
909
|
+
|
|
910
|
+
### CSS Custom Properties (no React context needed)
|
|
911
|
+
|
|
912
|
+
```jsx
|
|
913
|
+
// Dark theme via CSS custom properties on a wrapper div
|
|
914
|
+
<div style={{
|
|
915
|
+
"--semiotic-bg": "#1a1a2e",
|
|
916
|
+
"--semiotic-text": "#ededed",
|
|
917
|
+
"--semiotic-text-secondary": "#aaa",
|
|
918
|
+
"--semiotic-grid": "#333",
|
|
919
|
+
"--semiotic-border": "#555",
|
|
920
|
+
"--semiotic-font-family": "'Georgia', serif",
|
|
921
|
+
"--semiotic-tooltip-bg": "#1a1a2e",
|
|
922
|
+
"--semiotic-tooltip-text": "#ededed",
|
|
923
|
+
"--semiotic-tooltip-radius": "8px",
|
|
924
|
+
"--semiotic-tooltip-shadow": "0 4px 12px rgba(0,0,0,0.4)",
|
|
925
|
+
"--semiotic-primary": "#ff6b6b",
|
|
926
|
+
"--semiotic-focus": "#ff6b6b",
|
|
927
|
+
}}>
|
|
928
|
+
<LineChart
|
|
929
|
+
data={[{ x: 1, y: 10 }, { x: 2, y: 20 }, { x: 3, y: 15 }]}
|
|
930
|
+
xAccessor="x" yAccessor="y"
|
|
931
|
+
showGrid
|
|
932
|
+
annotations={[{ type: "y-threshold", value: 18, label: "Target" }]}
|
|
933
|
+
/>
|
|
934
|
+
</div>
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
### ThemeProvider (React context)
|
|
938
|
+
|
|
939
|
+
```jsx
|
|
940
|
+
import { ThemeProvider, DARK_THEME, COLOR_BLIND_SAFE_CATEGORICAL } from "semiotic"
|
|
941
|
+
|
|
942
|
+
// Preset dark theme
|
|
943
|
+
<ThemeProvider theme="dark">
|
|
944
|
+
<BarChart data={data} categoryAccessor="name" valueAccessor="value" />
|
|
945
|
+
</ThemeProvider>
|
|
946
|
+
|
|
947
|
+
// Custom brand theme (partial merge with defaults)
|
|
948
|
+
<ThemeProvider theme={{
|
|
949
|
+
mode: "light",
|
|
950
|
+
colors: {
|
|
951
|
+
primary: "#cc0000",
|
|
952
|
+
categorical: ["#cc0000", "#333333", "#c8a415", "#4682b4"],
|
|
953
|
+
background: "#fafafa",
|
|
954
|
+
text: "#1a1a1a",
|
|
955
|
+
textSecondary: "#666",
|
|
956
|
+
grid: "#e0e0e0",
|
|
957
|
+
border: "#e0e0e0",
|
|
958
|
+
},
|
|
959
|
+
typography: {
|
|
960
|
+
fontFamily: "'Helvetica Neue', Helvetica, Arial, sans-serif",
|
|
961
|
+
titleSize: 16, labelSize: 12, tickSize: 10,
|
|
962
|
+
},
|
|
963
|
+
tooltip: {
|
|
964
|
+
background: "#fafafa",
|
|
965
|
+
text: "#1a1a1a",
|
|
966
|
+
borderRadius: "4px",
|
|
967
|
+
},
|
|
968
|
+
}}>
|
|
969
|
+
<GroupedBarChart data={data} categoryAccessor="quarter" valueAccessor="revenue"
|
|
970
|
+
groupBy="region" colorBy="region" showGrid showLegend />
|
|
971
|
+
</ThemeProvider>
|
|
972
|
+
|
|
973
|
+
// Color-blind safe palette (Wong 2011 — 8 colors)
|
|
974
|
+
<Scatterplot data={data} xAccessor="x" yAccessor="y"
|
|
975
|
+
colorBy="category" colorScheme={COLOR_BLIND_SAFE_CATEGORICAL} />
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
### Annotations with Theme Colors
|
|
979
|
+
|
|
980
|
+
```jsx
|
|
981
|
+
// Annotations inherit --semiotic-primary and --semiotic-text-secondary from theme
|
|
982
|
+
<LineChart
|
|
983
|
+
data={salesData}
|
|
984
|
+
xAccessor="month" yAccessor="revenue"
|
|
985
|
+
annotations={[
|
|
986
|
+
// Threshold line — defaults to --semiotic-primary if no color set
|
|
987
|
+
{ type: "y-threshold", value: 50000, label: "Q3 Target" },
|
|
988
|
+
// Widget annotation at specific data point
|
|
989
|
+
{ type: "widget", month: "Jul", revenue: 72000, dy: -15, content: (
|
|
990
|
+
<span style={{ fontSize: 11, fontWeight: 700 }}>Record month</span>
|
|
991
|
+
)},
|
|
992
|
+
// Enclose a cluster of outliers
|
|
993
|
+
{ type: "enclose", coordinates: outlierPoints, label: "Outlier cluster", padding: 15 },
|
|
994
|
+
]}
|
|
995
|
+
showGrid
|
|
996
|
+
/>
|
|
997
|
+
```
|