@vishxrad/openui-plotly
v0.3.1
Published
Standalone OpenUI + Plotly scaffold. Run `npx @vishxrad/openui-plotly my-app` for a working Next.js generative-UI chat with 47 typed Plotly chart components — or import <PlotlyChat /> into an existing app.
Maintainers
Readme
@vishxrad/openui-plotly
A single-command OpenUI chat shell with 47 typed Plotly chart components built in. An LLM speaking openui-lang gets bar, line, heatmap, violin, sankey, sunburst, candlestick, scatter3d, choropleth, parallel coordinates, and the rest of Plotly's catalog — plus a Figure escape hatch for raw Plotly Graph-Objects JSON. Replaces the multi-package openui-cli scaffolding flow.
Scaffold a new app
npx @vishxrad/openui-plotly my-appThat's it. The CLI generates a complete Next.js 16 + React 19 project with <PlotlyChat /> already wired up and an /api/chat OpenAI proxy. Then:
cd my-app
cp .env.local.example .env.local # paste your OPENAI_API_KEY
npm run dev # http://localhost:3000Open the URL, ask the agent "plot Q4 revenue by month for products A, B, C as grouped bars" — done.
Add to an existing app
If you already have a Next.js / Vite / Remix app and want to drop the chat into it:
npm install @vishxrad/openui-plotly// src/app/page.tsx (or equivalent)
"use client";
import "@vishxrad/openui-plotly/styles.css";
import { PlotlyChat } from "@vishxrad/openui-plotly";
export default function Home() {
return (
<div className="h-screen w-screen overflow-hidden">
<PlotlyChat />
</div>
);
}// src/app/api/chat/route.ts — your LLM proxy
import { NextRequest } from "next/server";
import OpenAI from "openai";
const client = new OpenAI();
export async function POST(req: NextRequest) {
const { messages, systemPrompt } = await req.json();
const response = await client.chat.completions.create({
model: "gpt-5.2",
messages: [{ role: "system", content: systemPrompt }, ...messages],
stream: true,
});
return new Response(response.toReadableStream(), {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}Set OPENAI_API_KEY, npm run dev, working chat.
Component catalog
| Family | Components |
|---|---|
| Layout | Stack, Card, CardHeader, Heading, Text, Callout, KPI |
| Cartesian | Bar, Line, Scatter, Area, Histogram |
| Distributions | Violin, Box |
| Matrix & 2D-density | Heatmap, Histogram2D, Histogram2DContour, Contour |
| Hierarchical | Sunburst, Treemap, Icicle |
| Categorical | Pie, Donut, Funnel, FunnelArea, Waterfall |
| Flow | Sankey |
| Multivariate | ScatterMatrix, ParCoords, ParCats |
| Financial | Candlestick, OHLC |
| Polar | ScatterPolar, BarPolar |
| Specialty coords | ScatterTernary, ScatterSmith |
| Geo | Choropleth, ScatterGeo |
| Data display | Indicator, Table |
| Escape hatches | Figure, PlotlyJSON |
The LLM is taught these components via the auto-generated system prompt — you don't write Plotly schema by hand.
Customizing <PlotlyChat />
<PlotlyChat
apiUrl="/api/llm" // default: "/api/chat"
agentName="Data analyst" // default: "OpenUI × Plotly"
systemPrompt={customPrompt} // default: full Plotly catalog prompt
/>Or supply your own message processor (overrides apiUrl):
<PlotlyChat
processMessage={async ({ messages, abortController }) => {
return fetch("https://my-backend.example.com/agent", {
method: "POST",
body: JSON.stringify({ messages }),
signal: abortController.signal,
});
}}
/>Custom chat shell
If <PlotlyChat /> is too restrictive, drop down to the lower-level pieces — they're all re-exported from this package:
"use client";
import "@vishxrad/openui-plotly/styles.css";
import {
FullScreen,
openAIMessageFormat,
openAIReadableStreamAdapter,
plotlyLibrary,
plotlyPromptOptions,
PlotlyAssistantMessage,
} from "@vishxrad/openui-plotly";
const systemPrompt = plotlyLibrary.prompt(plotlyPromptOptions);
export default function MyChat() {
return (
<FullScreen
processMessage={async ({ messages, abortController }) =>
fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
systemPrompt,
messages: openAIMessageFormat.toApi(messages),
}),
signal: abortController.signal,
})
}
streamProtocol={openAIReadableStreamAdapter()}
componentLibrary={plotlyLibrary}
agentName="Custom"
assistantMessage={({ message, isStreaming }) => (
<PlotlyAssistantMessage
message={message}
isStreaming={isStreaming}
library={plotlyLibrary}
/>
)}
/>
);
}This is exactly what <PlotlyChat /> does internally — just inlined so you can swap any piece.
Two-tier component API
Following Plotly's own dual API:
Tier 1 — Typed Plotly-Express-style components. Small zod schemas, ergonomic for the LLM:
Bar(rows, "month", "revenue", "product") # Express style
Bar(null, ["Jan", "Feb", "Mar"], [1.2, 1.5, 1.8]) # Graph-Objects styleTier 2 — Figure accepts the full Plotly Graph-Objects schema. Use this for any chart Tier 1 doesn't cover — multi-trace overlays, dual axes, animation frames, custom polar/ternary/carpet, advanced 3D scenes:
Figure(
[
{ type: "bar", x: months, y: vol, name: "Volume" },
{ type: "scatter", mode: "lines+markers", x: months, y: trend, yaxis: "y2", name: "Trend" }
],
{ yaxis2: { overlaying: "y", side: "right" } }
)Tier 0 — PlotlyJSON({ figure }) renders a fully-formed Plotly figure JSON verbatim. Use this when a backend tool returns a precomputed figure (e.g. Python fig.to_json()).
What's bundled
@openuidev/react-ui— chat shell (FullScreen, BottomTray, Copilot, ThemeProvider)@openuidev/react-headless— message formats, stream adapters@openuidev/react-lang— generative-UI runtime (Renderer, parser, hooks)plotly.js-dist-min— full pre-built Plotly bundlereact-plotly.js— React wrapper
You only need react, react-dom, and (transitively) zod from the consumer side.
Bundle & loading
- The Plotly bundle (~3.5 MB minified) is loaded lazily via
React.lazyon first chart render. The chat shell first paint is unaffected. - Chart instances mount only after the assistant message finishes streaming — avoids token-by-token flicker as the LLM populates trace shape.
Framework support
- Next.js (App Router or Pages Router) — works out of the box. Wrap your chat shell page in
"use client". - Vite SPA — works out of the box.
- Remix / React Router — works.
- CRA — works.
The package never imports next/* or any other framework-specific module. Everything routes through React.lazy + Suspense.
License
MIT
