Every chart rendered by semiotic/server can be exported as SVG, PNG, or animated GIF. The SVG is self-contained — embed it directly in HTML, email, or a PDF. The PNG and GIF formats use sharp andgifenc on the server, but this page demonstrates client-side previews of the same output.
SVG Export
Self-contained SVG with inline styles. Works everywhere — HTML, email, PDFs. Smallest file size, infinite resolution.
Code
JS
import { renderChart } from "semiotic/server" const svg = renderChart("LineChart", { data: latencyData, xAccessor: "x", yAccessor: "y", theme: "journalist", title: "API Latency (ms) — 14:00-14:30 UTC", showGrid: true, annotations: [ { type: "y-threshold", value: 200, label: "SLA: 200ms", color: "#e45050" }, ], }) fs.writeFileSync("incident.svg", svg)
PNG Export
Rasterized at 2x for retina displays. Best for Slack, social media, and OG images. Requires sharp on the server.
Code
JS
import { renderToImage } from "semiotic/server" const png = await renderToImage("BarChart", { data, categoryAccessor: "category", valueAccessor: "value", theme: "carbon", title: "Revenue by Region", }, { format: "png", scale: 2 }) fs.writeFileSync("chart@2x.png", png)
HTML Embed
Paste directly into any HTML page. No JavaScript needed. The SVG includes role='img', title, and desc for accessibility.
Code
JS
// In your HTML template or React component: <div style={{ maxWidth: 400 }} dangerouslySetInnerHTML={{ __html: svg }} /> // Or in plain HTML: <div style="max-width:400px"> ${svg} </div>
Animated GIF — Incident Replay
A static chart shows you the breach. The animation shows you the buildup. Watch 30 minutes of API latency unfold: steady state, early wobble at T-12m, escalating oscillations at T-7m, and the SLA breach at T-2m. This is what you attach to the incident report — not a screenshot, but the story.
Code
JS
import { renderToAnimatedGif } from "semiotic/server" // 30 minutes of latency data at 1-minute intervals const gif = await renderToAnimatedGif("line", latencyData, { xAccessor: "minute", yAccessor: "latencyMs", theme: "dark", title: "API Latency — Incident Replay", width: 440, height: 260, annotations: [ { type: "y-threshold", value: 200, label: "SLA: 200ms", color: "#e45050" }, ], }, { fps: 10, stepSize: 1, // one data point per frame xExtent: [0, 29], // lock axes so they don't shift yExtent: [60, 350], transitionFrames: 0, // no easing — raw data progression loop: true, }) // Attach to incident report, email, Slack fs.writeFileSync("incident-replay.gif", gif) // Or send in an alert email: await sendEmail({ to: "oncall@company.com", subject: "SLA Breach: API latency > 200ms", html: `<p>API latency breached the 200ms SLA at 14:28 UTC.</p> <img src="cid:replay" width="440" />`, attachments: [{ content: gif, cid: "replay", contentType: "image/gif" }], })
Network Fragmentation — Key Player Removal
A team communication network with one key connector. Watch what happens when they leave: edges disappear one by one, the connector node vanishes, and two isolated clusters remain. A static graph shows the before or after — the animation shows how fragile the topology was.
Code
JS
import { generateFrameSequence } from "semiotic/server" // Each snapshot is a complete graph state const snapshots = [ { nodes: allNodes, edges: allEdges }, // steady { nodes: allNodes, edges: withoutEdge("Lead→Dave") }, { nodes: allNodes, edges: withoutEdge("Lead→Carol") }, // ...remove remaining edges to Lead... { nodes: nodesWithoutLead, edges: remainingEdges }, // split ] const frames = generateFrameSequence( "ForceDirectedGraph", snapshots, { width: 440, height: 300, theme: "dark", showLabels: true } ) // frames is string[] — cycle on client or encode as GIF on server
ETL Pipeline — Failover
A multi-stage data pipeline: three source tables feed Kafka, which splits to Transform-A and Transform-B, both writing to Warehouse and Dashboard. Watch Transform-B degrade — throughput drops, Backup-B spins up and scales, then Transform-B goes offline entirely. Nodes are color-coded by stage: blue sources, teal ingestion, amber transforms, muted amber backup, green sinks.
Code
JS
import { generateFrameSequence } from "semiotic/server" const stageColors = { "Clickstream": "#7b9ec4", "Logs": "#7b9ec4", "Metrics": "#7b9ec4", "Kafka": "#5ba8a0", "Transform-A": "#d4a24e", "Transform-B": "#d4a24e", "Backup-B": "#c49860", // muted variant of transform amber "Warehouse": "#6aaf6a", "Dashboard": "#6aaf6a", } const frames = generateFrameSequence("SankeyDiagram", [normalFlow, degrading, failing, failoverComplete], { width: 520, height: 300, theme: "light", showLabels: true, nodeStyle: (d) => ({ fill: stageColors[d.data?.id] }), } )
Why animate?
A static chart answers "what happened." An animated chart answers "how did we get here."
The three animations above each tell a different kind of story. The incident replay shows a line chart building toward a threshold breach — you see the system straining before it broke. The network fragmentation shows what happens when a key connector leaves a team — a single topology snapshot can't convey that the graph was one node away from splitting. The Sankey failover shows data rerouting through a backup pipeline in real time — a static Sankey shows the before or after, not the transition.
All three work everywhere static images work — email, Slack, PDFs, wikis — but they carry temporal context that screenshots cannot.
For scenarios where data changes non-monotonically (edges disappearing, nodes removed, paths rerouting), use generateFrameSequence() — it accepts an array of complete data snapshots rather than progressively slicing a single array.
Format comparison
| Format | Size | Resolution | Animation | Email | Dependencies |
|---|
| SVG | 2-8 KB | Infinite | No | Yes | None |
| PNG | 20-80 KB | Fixed (scalable via `scale`) | No | Yes (as CID attachment) | sharp |
| JPEG | 15-50 KB | Fixed | No | Yes | sharp |
| GIF | 50-500 KB | Fixed | Yes | Yes | sharp + gifenc |
Physics GIFs
Standard renderToAnimatedGif advances by slicing data into frames. Physics charts need simulation time instead: enqueue bodies once, advance a deterministic fixed-step store, and render each live snapshot.
JS
import { generatePhysicsFrameSVGs, renderPhysicsToAnimatedGif, } from "semiotic/server" import { buildEventDropPhysics } from "semiotic/physics" const layout = buildEventDropPhysics({ data: events, timeAccessor: "eventTime", arrivalAccessor: "arrivalTime", windows: { size: 12 }, watermark: { delay: 18 }, ballRadius: 5, seed: 17, size: [640, 360], timeScale: 0.08, }) const frames = generatePhysicsFrameSVGs({ width: 640, height: 360, config: layout.config, initialSpawns: layout.initialSpawns, initialSpawnPacing: layout.initialSpawnPacing, title: "Watermark replay", }, { fps: 12, frameCount: 36, }) const gif = await renderPhysicsToAnimatedGif({ width: 640, height: 360, config: layout.config, initialSpawns: layout.initialSpawns, initialSpawnPacing: layout.initialSpawnPacing, }, { fps: 12, frameCount: 36, })
Server-side API overview
JS
import { renderChart, // SVG string (sync) renderToImage, // PNG/JPEG Buffer (async, requires sharp) renderToAnimatedGif, // GIF Buffer (async, requires sharp + gifenc) renderPhysicsToAnimatedGif, // Physics GIF Buffer (async, requires sharp + gifenc) renderDashboard, // Multi-chart SVG string (sync) generateFrameSVGs, // Animation frame SVGs (sync, no deps) generatePhysicsFrameSVGs, // Physics frame SVGs (sync, no deps) } from "semiotic/server"