Openai Dotcom Viz
yzlnew/infra-skills
Build figures in OpenAI's blog / research / system-card "dotcom" visual style — both (a) bar charts (monochrome bars with a darker same-hue stroke, rounded corners, a black y-axis with outward ticks…
Render rich interactive visuals — SVG diagrams, HTML widgets, Chart.js charts, and interactive explainers — directly inline in chat using visualize().
$ npx skills add Classic298/open-webui-plugins --skill visualize -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install Classic298/open-webui-plugins visualize --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/Classic298/open-webui-plugins.git skills-src && mkdir -p .claude/skills && cp -r skills-src/inline-visualizer-v2 .claude/skills/visualize && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "visualize" agent skill from https://github.com/Classic298/open-webui-plugins/tree/main/inline-visualizer-v2 into .claude/skills/visualize/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "visualize", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/Classic298/open-webui-plugins/tree/main/inline-visualizer-v2Type this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add Classic298/open-webui-plugins --skill visualize -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install Classic298/open-webui-plugins visualize --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Classic298/open-webui-plugins.git skills-src && mkdir -p .agents/skills && cp -r skills-src/inline-visualizer-v2 .agents/skills/visualize && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "visualize" agent skill from https://github.com/Classic298/open-webui-plugins/tree/main/inline-visualizer-v2 into .agents/skills/visualize/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "visualize", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add Classic298/open-webui-plugins --skill visualize -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install Classic298/open-webui-plugins visualize --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Classic298/open-webui-plugins.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/inline-visualizer-v2 .cursor/skills/visualize && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "visualize" agent skill from https://github.com/Classic298/open-webui-plugins/tree/main/inline-visualizer-v2 into .cursor/skills/visualize/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "visualize", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/Classic298/open-webui-plugins.git --path inline-visualizer-v2--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add Classic298/open-webui-plugins --skill visualize -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install Classic298/open-webui-plugins visualize --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Classic298/open-webui-plugins.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/inline-visualizer-v2 .gemini/skills/visualize && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "visualize" agent skill from https://github.com/Classic298/open-webui-plugins/tree/main/inline-visualizer-v2 into .gemini/skills/visualize/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "visualize", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install Classic298/open-webui-plugins visualizeInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add Classic298/open-webui-plugins --skill visualize -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/Classic298/open-webui-plugins.git skills-src && mkdir -p .github/skills && cp -r skills-src/inline-visualizer-v2 .github/skills/visualize && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "visualize" agent skill from https://github.com/Classic298/open-webui-plugins/tree/main/inline-visualizer-v2 into .github/skills/visualize/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "visualize", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add Classic298/open-webui-plugins --skill visualize -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install Classic298/open-webui-plugins visualize --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Classic298/open-webui-plugins.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/inline-visualizer-v2 .opencode/skills/visualize && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "visualize" agent skill from https://github.com/Classic298/open-webui-plugins/tree/main/inline-visualizer-v2 into .opencode/skills/visualize/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "visualize", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
visualizeRender rich interactive visuals — SVG diagrams, HTML widgets, Chart.js charts, and interactive explainers — directly inline in chat using visualize().
Visualize is an agent skill from Classic298/open-webui-plugins. Render rich interactive visuals — SVG diagrams, HTML widgets, Chart.js charts, and interactive explainers — directly inline in chat using visualize(). Use only when the user explicitly asks for a visualization, diagram, chart, graph, drawing, map, dashboard, or similar visual artifact. Do not use for ordinary markdown, code blocks, file previews, or answer formatting
Its SKILL.md is about 10k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files (for example `README.md` and `tool.py`).
It sits in Development, covering Diagrams. It works with Chart.js. The repository describes itself as: A curated collection of Open WebUI plugins - tools, skills, filters, pipes, actions and events that extend your AI chat experience. The licence is BSD-3-Clause.
2 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit d4a78a6. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships script files (Python), which the agent can run.
From the folder's file list and the shell code blocks in SKILL.md.
Links to these hosts (documentation or services it may open):
cdnjs.cloudflare.comcdn.jsdelivr.netgithub.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Visualize loads about 10k tokens when it runs. Until then it costs about 95 tokens; SKILL.md has 5,169 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from Classic298/open-webui-plugins at commit d4a78a6, republished under its BSD-3-Clause licence (© Classic298). 5,169 words, ~10,389 tokens.
.claude/skills/visualize/SKILL.md (or your agent's skills folder). This skill also uses 2 other files; get the full folder from GitHub.This is the handbook/tutorial on how to use the visualizer tool. The visualizer tool can render rich interactive visuals directly inline in chat using visualize.
you called the view_skill() tool to read the tutorial/handbook about this tool. Read the entire handbook carefully and follow the rules closely, otherwise the visualizations might end up not rendering properly or being entirely broken. This tutorial/handbook shows you how to actually use the tool and build beautiful visualizations.
<html>, <head>, <body>)The raw markers + SVG source are auto-hidden from the chat — users see only the rendered iframe filling in live.
Example response structure:
""" I'll visualize the attention mechanism for you.
@@@VIZ-START
<svg viewBox="0 0 680 240">
<!-- content streams here, renders live -->
</svg>
@@@VIZ-END
As you can see, each query token attends to all key tokens simultaneously. """
Streaming rules:
<viz> or <svg data-iv> — only the text markers are detected.<style> first → visible content → <script> last.<script> you include runs once, after the full block has streamed in.These tags get theme-aware default styling when emitted without a class or inline style attribute. Other attributes (placeholder, value, id, aria-*, min/max, etc.) are fine — they don't disable the defaults. Adding class or style is treated as an opt-out: the default is suppressed and you can style it from scratch. Useful for short forms or quick UIs where the design doesn't need to deviate.
Pre-styled elements included in the tool:
<button> — themed button. Use it for actions.<input type="text|number|email|search|password|tel|url|date|time|datetime-local"> — themed text input. Use it where a user types or picks a value.<input type="range"> — slider. Use it for "from–to" picks, intensity dials, or any continuous value where exact precision doesn't matter.<input type="checkbox">, <input type="radio"> — multi-pick / single-pick. Self-explanatory.<textarea> — multi-line text input.
<select> — dropdown. Use it when a list of choices is too long for radios.<label>, <fieldset>, <legend> — form structure. Group related inputs and label them.<kbd> — keyboard-key cap. Use it whenever you mention a shortcut, so the key visually pops as a key.<kbd>⌘</kbd><kbd>K</kbd><kbd>Ctrl</kbd><kbd>K</kbd><hr> — horizontal divider. Separate sections inside a card or between groups of content.
<details> / <summary> — collapsible disclosure. Use it for progressive disclosure: hide secondary detail behind a clickable summary so the surface stays clean.
<blockquote> — pull-quote / callout. Use it to set apart a quote, an aside, or a piece of context the reader should pause on.
<table> (with <thead> / <tbody> / <th> / <td> / <caption>) — tabular data with multiple columns and rows. Use it when the relationship between rows and columns matters. For numeric columns add align="right" or class="num" to the cells (right-aligns + tabular-nums).
<mark> — highlighter. Use it sparingly to draw attention to a key word or number inside a sentence.<dl> / <dt> / <dd> — definition lists. **Far cheaper than tables for label/value layouts**. Three modes:
- Bare <dl> → **stacked glossary**. Best when each term needs a sentence or two: definitions, FAQs, term-explained-below.
- <dl data-layout="grid"> → **two-column card**. The lightweight alternative to a table when you have key/value pairs and don't need row separators or hover: contact cards, metadata blocks, summary panels, settings rows.
- <dl data-layout="inline"> → **pill row** of label: value pairs (wrap each <dt>/<dd> in a <div>). Best for a tight strip of facts at the top of a card or near a chart: small numbers, status flags, tags. Colon separator is added automatically via CSS.
Bonus on bare elements:
The default accent is purple. Switch to one of the other ramps via the data-accent attribute. The chosen color drives --accent and --accent-foreground, which in turn power focus rings, checkbox/radio fills, and any var(--accent) reference you write yourself. The same nine names match the chart color ramps, so a teal-accented form sits naturally next to a teal-accented chart.
Available values: purple (default), teal, coral, pink, gray, blue, green, amber, red
To apply an accent color globally to the whole visualization: wrap the entire content in a single root <div data-accent="…">.
Every supported element inside inherits the chosen accent.
<div data-accent="teal">
<style>/* CSS */</style>
…all focus rings, checkboxes, and var(--accent) consumers go teal…
</div>
To apply an accent color to a specific section: set data-accent on any inner container to recolor just its subtree:
<div data-accent="teal">
<button>Save</button> <!-- teal focus ring -->
<input type="checkbox" checked> <!-- teal accent -->
</div>
<button>Cancel</button> <!-- still default purple -->
To apply an accent color to a single element: set directly on an element to recolor just it:
<button data-accent="green">Approve</button>
<button data-accent="red">Reject</button>
Both light and dark themes are handled — accent values track per-theme ramp stops automatically, and foreground text color flips for legibility in dark mode. No manual override needed.
Pick an accent that matches the topic: green for finance/positive, red for warnings/critical actions, blue for informational dashboards, amber for attention/caution, etc. Default to purple for neutral or multi-purpose visualizations.
These rules keep visuals clean, accessible, and consistent with the host UI:
The tool injects theme-aware CSS variables that adapt to light/dark mode automatically. Use them by default for text, surface, and border colors; reach for a specific hex only when the design genuinely calls for a fixed color (a brand mark, a deliberate accent that shouldn't track the theme).
| Token | Purpose |
|---|---|
| --color-text-primary | Main text |
| --color-text-secondary | Labels, muted text |
| --color-text-tertiary | Hints, placeholders |
| --color-text-info/success/warning/danger | Semantic text |
| --color-bg-primary | Main background |
| --color-bg-secondary | Cards, surfaces |
| --color-bg-tertiary | Page background |
| --color-border-tertiary | Default borders (0.15 alpha) |
| --color-border-secondary | Hover borders (0.3 alpha) |
| --font-sans | Default font |
| --font-mono | Code font |
| --radius-md / --radius-lg / --radius-xl | 8px / 12px / 16px |
Each ramp provides fill, stroke, and text variants that adapt to the theme automatically via CSS classes.
| Ramp | 50 (light fill) | 200 | 400 | 600 (light stroke) | 800 (light title) |
|---|---|---|---|---|---|
| purple | #EEEDFE | #AFA9EC | #7F77DD | #534AB7 | #3C3489 |
| teal | #E1F5EE | #5DCAA5 | #1D9E75 | #0F6E56 | #085041 |
| coral | #FAECE7 | #F0997B | #D85A30 | #993C1D | #712B13 |
| pink | #FBEAF0 | #ED93B1 | #D4537E | #993556 | #72243E |
| gray | #F1EFE8 | #B4B2A9 | #888780 | #5F5E5A | #444441 |
| blue | #E6F1FB | #85B7EB | #378ADD | #185FA5 | #0C447C |
| green | #EAF3DE | #97C459 | #639922 | #3B6D11 | #27500A |
| amber | #FAEEDA | #EF9F27 | #BA7517 | #854F0B | #633806 |
| red | #FCEBEB | #F09595 | #E24B4A | #A32D2D | #791F1F |
| Series | Color | Hex |
|---|---|---|
| 1 | teal-400 | #1D9E75 |
| 2 | purple-400 | #7F77DD |
| 3 | coral-400 | #D85A30 |
| 4 | blue-400 | #378ADD |
| 5 | amber-400 | #BA7517 |
For area/line fills, use same color at 20% opacity.
If you want to build a beautiful SVG to be rendered inside the chat, follow these rules too: Always use this SVG boilerplate:
<svg width="100%" viewBox="0 0 680 H">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5"
markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
</defs>
</svg>
Drop these on SVG elements instead of writing inline fill, stroke, or font-size. They track the theme automatically.
| Class | What it is | When to use |
|---|---|---|
| .t | 14px primary-color text | Default for any visible label inside a node, axis tick, or callout. |
| .ts | 12px secondary-color text | Subtitles, captions, units (e.g. "users", "ms"), supporting text under a .t label. |
| .th | 14px primary text, 500 weight | Node titles, KPI numbers, anything that needs to read as "the headline" of a small region. |
| .box | Neutral rect — secondary bg, tertiary border | Default container for a labeled region. Use whenever you need a neutral chip / panel and don't have a semantic color. |
| .node | Cursor-pointer + hover opacity on a <g> | Mark a <g> as clickable. Pair with onclick="sendPrompt(...)" so a user can drill into the topic. |
| .arr | 1.5px stroke matching theme borders | Arrow lines and connectors. Combine with marker-end="url(#arrow)". |
| .leader | 0.5px dashed guide line | Pulling a label to a part of an illustration when the label can't sit on top of it. |
| .c-{ramp} | Sets fill/stroke + text colors on a whole <g> from one of the 9 color ramps | Color a node by category — apply .c-teal (etc.) to a <g> and its direct children pick up the matching ramp (not deeper descendants). Un-classed, un-filled <path>/<polygon> children (pie wedges, areas) take the ramp's series color; on a classed or filled mark, fill="currentColor" opts back in. |
Browsers don't auto-size SVG boxes to text. To pick a width, estimate the rendered glyph width per character and size the box from the longest line.
<text> defaults to dominant-baseline="alphabetic" — y is the text's
baseline, not its center, so a label placed at the vertical midpoint of
a box actually sits ~4 px too high. For text inside a node, callout, or
any rounded rect, add dominant-baseline="central" and put y at the
box midpoint.
Keep the default (no dominant-baseline) for text that's meant to sit on a baseline: axis tick labels (resting on the axis line), legend labels (aligned to the swatch baseline), and anything where the bottom edge of the glyphs is the visual anchor. Setting central on those will make them look ~4 px low instead.
Single-line node:
<g class="node c-teal" onclick="sendPrompt('Tell me about X')">
<rect x="100" y="20" width="180" height="44" rx="8"/>
<text class="th" x="190" y="42" text-anchor="middle" dominant-baseline="central">Label</text>
</g>
Two-line node:
<g class="node c-teal">
<rect x="100" y="20" width="200" height="56" rx="8"/>
<text class="th" x="200" y="38" text-anchor="middle" dominant-baseline="central">Title</text>
<text class="ts" x="200" y="56" text-anchor="middle" dominant-baseline="central">Subtitle</text>
</g>
For diagrams that show what contains what: services inside zones, modules inside layers, components inside subsystems. The nesting itself is the information — outer regions are the system, inner regions are the parts.
For "how does this actually work" topics where the answer is spatial: how light refracts through a prism, how a transformer attention head weighs tokens, how a heat pump moves heat against a gradient. Draw the thing itself, not a labeled diagram about it.
Load Chart.js in your HTML fragment:
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
Setup pattern:
<div style="position: relative; height: 300px;">
<canvas id="chart"></canvas>
</div>
<script>
const ctx = document.getElementById('chart').getContext('2d');
const s = getComputedStyle(document.documentElement);
const textColor = s.getPropertyValue('--color-text-secondary').trim();
const gridColor = s.getPropertyValue('--color-border-tertiary').trim();
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Q1','Q2','Q3','Q4'],
datasets: [{ label: 'Revenue', data: [12,19,8,15],
backgroundColor: '#1D9E75', borderRadius: 4, borderSkipped: false }]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { labels: { color: textColor, font: { size: 12 } } } },
scales: {
x: { grid: { display: false }, ticks: { color: textColor, font: { size: 12 } }, border: { color: gridColor } },
y: { grid: { color: gridColor }, ticks: { color: textColor, font: { size: 12 } }, border: { display: false } }
}
}
});
</script>
Chart rules:
Chart type selection:
| Data shape | Type | Notes |
|---|---|---|
| Categories + values (a few items, comparable magnitudes) | Bar | Default for "compare values across labels". Switch to a horizontal bar (indexAxis: 'y') when labels are long, when there are 8+ categories, or when ranking is the point. |
| Time series, anything sampled at regular intervals | Line | tension: 0.3 for a natural curve. Stack multiple datasets when you're comparing trends, not when each line wanders independently — overlap gets unreadable past 4 lines. |
| Parts of a whole, ≤5 slices | Doughnut | Use cutout: '60%' so the empty middle can hold a total or label. Skip if the segments are very uneven (one slice >70%) — the small slices vanish; show a stacked bar instead. |
| Two continuous variables, looking for correlation | Scatter | Add a trend line if the relationship is the takeaway. For dense clouds, drop point opacity to 0.3–0.5 so density reads. |
| Stacked / cumulative composition over time | Stacked bar / stacked area | Bar when the buckets are discrete (months, segments); area when the underlying signal is continuous. |
| Single-value vs target / threshold | Bar with reference line or KPI card | A whole chart is overkill for one number — consider a metric card with a sparkline instead. |
| Multi-dimensional comparison (3–6 axes) | Radar | Only when the axes are genuinely commensurate — otherwise a small-multiples bar grid is clearer. |
Reach for inline SVG when the data is small, the shape is simple, or you want the chart to share design with surrounding diagrams (matching corner radii, palette, type). No script, no CDN — just shapes and text. Reach for Chart.js when you need axes, tooltips, hover, animation, or many series.
Good fits for inline SVG:
Theme consistency for inline SVG:
<text> for labels, captions, and headlines.
They pick up the theme's text colors and typography scale automatically.
Never set font-size or fill on label text manually unless you need a specific deviation.<g class="c-teal"> (or any of the 9 ramp classes) and let the SVG class system handle fill + stroke + text in one shot.Math hints for the less obvious shapes:
<div style="display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px;">
<div style="background:var(--color-bg-secondary); border:0.5px solid var(--color-border-tertiary);
border-radius:var(--radius-lg); padding:16px;">
<div style="font-size:12px; color:var(--color-text-secondary);">Revenue</div>
<div style="font-size:28px; font-weight:500; color:var(--color-text-primary); margin-top:4px;">$3,870</div>
<div style="font-size:12px; color:var(--color-text-success); margin-top:2px;">▲ 12.4%</div>
</div>
<!-- repeat for other metrics -->
</div>
Pair with a chart below for a compact dashboard. Add a tiny inline-SVG sparkline under each value if the trend matters.
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
<div style="background:var(--color-bg-secondary); border:0.5px solid var(--color-border-tertiary);
border-radius:var(--radius-lg); padding:18px;">
<div style="font-size:14px; font-weight:500;">Monolith</div>
<dl data-layout="grid" style="margin-top:12px;">
<dt>Deploy unit</dt><dd>1 service</dd>
<dt>Latency</dt><dd>Low (in-process)</dd>
<dt>Scaling</dt><dd>Vertical</dd>
</dl>
</div>
<div style="background:var(--color-bg-secondary); border:0.5px solid var(--color-border-tertiary);
border-radius:var(--radius-lg); padding:18px;">
<div style="font-size:14px; font-weight:500;">Microservices</div>
<dl data-layout="grid" style="margin-top:12px;">
<dt>Deploy unit</dt><dd>N services</dd>
<dt>Latency</dt><dd>Higher (network)</dd>
<dt>Scaling</dt><dd>Horizontal per service</dd>
</dl>
</div>
</div>
<label style="display:flex; gap:12px; align-items:center;">
<span style="min-width:80px;">Interest</span>
<input type="range" id="rate" min="0" max="20" step="0.1" value="5" style="flex:1;">
<span id="rate-out" style="min-width:48px; font-variant-numeric:tabular-nums;">5.0%</span>
</label>
<div id="result" style="margin-top:12px; font-size:24px; font-weight:500;"></div>
<script>
var rate = document.getElementById('rate');
var out = document.getElementById('rate-out');
var result = document.getElementById('result');
function recalc() {
var r = parseFloat(rate.value);
out.textContent = r.toFixed(1) + '%';
result.textContent = '$' + (10000 * Math.pow(1 + r/100, 10)).toFixed(0);
}
rate.addEventListener('input', recalc);
recalc();
</script>
The pattern generalises: every interactive element binds an input listener, recomputes a value, and writes it to a result node. Pair with an inline SVG that re-draws on every input change for a "live diagram".
<div style="display:flex; gap:4px; border-bottom:0.5px solid var(--color-border-tertiary);">
<button class="tab active" onclick="showTab('a', this)">Overview</button>
<button class="tab" onclick="showTab('b', this)">Details</button>
<button class="tab" onclick="showTab('c', this)">Source</button>
</div>
<div id="tab-a" class="tab-panel">…</div>
<div id="tab-b" class="tab-panel" hidden>…</div>
<div id="tab-c" class="tab-panel" hidden>…</div>
<style>
.tab { background:none; border:none; padding:8px 12px; cursor:pointer;
border-bottom:2px solid transparent; }
.tab.active { border-bottom-color: var(--accent); color: var(--color-text-primary); }
.tab-panel { padding:12px 0; }
</style>
<script>
function showTab(id, btn) {
document.querySelectorAll('.tab-panel').forEach(p => p.hidden = true);
document.getElementById('tab-' + id).hidden = false;
document.querySelectorAll('.tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
</script>
Persist the active tab with saveState/loadState so it survives reloads.
Charts in inactive tabs render at 0×0. Plotly, ECharts, and vis-network all measure their container at init time. If that container is inside a hidden / display:none panel, they paint into a zero-size canvas and stay blank even after the tab becomes visible. Two workarounds, pick one:
Lazy-init: only call Plotly.newPlot / echarts.init / new vis.Network the first time its tab is shown (track a tabInit[id] flag in the handler).
Resize on show: init everything up front (so data is ready), then in showTab call the right resize hook for whichever lib is in that tab. Note the API differs per library — c.resize() does not work for all of them:
// ECharts: instance.resize() echartsInstance.resize(); // Plotly: pass the container element, no .resize() on the chart Plotly.Plots.resize(document.getElementById('plotly-container')); // vis-network: redraw + fit — the instance has no .resize() networkInstance.redraw(); networkInstance.fit(); // Chart.js: instance.resize() — but Chart.js auto-resizes on // container size change so usually nothing needed.
Skip the resize call for D3 / Vega-Lite / inline SVG — they paint declaratively into the SVG namespace and aren't bothered by hidden parents.
A "Next ▶" button advances through a sequence of stages, each with its own caption and (optionally) a different highlighted region of the same diagram. Useful for explaining algorithms, processes, or any topic where the order matters more than the totals.
<div id="stage" style="font-size:14px;">Click Next to begin.</div>
<button onclick="step()">Next ▶</button>
<script>
var steps = [
'Step 1: request lands at the load balancer',
'Step 2: routed to a healthy backend',
'Step 3: backend writes to primary DB',
'Step 4: replica catches up async',
];
var i = 0;
function step() {
document.getElementById('stage').textContent = steps[i % steps.length];
i++;
}
</script>
sendPrompt(text) is the function that makes visualizations conversational. When called, it injects the given text into the chat input field and submits it — exactly as if the user had typed and sent it themselves. The model then receives that message and responds normally, creating a feedback loop between the visual and the conversation.
This is what separates a static diagram from an exploration interface. A user sees a system architecture diagram, clicks on the "Load Balancer" node, and the model receives "Tell me more about the load balancer — how does it distribute traffic across the backend services?" as a user message. The model then responds with details, and could even generate a new sub-diagram showing the load balancer internals. The user never had to type anything — they just clicked.
Without sendPrompt, interactive elements inside the iframe are isolated — they can toggle visibility, animate, or filter data, but they can never talk back to the model. The user sees a cool diagram but has to manually type follow-up questions. With sendPrompt, every clickable element becomes a conversation starter. The diagram itself becomes a navigation interface for the topic.
The text you pass to sendPrompt becomes the user's message to the model. Write it as a natural follow-up question — conversational, specific, and referencing the context of the diagram:
Good prompt text (specific, contextual, references the diagram):
Simple patterns — single-click sendPrompt on a node or button:
Form / preference collector — gather multiple user selections, then send them all at once. Use local JS to track choices (button highlights, state object) and a submit button that composes a sendPrompt from the collected answers:
<script>
var choices = {};
function pick(category, value, btn) {
choices[category] = value;
// Highlight selected button, dim siblings
btn.parentElement.querySelectorAll('button').forEach(function(b) {
b.classList.toggle('active', b === btn);
});
}
function submitChoices() {
var parts = [];
for (var k in choices) parts.push(k + ': ' + choices[k]);
sendPrompt('Here are my preferences:\n' + parts.join('\n') + '\nGive me a personalized recommendation based on these choices.');
}
</script>
<h3>What's your style?</h3>
<p style="margin:8px 0 4px;">Pace</p>
<button onclick="pick('pace','relaxed',this)">Relaxed</button>
<button onclick="pick('pace','moderate',this)">Moderate</button>
<button onclick="pick('pace','intensive',this)">Intensive</button>
<p style="margin:8px 0 4px;">Focus</p>
<button onclick="pick('focus','culture',this)">Culture</button>
<button onclick="pick('focus','nature',this)">Nature</button>
<button onclick="pick('focus','food',this)">Food</button>
<button onclick="submitChoices()" style="margin-top:12px; font-weight:500;">Get my recommendation →</button>
This pattern is powerful because the model receives a structured summary of all user preferences in one message. Use local JS for the selection UI (instant feedback), then sendPrompt only on final submit.
| User action | Use | Why |
|---|---|---|
| Learn more about a component | sendPrompt | Model gives a contextual explanation |
| Explore a stage / drill down | sendPrompt | Model can generate a sub-diagram |
| Submit answers or preferences | sendPrompt | Model evaluates or personalizes |
| Toggle views, adjust sliders | Local JS | Instant feedback, no reasoning needed |
| Filter/sort data | Local JS | Instant response, no model needed |
Build dashboards, charts, graphs, interactive functions, animated sections, moving objects, expandable detail sections, cards, copyable text elements and more. If the topic allows and it makes sense for the topic, build complex and visually stunning elements.
Visualizations should feel alive and polished — not static images dumped into chat. Build interfaces that invite interaction:
<details> elements or JS-toggled sections so users can explore at their own pace without overwhelming them upfrontThe goal is to build something that feels like a real app component embedded in chat with reactivity, sections and extra elements — not a screenshot. If the visualization has multiple facets, give the user controls to explore them. If it has hierarchical information, let them expand and collapse. If it has data, let them sort or filter.
openLink(url) opens a URL in a new browser tab from within the visualization iframe. Normal <a href="..."> links inside an iframe can behave unpredictably (opening inside the iframe, being blocked by sandbox restrictions, etc.). This function handles that by opening the link in the parent window instead.
<button onclick="openLink('https://docs.example.com/api-reference')">
Open API docs ↗
</button>
Or in SVG:
<g class="node c-blue" onclick="openLink('https://github.com/org/repo')">
<rect x="100" y="20" width="200" height="44" rx="8"/>
<text class="th" x="200" y="42" text-anchor="middle" dominant-baseline="central">View source ↗</text>
</g>
Use openLink for external references, documentation links, or source code links. Unlike sendPrompt, this navigates away from the chat — use it when the user needs to access an external resource, not when they need the model to explain something.
copyText(text) copies text to the system clipboard and automatically shows a localized "Copied" toast in the top-right corner of the iframe. Works from HTTPS and HTTP origins (falls back to execCommand('copy') if the async Clipboard API is blocked). Use this on "Copy" buttons inside interactive visualizations — data tables, code snippets, shareable values.
<button onclick="copyText(JSON.stringify(data, null, 2))">Copy JSON</button>
toast(message, kind) shows a small auto-dismissing banner inside the iframe. kind is optional and controls the text color: 'success' (green, default), 'info' (blue), 'warn' (amber), 'error' (red). Use it for status notifications inside long-running interactive tools — "Calculation done", "Invalid input", etc.
<button onclick="recompute(); toast('Recomputed', 'info')">Recompute</button>
Toasts auto-dismiss after ~2.2 s and stack vertically if fired in quick succession.
saveState(key, value) and loadState(key, fallback) proxy parent.localStorage with a key prefix scoped to this assistant message. State survives page reloads and tab switches, but two different chats (or different messages in the same chat) each get their own independent state — no cross-contamination.
<script>
// Restore toggle state on load
var showRaw = loadState('showRaw', false);
document.getElementById('raw-toggle').checked = showRaw;
applyView(showRaw);
function onToggleChange(el) {
saveState('showRaw', el.checked);
applyView(el.checked);
}
</script>
Use it for: selected tabs, picked chart range, hidden/shown layers, theme overrides, collapsed sections — anything the user would expect to be remembered when they re-open the chat.
Values are JSON-serialized. If localStorage is blocked (private browsing, sandboxed), both functions silently no-op and loadState returns fallback.
The CSP allowlists three CDN hosts for scripts only: any script served from them loads, CDN stylesheets and fonts are blocked, so inline any CSS a library needs.
Allowed hosts:
Common picks:
| Library | Why reach for it | Example loader |
|---|---|---|
| Chart.js | Bar / line / doughnut / scatter with animation out of the box | <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script> |
| D3.js | Custom data-driven SVG (force graphs, arcs, maps, non-standard charts) | <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script> |
| Vega-Lite | Declarative grammar of graphics — feed it a JSON spec, it draws the chart | <script src="https://cdn.jsdelivr.net/npm/vega@5"></script><script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script><script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script> |
| ECharts | Rich interactive dashboards, advanced chart types | <script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js"></script> |
| Plotly | Scientific / 3D plots, statistical charts | <script src="https://cdn.jsdelivr.net/npm/plotly.js-dist@2"></script> |
| vis-network | Force-directed network / node-link graphs | <script src="https://cdn.jsdelivr.net/npm/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script> (the standalone UMD bundle — exposes vis.Network and vis.DataSet. The bare vis-network.min.js on cdnjs is the peer build and requires vis-data loaded separately, otherwise new vis.DataSet(...) throws vis is not defined.) |
| Tone.js / Wavesurfer | Audio synthesis, waveform visualisation | <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/15.0.4/Tone.js"></script> |
Anything else on those three CDNs is fair game — apexcharts, d3-force, konva, flatpickr, etc. Pick whatever fits the topic.
Two patterns to follow when using a CDN library:
maintainAspectRatio: false makes Chart.js use the container's height. If the canvas has no intrinsic height (e.g. inside a flex column without a height set), it collapses to zero and nothing draws:
<div style="position: relative; height: 260px;">
<canvas id="chart"></canvas>
</div>
<script>
new Chart(document.getElementById('chart').getContext('2d'), {
type: 'bar',
data: { /* … */ },
options: { responsive: true, maintainAspectRatio: false, /* … */ }
});
</script>
Put external <script src="…"> tags before the inline <script> that
uses them. They execute in order, so a consumer that runs before its
library is loaded will fail with Chart is not defined.
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<script>/* uses Chart and d3 */</script>
© Classic298, BSD-3-Clause. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 2 other files in inline-visualizer-v2 of Classic298/open-webui-plugins.
Open the folder on GitHubat commit d4a78a6
Visualize next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Visualize this skillClassic298/open-webui-plugins | 577 | — | ~10k | Automated safety check: Pass | BSD-3-Clause | |
| Openai Dotcom Vizyzlnew/infra-skills | 149 | — | ~1.3k | Automated safety check: Pass | None | |
| Microsim Generatorynulihao/AgentSkillOS | 617 | — | ~2.1k | Automated safety check: Pass | None | |
| Microsim Generatordmccreary/ibook-skills | 105 | — | ~11k | Automated safety check: Pass | None | |
| Archify Diagramstt-a1i/archify | 79k | — | ~2.9k | Automated safety check: Pass | MIT | |
| JSON Canvasheyitsnoah/claudesidian | 2.6k | 18 repos | ~3.5k | Automated safety check: Pass | MIT |
yzlnew/infra-skills
Build figures in OpenAI's blog / research / system-card "dotcom" visual style — both (a) bar charts (monochrome bars with a darker same-hue stroke, rounded corners, a black y-axis with outward ticks…
ynulihao/AgentSkillOS
Creates interactive educational MicroSims using the best-matched JavaScript library (p5.js, Chart.js, Plotly, Mermaid, vis-network, vis-timeline, Leaflet, Venn.js).
dmccreary/ibook-skills
Creates interactive educational MicroSims, routing to the best-matched generator - p5.js, Chart.js, Plotly, Mermaid, vis-network, timelines, maps, Venn, causal-loop/feedback-loop diagrams (CLD)…
tt-a1i/archify
Creates interactive architecture, workflow, sequence, data-flow and lifecycle diagrams as standalone HTML with inline SVG, themes and image or video export.
heyitsnoah/claudesidian
Create and edit JSON Canvas files (.canvas) with nodes, edges, groups, and connections.
cathrynlavery/diagram-design
Creates branded diagrams, from architecture, flowchart and sequence to charts and maps, as self-contained HTML with inline SVG, with import from draw.io, Mermaid and Excalidraw.
Works with
Categories
Render rich interactive visuals — SVG diagrams, HTML widgets, Chart.js charts, and interactive explainers — directly inline in chat using visualize(). Visualize is an agent skill from Classic298/open-webui-plugins.js charts, and interactive explainers — directly inline in chat using visualize().
Visualize fits situations like: explicitly asks for a visualization; similar visual artifact; ordinary markdown; answer formatting.
Run `npx skills add Classic298/open-webui-plugins --skill visualize -a claude-code`. Or copy the skill folder (inline-visualizer-v2 in Classic298/open-webui-plugins) into .claude/skills/visualize in your project. Claude Code loads it when a task matches its description.
Run `npx skills add Classic298/open-webui-plugins --skill visualize -a codex`. Or copy the skill folder (inline-visualizer-v2 in Classic298/open-webui-plugins) into .agents/skills/visualize in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add Classic298/open-webui-plugins --skill visualize -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/visualize, .gemini/skills/visualize, .github/skills/visualize and .opencode/skills/visualize in your project.
Going by SKILL.md and its folder, Visualize needs Python for the scripts in its folder. Our summary lists: Python 3.
SKILL.md names 3 domains. As links in the text: cdnjs.cloudflare.com, cdn.jsdelivr.net and github.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Visualize is published under the BSD-3-Clause licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 10k tokens (SKILL.md is roughly 42k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Visualize: Openai Dotcom Viz (yzlnew/infra-skills, 149 stars), Microsim Generator (ynulihao/AgentSkillOS, 617 stars), Microsim Generator (dmccreary/ibook-skills, 105 stars) and Archify Diagrams (tt-a1i/archify, 79k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
Classic298 (a GitHub user) maintains it in Classic298/open-webui-plugins, which has 577 GitHub stars. The repository was last updated on October 5, 2026.
Source: Classic298/open-webui-plugins on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.