build-html-dashboard
GitHub用于手动编写自包含 HTML 数据仪表板,规定使用 Apache ECharts、暗色主题及内联资源等规范,强调增量写入与内存控制以规避失败。
触发场景
安装
npx skills add mindsdb/anton --skill build-html-dashboard -g -y
SKILL.md
Frontmatter
{
"name": "build-html-dashboard",
"metadata": {
"provenance": "builtin",
"display_name": "HTML dashboard & visualization output format"
},
"description": "ONLY for writing dashboard or chart HTML BY HAND. NOT needed on the normal path — create_artifact(type=\"html-app\") followed by generate_artifact writes the dashboard itself and carries this contract internally. NOT needed to change the content of a report or add sections (report_tools does that; see DIRECT REPORTS). Recall this before rewriting the code of a generated dashboard, or when generate_artifact failed and the user asked you to continue manually: it is the output contract — self-contained file rules, Apache ECharts setup, dark theme, layout\/design standards, large-dataset handling. Not needed to repair a page generate_artifact returned as browser_check_failed: that result's instruction says how."
}
LIST THE INSIGHTS (terse — one line each, not an essay):
Before coding, list the insights you want to present/convey/highlight as 1 - <chart/infographic/etc>: <insight it conveys and why it matters>..
Example: 1 - Line chart of weekly signups: shows growth inflection after the March launch, flags whether momentum is sustained.
This is a checklist, not a brief — no narrative prose, no design discussion.
BUILD THE DASHBOARD — use multiple scratchpad cells, but produce ONE single self-contained HTML file:
Before the first write, call create_artifact(type="html-app", name=..., description=..., primary="index.html") and use the returned <artifact_path> for every file you write. All paths below referring to "the output directory" mean <artifact_path>. The final dashboard MUST be a single .html file with all data, CSS, JS and images inlined, with no exception: the web app preview shows only the page itself, so a separate file next to it (dashboard_data.js, logo.png) does not load there, whatever its size. Build every page this way so it renders on every deployment. Keep the whole page under 8 MB; a larger page cannot be checked in a browser.
REROUND DISCIPLINE (critical — most "round-cap exhaustion" failures we've seen on real dashboards come from drifting off one or more of these):
- ONE scratchpad, ONE name. Pick a name on the first cell (e.g.
dash) and reuse it for the entire build. Switching names (build_pres→write_html→pres1…) creates separate isolated environments — variables in one don't exist in another — and burns rounds on recovery. - WRITE TO DISK INCREMENTALLY. Open the output
.htmlonce in 'w' mode, thenopen(path, 'a')to append head → body skeleton → each chart section → nav/JS → closing tags. Each cell appends a small chunk you can sanity-check. Do NOT build a single 20KB+ HTML string in memory and write it at the end. - CAP STRING SIZE PER CELL at ~5KB. Large-string scratchpad calls are the single biggest cause of silent failures (the tool occasionally drops the
codepayload on oversized inputs and the cell comes back with an empty-code error, which still counts against the round cap). If a section is too big, split it. - NEVER re-emit the full HTML mid-build. Append deltas, don't re-print the world. Assembly is a one-line concat at the end, not a re-render of everything you've written so far.
- KEEP READS SMALL. To verify what landed,
os.path.getsize(path)or theread_text_filetool withstart_line=-20— never read a multi-KB HTML whole.
HOST CONTRACT (critical — the checks generate_artifact runs on every page it produces: 1–7 fail the step (<html lang> in 1 is not checked), 8–9 are warnings, 10 runs in a headless browser. A hand-built page gets the browser check too: after every scratchpad cell that changes an artifact's .html file, the host loads the page in a browser where the deployment can and adds what it found under [artifact lint] in the cell's result, with a screenshot of the page when the check sent one. When it could not check the page, that line says the page was not checked in a browser and why: then tell the user the page is unverified in a browser. Items 1–9 are not checked for a hand-built page, so verify them yourself before you finish):
- A complete HTML document with an explicit
<body>...</body>, and<html lang>set to the language the user writes in, unless they ask for another language for the page (UI text follows it too). <meta name="viewport" content="width=device-width, initial-scale=1.0">in<head>.- No absolute URL in any
fetch()call. A map loads its atlas,https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json, withd3.json(...)(item 9), not withfetch(). No root-relative path either, insrc,href,fetch()ornew EventSource(): a value that starts with a single/(/logo.png,/api/export) breaks in the app preview and once published, because the page is served under a path prefix that a path from the root drops. Use a relative path for a link to another page (details.html); in a fullstack app, call the backend through theapi()helper that build-fullstack-backend defines.https://,//,data:,blob:,#andmailto:values are fine, sohref/srclinks to external sources stay allowed. Moving the same root-relative path into a JS string does not fix it. - Every opened
<script>block is closed with</script>— an unclosed one silently disables all JS. - The global name
window.__antonCommentsLayeris never used; the host app reserves it for its comment layer. - No universal
* { ... !important }rule (a reduced-motion media query is the one accepted exception). - Every
z-indexis 1000 or below — the host app's own overlays sit above the page. - Significant block containers (
div,section,table,main,article) carry stableidattributes — the host app attaches comments to them. - The web app preview runs the page in a sandboxed frame under a content security policy that blocks what it does not allow. Build to that policy so the page renders on every deployment. Scripts load only from
https://cdn.jsdelivr.net/npm/<package>@<exact version>/...: pin an exact version, since a floating@4,@5or@latestcannot be checked. Styles are inline only (<style>orstyle=): no external stylesheet, so no Leaflet CSS and no web fonts. No external images or map tiles: embed an image as adata:URI. Noeval/new Functionand no web workers. The default external resources are the two CDN scripts named under Visual design (Tailwind, ECharts); another library only when the user asked for it, from jsDelivr at an exact version. For a map or a globe, use a tested recipe: d3@7.9.0 with topojson-client@3.1.0 and world-atlas@2.0.2, loadingcountries-110m.jsonwithd3.json(item 3) and turning it into GeoJSON withtopojson.feature(world, world.objects.countries). Draw that GeoJSON with d3 into an<svg>; or with ECharts 5.5.1 throughecharts.registerMap(name, geoJson)and amapseries; or on a globe with globe.gl@2.34.2 withpolygonsData. Also tested: three@0.169.0 as an ES module, 3d-force-graph@1.73.4, gsap@3.12.5. Leaflet, raster tiles, MapLibre (it needs a worker) and echarts-gl (it needseval) break that policy, so they do not render in the web app preview. - Loaded the way the app preview loads it, at a desktop and a phone width, the page logs no console error or uncaught exception, breaks no rule of the preview's content security policy, asks for no separate file, leaves no chart, map or canvas blank, leaves no loading message showing over a view that has drawn, is no wider than the screen, and renders visible content without user interaction. Start every view through this one helper: each chart, map, globe or other view that runs setup code, on load or on its tab's first visit. A view whose setup throws or whose promise rejects then fails alone, with a plain-text message in its place, and every other view still draws:
function runView(name, el, setup) { const fail = (err) => { const why = err && err.message ? err.message : String(err); if (el) { el.setAttribute('data-view-failed', name); el.textContent = name + ' could not load.'; } console.error(name + ' failed: ' + why); if (typeof reportError === 'function') reportError(new Error(name + ': ' + why)); }; try { const done = setup(el); if (done && typeof done.then === 'function') done.catch(fail); } catch (err) { fail(err); } }. Call it with the view's title as the page shows it, its element and its setup function, which may beasync:runView('Shipment map', document.getElementById('world-map'), drawMap);. Await or return every promise the setup starts, such as the atlas'sd3.json(...). Don't end it with a.catchof your own: the helper must see the failure to name and mark the view. Keep the helper as written: itsconsole.errorandreportErrorname the failed view to the check and to the preview's error list, anddata-view-failedlets the check find the failed view whatever its message says. A view's owntry/catchthat only writes a message hides the error from the check. Log withconsole.erroronly a view that failed to draw. An expected failure stays silent or usesconsole.warn: the storage helper in item 11 catches a throw on every load in the preview frame, and aconsole.errorthere fails the check. Not checked by the pipeline, but the page breaks inside the frame without them: - BROWSER STORAGE (critical):
localStorage/sessionStorage/indexedDB/document.cookiemay be unavailable — the in-app preview runs the page in a sandboxed frame. Never touch them directly during top-level initialisation: a throw there aborts the script before any listener is bound, and the page renders with every control dead. Go through one guarded helper with an in-memory fallback. - LINKS: the page runs inside a frame, in the app preview and on the published link alike.
target="_top"andtarget="_parent"do not work; give a link to another artifact or an external sitetarget="_blank", or it opens inside the frame.
SECURITY (critical): Dashboards may be published to the web. NEVER embed API keys, tokens, passwords, connection strings, or any credentials in the HTML, JS, or inline data. Fetch data in scratchpad cells using credentials from environment variables, then serialize only the resulting data into the dashboard. If the user explicitly asks to embed a credential (e.g. for a live-updating dashboard), warn them that publishing will expose it and get confirmation before proceeding.
Build the parts in separate cells, then assemble at the end:
CELL 1 — Serialize data to a JS string variable (programmatic, no HTML):
Serialize all computed data (dataframes, metrics, KPIs) into a Python string. Build a Python dict with keys like "kpis", "tables", "charts" — each containing the relevant data. Convert DataFrames with df.to_dict(orient='records'). Use json.dumps(data, default=str) to handle dates, Decimal, numpy types. Store as a Python variable: data_js = 'const D = ' + json_string + ';' — do NOT write to a separate file.
CELL 2 — Build CSS + HTML structure as a Python string variable:
Write the HTML head (styles, CDN script tags) and body structure (header, KPIs, chart divs, tabs, tables) as a Python string variable html_body. This cell builds the template.
CELL 3+ — Build JS chart rendering logic as Python string variables:
Write the JavaScript that initializes charts, populates tables, handles tabs, etc. Define the runView helper from item 10 first, and start every view through it, views behind tabs included. Split across multiple cells if needed to avoid token limits. Store as js_charts etc.
FINAL CELL — Assemble and write the HTML file:
Combine: html = html_body.replace('</body>', f'<script>{data_js}{js_charts}</script></body>') or similar.
SELF-CONTAINED OUTPUT (critical):
Inline everything, however large: CSS in <style>, JS in <script>, data as JS variables, images as data: URIs. A big dataset goes into the page as a const D = ...; script, not into a dashboard_data.js beside it, and an image into an <img src="data:...">, not a screenshot.png beside it. The web app preview loads only the page itself, so a separate file does not show there. If the data makes the page too heavy, aggregate it in the cell first instead of splitting it out.
UPLOADED IMAGES AND BINARY ASSETS:
Files the user attached or pasted appear with ABSOLUTE paths in the conversation
context (.cowork/files/<uuid>/<name> in the app, .anton/uploads/clipboard_*.png
for a CLI paste). Use the path you were given — never guess one or scan directories.
If the user refers to a file whose path you cannot see, ask them to attach it
to the conversation — never ask for a filesystem path.
Never point the HTML at that original path: it sits outside <artifact_path>, and
the web app preview loads only the page itself. Embed the image instead, as a data: URI,
in one scratchpad cell:
import base64, mimetypes
from pathlib import Path
mime = mimetypes.guess_type(src)[0] or "application/octet-stream"
b64 = base64.b64encode(Path(src).read_bytes()).decode()
img_src = f"data:{mime};base64,{b64}"
A photo of several MB makes the page heavy: shrink it first (Pillow, a smaller width, JPEG) when the page would pass a few MB. For uploaded DATA files (CSV/JSON) do not use a data: URI — read them in CELL 1 and inline the result as a JS variable like any other dataset.
WHY: (1) The web app preview loads only the page itself: a separate file does not reach it there. (2) Splitting the build across cells catches JS/CSS errors early: if a cell has a syntax issue in a string, you'll see it before the final assembly. (3) Large datasets in single cells timeout. (4) Self-contained files can be published to the web via /publish without missing assets.
PYTHON → JS STRING SAFETY (critical): When building JS code inside Python strings, escape sequences get resolved by Python BEFORE writing to the file. This means '\n' in Python becomes a literal newline in the output, which breaks JavaScript string literals. Rules:
- Use '\n' in Python if you need a literal \n in the JS output
- Use raw strings (r"...") for JS code blocks when possible
- NEVER use '\n', '\t', or '"' inside JS strings within Python — double-escape them
- After writing the file, sanity-check that no string literals span multiple lines
Output format:
- Unless the user explicitly asks for a different format, always output visualizations as polished, single-file HTML pages — never raw PNGs or bare image files.
Visual design:
- Make it look good by default. Use a dark theme (#0d1117 background, #e6edf3 text), clean typography (system sans-serif stack), generous padding, and responsive layout.
- STYLING: Tailwind CSS is the recommended way to style the page. Load it once in
<head>via CDN:<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>and use utility classes for layout, spacing, typography and colour (bg-[#0d1117] text-[#e6edf3]for the theme). Hand-written CSS in a<style>block stays allowed wherever utilities fall short — keyframes, chart containers, complex selectors — or when the page is too small to need a framework. No other CSS or JS library unless the user asked for one (host contract item 9): the two CDN scripts named in these rules (Tailwind, ECharts), at exactly the versions shown, are the default external resources. Both are fetched every time the page opens, so a page that must work OFFLINE or without external resources (the user says so) skips Tailwind and styles itself with hand-written CSS. - ALWAYS use Apache ECharts for interactive charts. Load it via CDN:
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>. No Python dependencies needed — just write the HTML with inline JS. Use ECharts' built-in dark theme:echarts.init(dom, 'dark'), then customize colors to match #0d1117 background. - NEVER use Plotly, matplotlib, or other charting libraries unless the user explicitly asks.
Chart type by purpose (the same rule generate_artifact gives its generators):
- Line for change over time; bar for comparing categories (horizontal bars when labels are long); stacked bar for composition across categories; scatter for the relation between two measures.
- Pie or donut ONLY for a simple part-of-a-whole with few slices — never for comparison or change over time.
- A value with no dimension is a KPI card, not a chart. Titles, legends and axis labels name what the data IS. No chart junk: 3D, gradients, decorative markers.
Axes and units (critical — a number without a unit is a guess for the reader):
- Every axis label names the measure AND its unit (
Revenue, USD,Latency, ms,Share, %). - Every number in a tooltip, axis or KPI goes through a
formatter: thousands separators, fixed decimals, currency or percent sign, unit suffix — never a raw float. - One date format per page, consistent between axes and tooltips. Say when a value is estimated, sampled or converted, and from which unit.
Line smoothing (critical — smooth: true misrepresents volatile data):
- DEFAULT:
smooth: falseon ALL line series. Straight segments between data points are the honest representation — they show actual volatility, drawdowns, and inflection points. - EXCEPTION: Use
smooth: trueONLY for cumulative/monotonic series (cumulative returns, running totals, growth curves) where the trend matters more than point-to-point moves. - Decision heuristic: Does the line ever reverse direction meaningfully? If yes → smooth: false. Is it a running sum, cumulative metric, or long-horizon trend? → smooth: true is acceptable.
- Line widths: 2.5 for hero/primary lines, 1.5 for multi-line comparisons, 1 for secondary/reference lines.
Chart readability (critical — labels must NEVER overlap):
- Use
axisLabel: { rotate: -45 }or{ rotate: 45 }on crowded axes. Setgrid: { containLabel: true }so labels never clip. Uselegend: { type: 'scroll', bottom: 0 }to place scrollable legends below the chart. For pie/donut charts uselabel: { show: true, position: 'outside' }withlabelLayout: { hideOverlap: true }. For bar charts with many categories, use horizontal bars (yAxisas category) or abbreviate labels withaxisLabel: { formatter }. Always configure richtooltipwithformatterfunctions for precise value display on hover. UsedataZoomfor time series so users can zoom into ranges.
Multi-tab / multi-view dashboards (critical — charts fail silently on hidden containers):
- ECharts, Chart.js, and Plotly all render nothing when called on a container with
display: noneor 0×0 dimensions — no error, no warning, just a blank chart. NEVER callecharts.init()insideDOMContentLoadedfor tabs/pages that start hidden. - Initialize charts lazily, gated on first visibility: in the tab-click handler, check a
Setof already-rendered tabs and call the page's init function only on first visit. Example pattern:const _rendered = new Set(['overview']); function showPage(name) { /* toggle classes */ if (!_rendered.has(name)) { _rendered.add(name); initChartsFor(name); } }— only the default-visible page initializes on load.initChartsFor(name)starts each view on that tab throughrunView(item 10), so a view that fails on a tab fails alone.
Layout and composition:
- For non-chart visualizations (tables, reports, dashboards), write clean HTML/CSS directly. Use CSS grid or flexbox. Add subtle styling: rounded corners, soft shadows, hover effects.
- When showing multiple related visuals, combine them into a single page with sections, not separate files. Ensure each chart has enough height (min 400px) and breathing room between them so nothing feels cramped.
- Hero KPI cards at the top (large numbers, color-coded positive/negative, with delta arrows).
- Main narrative chart immediately below the KPIs — this is the chart that tells the story.
- Supporting charts below, each with a clear subtitle explaining what it reveals.
- Annotations on charts: use ECharts
markLinefor thresholds,markPointfor outliers, andmarkAreafor highlighted regions. A chart without annotations is a missed opportunity. - The goal: every visualization should look like a polished product page, not a homework assignment. Think dark-mode dashboard, not Jupyter default.
Responsive layout (critical — dashboards must work on phones too):
- ALWAYS include
<meta name="viewport" content="width=device-width, initial-scale=1.0">in<head>. Without this, mobile browsers render at desktop width and the user pinch-zooms. - Multi-card sections use
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr))(or 300px on dense layouts). This lets the browser reflow to single-column on narrow viewports without a media query — cards stack vertically instead of getting squashed into unreadable columns. - Chart containers use
width: 100%andheight: min(420px, 60vh)(NOT fixed pixel widths). For each ECharts instance, register a window resize hook so it refits:window.addEventListener('resize', () => myChart.resize());— without this, rotating a phone or resizing the window leaves charts the wrong size. - Tables wrap in
<div style="overflow-x: auto;">so they scroll horizontally on narrow screens rather than overflowing the page. Do NOT set fixed table widths. - Default to one column on narrow viewports unless the user explicitly asks for a fixed multi-column layout (e.g. for a printable PDF).
版本历史
-
161d2dd
当前 2026-11-03 00:54
新增对仅浏览器检查失败页面的修复流程支持,并优化了视图隔离与错误处理机制。
-
cf92364
2026-10-19 07:51
默认文件名从 dashboard.html 改为 index.html
-
18b043f
2026-09-03 08:35
从系统提示词迁移为内置Skill,优化召回机制与版本管理,增强安全性与稳定性。
- 0f2b69b 2026-08-20 00:13


