Tufte Report — Data-Driven Infographic Skill Create standalone HTML reports that combine editorial narrative with interactive data visualization in Edward Tufte's style: high information density, minimal chart junk, typography-first design. Design Philosophy Tufte's core principles drive every decision: - Data-ink ratio : every pixel of ink should represent data, not decoration - Small multiples : repeat a design to show comparison, not animation - Sparklines : word-sized graphics that live inside prose - Layering : overview first, then detail on demand - Integration : text and graphics share…

` prefix.\nWrap in `.toc-layout` grid alongside the overview: `grid-template-columns:1fr 180px`.\n\n## 9. Section Header (with back-to-top)\n\n```html\n\u003ch2 id=\"section-id\">section title \u003ca href=\"#toc\" class=\"back-to-top\" title=\"Back to contents\">+\u003c/a>\u003c/h2>\n```\n\nThe arrow is near-invisible (uses `--rule` color), darkens on hover, floats up 2px.\n\n## 10. Inline Sparkline\n\n```html\n\u003csvg class=\"spark-inline\" id=\"sparkId\" width=\"50\" height=\"14\">\u003c/svg>\n```\n\nRendered via JS `drawSparkline(id, valuesArray, color)` function. Creates a filled area + line + end dot.\nUse inside table cells or inline with text.\n\n## 11. Strip Chart (horizontal bar rows)\n\n```html\n\u003cdiv class=\"telegram-strip\" id=\"stripId\">\n \u003c!-- rows generated by JS -->\n\u003c/div>\n```\n\nEach row: `.tg-week` (label) + `.tg-count` (number) + `.tg-bar-area` (proportional bar) + `.tg-note` (annotation).\nGood for weekly/periodic data with sparse annotations.\n\n## 12. Scroll Reveal\n\nApplied via JS IntersectionObserver. Add `.reveal` class to any element:\n\n```javascript\ndocument.querySelectorAll('.chart-container, .flyout, .aside-container')\n .forEach(el => el.classList.add('reveal'));\nconst observer = new IntersectionObserver((entries) => {\n entries.forEach(entry => {\n if (entry.isIntersecting) {\n entry.target.classList.add('visible');\n observer.unobserve(entry.target);\n }\n });\n}, { threshold: 0.15 });\ndocument.querySelectorAll('.reveal').forEach(el => observer.observe(el));\n```\n\nRespects `prefers-reduced-motion`.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5257,"content_sha256":"7b363508f2cef55a92c4909bb2464e2c93bd92aab169f2c166af06f3a8f25a20"},{"filename":"references/data-adapter.md","content":"# Universal Data Adapter\n\nNormalize any data source into a standard intermediate format before building the report.\n\n## Intermediate Schema: ReportData\n\nEvery report starts from this JSON structure. Claude transforms user-provided data (CSV, JSON, SQLite query results, API responses, raw numbers) into this format before generating HTML.\n\n```json\n{\n \"meta\": {\n \"title\": \"Q1 Health Report\",\n \"subtitle\": \"Jan–Mar 2026\",\n \"question\": \"Is my sleep quality improving?\",\n \"generated\": \"2026-04-22T15:00:00Z\",\n \"sources\": [\"Apple Health\", \"Oura Ring API\"]\n },\n \"kpis\": [\n {\n \"id\": \"hrv\",\n \"label\": \"Avg HRV\",\n \"value\": 42.3,\n \"unit\": \"ms\",\n \"trend\": -0.13,\n \"status\": \"red\",\n \"sparkline\": [38, 41, 44, 39, 42, 45, 40, 43],\n \"context\": \"below 50ms baseline\"\n }\n ],\n \"sections\": [\n {\n \"id\": \"sleep\",\n \"title\": \"Sleep Architecture\",\n \"state_line\": \"Deep sleep down **18%**, REM stable at 22%\",\n \"blocks\": [\n {\n \"type\": \"trend-chart\",\n \"data\": {\n \"labels\": [\"Jan\", \"Feb\", \"Mar\"],\n \"datasets\": [\n {\"label\": \"Deep Sleep\", \"values\": [1.2, 1.0, 0.98], \"color\": \"primary\"},\n {\"label\": \"REM\", \"values\": [1.5, 1.4, 1.5], \"color\": \"secondary\"}\n ]\n },\n \"caption\": \"Hours per night, 7-day rolling average\"\n },\n {\n \"type\": \"narrative\",\n \"content\": \"**Deep sleep** declined steadily after the caffeine experiment in Feb. **REM** held steady, suggesting the issue is slow-wave, not total sleep.\"\n },\n {\n \"type\": \"correlation-matrix\",\n \"data\": {\n \"variables\": [\"Screen time\", \"Caffeine\", \"Exercise\", \"Deep sleep\"],\n \"matrix\": [\n [1.0, 0.15, -0.22, -0.45],\n [0.15, 1.0, -0.08, -0.38],\n [-0.22, -0.08, 1.0, 0.52],\n [-0.45, -0.38, 0.52, 1.0]\n ]\n },\n \"caption\": \"Pearson correlations, 90-day window\"\n }\n ]\n }\n ]\n}\n```\n\n## Field Reference\n\n### meta\n| Field | Type | Required | Notes |\n|-------|------|----------|-------|\n| title | string | yes | Becomes h1 |\n| subtitle | string | no | Date range or scope |\n| question | string | yes | The one question the report answers — drives design |\n| generated | ISO datetime | yes | Auto-set at generation time |\n| sources | string[] | yes | Shown in footer tags |\n\n### kpis[]\n| Field | Type | Required | Notes |\n|-------|------|----------|-------|\n| id | string | yes | CSS id and anchor |\n| label | string | yes | Human name |\n| value | number | yes | Current value, displayed in Monaspace Argon |\n| unit | string | no | \"ms\", \"%\", \"hrs\", etc. |\n| trend | number | no | Fractional change (-0.13 = -13%). Sign determines arrow |\n| status | \"red\" / \"amber\" / \"green\" | no | Maps to status-strip color |\n| sparkline | number[] | no | Last 7-14 data points for inline sparkline |\n| context | string | no | One-line note below the number |\n\n### sections[].blocks[]\n\nEach block has a `type` and a `data` shape. See `references/blocks.md` for the full block catalog.\n\n## Adapter Instructions\n\nWhen the user provides raw data, follow this process:\n\n1. **Identify the source type**: CSV → parse headers as labels; JSON → map keys; SQLite → run query, use column names; API → extract from response body; raw numbers → ask for labels\n2. **Ask the user** for the primary question (becomes `meta.question`) and desired sections\n3. **Normalize numbers**: strip currency symbols, convert percentages to decimals for `trend`, keep display values as-is for `value`\n4. **Compute derived fields**: sparklines from time-series slices, trends from first/last comparison, status from user-defined thresholds (ask if not provided)\n5. **Emit the ReportData JSON** and confirm with the user before generating HTML\n\n### Example: CSV → ReportData\n\nInput CSV:\n```csv\ndate,hrv_ms,deep_sleep_hrs,steps\n2026-01-01,45,1.3,8200\n2026-01-02,42,1.1,7800\n...\n```\n\nTransformation:\n- Each numeric column becomes a potential KPI (latest value, trend from first→last)\n- Time-series columns become sparkline arrays\n- Group related columns into sections\n- Compute correlations between columns for correlation-matrix blocks\n\n### Example: Raw numbers → ReportData\n\nUser says: \"HRV is 42ms (was 48), sleep score 72 (was 81), steps 6200 (was 9100)\"\n\nTransformation:\n- Three KPIs with computed trends: -12.5%, -11.1%, -31.9%\n- Status inferred: all declining → amber/red\n- Single section with a narrative summary block\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4582,"content_sha256":"17527ce82e5aaa9dd5e7c7bf732c4cc7e3cc6eeb6582272769464c220037ff93"},{"filename":"references/design-tokens.md","content":"# Design Tokens\n\n## CSS Variables\n\n```css\n:root {\n --ink: #1a1a1a; /* Primary text */\n --ink-light: #555; /* Secondary text, aside narratives */\n --ink-muted: #888; /* Tertiary text, captions, labels */\n --bg: #fffff8; /* Background (warm white, not pure white) */\n --bg-aside: #f9f6ee; /* Flyout/callout background */\n --accent: #a00; /* Accent markers (aside-marker, flyout diamond) */\n --rule: #ccc; /* Borders, rules, separator color */\n\n /* Semantic chart colors */\n --spark-primary: #c45a28; /* Primary data stream (orange) */\n --spark-secondary: #2a7a5a; /* Secondary/growth (green) */\n --spark-tertiary: #5a5aaa; /* Social/communication (purple) */\n\n /* Status colors */\n --status-red: #a02a2a;\n --status-amber: #c89000;\n --status-green: #2a7a3a;\n --status-blue: rgba(42,80,140,0.7);\n}\n```\n\n## Typography Scale\n\n| Element | Font | Size | Weight | Style |\n|---------|------|------|--------|-------|\n| h1 | EB Garamond | 2.2rem | 400 | small-caps |\n| h2 | EB Garamond | 1.5rem | 400 | small-caps |\n| h3 | EB Garamond | 1.15rem | 400 | small-caps, --ink-light |\n| body | EB Garamond | 18px / 1.6 | 400 | normal |\n| state-line | EB Garamond | 1.5rem / 1.45 | 400 | italic, --ink-light |\n| overview lede | EB Garamond | 1.25rem / 1.6 | 400 | drop cap first letter |\n| aside | EB Garamond | 0.85rem / 1.5 | 400 | italic, --ink-light |\n| caption | EB Garamond | 0.82rem | 400 | italic, --ink-muted, centered |\n| table header | EB Garamond | 0.92rem | 400 | small-caps, --ink-muted |\n| table numbers | Monaspace Argon | 0.85rem | 400 | tabular-nums |\n| big number | Monaspace Argon | 2.6rem | 400 | letter-spacing: -0.02em |\n| status value | Monaspace Argon | 1.5rem | 400 | tabular-nums |\n| source tags | Monaspace Argon | 0.65rem | 400 | --rule color |\n| ornament | Monaspace Argon | 0.9rem | 400 | ligatures enabled |\n\n## Font Loading\n\n```html\n\u003clink href=\"https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap\" rel=\"stylesheet\">\n\u003cstyle>\n @font-face {\n font-family: 'Monaspace Argon';\n src: url('https://cdn.jsdelivr.net/gh/githubnext/[email protected]/fonts/webfonts/MonaspaceArgon-Regular.woff2') format('woff2');\n font-weight: 400; font-display: swap;\n }\n @font-face {\n font-family: 'Monaspace Argon';\n src: url('https://cdn.jsdelivr.net/gh/githubnext/[email protected]/fonts/webfonts/MonaspaceArgon-Bold.woff2') format('woff2');\n font-weight: 700; font-display: swap;\n }\n\u003c/style>\n```\n\n## Layout Grid\n\n- Max width: 1200px, centered\n- Padding: 2rem 1.5rem 4rem\n- Aside-container: `1fr 280px` with 2rem gap\n- TOC layout: `1fr 180px` with 2rem gap\n- Summary cards: `repeat(3, 1fr)` with 1.5rem gap\n- Status strip: `repeat(4, 1fr)` with no gap\n- Mobile breakpoint: 800px (collapses all grids to single column)\n\n## Spacing\n\n- Between sections (ornament): 2rem\n- Chart container margin: 2rem 0\n- Chart side padding: 5% left/right\n- Table margin: 1.5rem 0\n- State-line margin: 1.5rem 0 2rem\n- Flyout margin: 1.5rem 0\n\n## Transitions\n\n- Hover on cards/flyouts: `border-color 0.3s ease, box-shadow 0.3s ease`\n- Table row hover: `background 0.2s ease`\n- Back-to-top arrow: `color 0.3s ease, transform 0.3s ease` (translateY -2px)\n- Scroll reveal: `opacity 0.6s cubic-bezier(0.25,0.1,0.25,1), transform 0.6s` (translateY 16px)\n- Reduced motion: all transitions disabled via `prefers-reduced-motion`\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3444,"content_sha256":"47901184d8d2714bbfefeaf56e7a67b6e016e3c01ac0427b5fad534684e46235"},{"filename":"references/preview-server.md","content":"# Preview Server\n\nZero-dependency Python script for live-reloading Tufte reports during development.\n\n## Usage\n\n```bash\npython3 ~/.claude/skills/tufte-report/scripts/serve.py report.html\n# → Serving report.html on http://localhost:8042\n# → Watching for changes...\n```\n\nOpens automatically in default browser. Reloads when the HTML file changes.\n\n## How It Works\n\n1. Injects a tiny WebSocket client `\u003cscript>` before `\u003c/body>` in the served HTML\n2. Watches the file's mtime every 500ms\n3. Sends `reload` message over WebSocket when the file changes\n4. Browser refreshes without full page navigation (preserves scroll position by default)\n\n## The Script\n\nClaude should create this script at `~/.claude/skills/tufte-report/scripts/serve.py` if it doesn't exist:\n\n```python\n#!/usr/bin/env python3\n\"\"\"Zero-dependency live-reload server for Tufte reports.\"\"\"\nimport http.server, hashlib, json, os, sys, threading, time, struct, webbrowser\nfrom pathlib import Path\n\nPORT = int(os.environ.get(\"TUFTE_PORT\", 8042))\nWS_PORT = PORT + 1\n\nINJECT = f'''\u003cscript>\n(function(){{\n var ws = new WebSocket(\"ws://localhost:{WS_PORT}\");\n ws.onmessage = function(e) {{\n if (e.data === \"reload\") {{\n var y = window.scrollY;\n sessionStorage.setItem(\"_tufte_scroll\", y);\n location.reload();\n }}\n }};\n ws.onclose = function() {{ setTimeout(function(){{ location.reload(); }}, 2000); }};\n window.addEventListener(\"load\", function() {{\n var y = sessionStorage.getItem(\"_tufte_scroll\");\n if (y) {{ window.scrollTo(0, parseInt(y)); sessionStorage.removeItem(\"_tufte_scroll\"); }}\n }});\n}})();\n\u003c/script>'''\n\nclass Handler(http.server.SimpleHTTPRequestHandler):\n def __init__(self, *a, html_path=None, **kw):\n self._html = html_path\n super().__init__(*a, **kw)\n\n def do_GET(self):\n if self.path in (\"/\", f\"/{self._html.name}\"):\n content = self._html.read_text()\n content = content.replace(\"\u003c/body>\", INJECT + \"\u003c/body>\")\n data = content.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", len(data))\n self.end_headers()\n self.wfile.write(data)\n else:\n super().do_GET()\n\n def log_message(self, fmt, *args): pass\n\ndef ws_server(html_path):\n \"\"\"Minimal WebSocket server — just enough for reload signals.\"\"\"\n import socket, hashlib, base64\n srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n srv.bind((\"localhost\", WS_PORT))\n srv.listen(1)\n clients = []\n\n def accept_loop():\n while True:\n conn, _ = srv.accept()\n data = conn.recv(4096).decode()\n key = \"\"\n for line in data.split(\"\\r\\n\"):\n if line.startswith(\"Sec-WebSocket-Key:\"):\n key = line.split(\": \", 1)[1].strip()\n accept = base64.b64encode(\n hashlib.sha1((key + \"258EAFA5-E914-47DA-95CA-5AB5DC11650A\").encode()).digest()\n ).decode()\n conn.send(\n f\"HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: {accept}\\r\\n\\r\\n\".encode()\n )\n clients.append(conn)\n\n threading.Thread(target=accept_loop, daemon=True).start()\n\n last_hash = hashlib.md5(html_path.read_bytes()).hexdigest()\n while True:\n time.sleep(0.5)\n try:\n cur = hashlib.md5(html_path.read_bytes()).hexdigest()\n except FileNotFoundError:\n continue\n if cur != last_hash:\n last_hash = cur\n frame = b\"\\x81\" + bytes([len(b\"reload\")]) + b\"reload\"\n dead = []\n for c in clients:\n try:\n c.send(frame)\n except Exception:\n dead.append(c)\n for c in dead:\n clients.remove(c)\n print(f\" ↻ reloaded ({time.strftime('%H:%M:%S')})\")\n\ndef main():\n if len(sys.argv) \u003c 2:\n print(\"Usage: serve.py \u003creport.html>\")\n sys.exit(1)\n html_path = Path(sys.argv[1]).resolve()\n if not html_path.exists():\n print(f\"File not found: {html_path}\")\n sys.exit(1)\n\n os.chdir(html_path.parent)\n\n handler = lambda *a, **kw: Handler(*a, html_path=html_path, **kw)\n server = http.server.HTTPServer((\"localhost\", PORT), handler)\n\n threading.Thread(target=ws_server, args=(html_path,), daemon=True).start()\n\n url = f\"http://localhost:{PORT}/\"\n print(f\" Serving {html_path.name} on {url}\")\n print(f\" Watching for changes... (Ctrl+C to stop)\")\n webbrowser.open(url)\n try:\n server.serve_forever()\n except KeyboardInterrupt:\n print(\"\\n Stopped.\")\n\nif __name__ == \"__main__\":\n main()\n```\n\n## Integration with Skill\n\nAfter generating a report, Claude should offer:\n\n> \"Want me to start the preview server? I'll watch for changes and auto-reload.\"\n\nThen run:\n```bash\npython3 ~/.claude/skills/tufte-report/scripts/serve.py /path/to/report.html\n```\n\nKeep it running in the background. Each time Claude updates the HTML file, the browser reloads automatically with scroll position preserved.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5272,"content_sha256":"4fd435590f8635923173dc8a7f88212cdef80feb2fcf9955c6357046aead2137"},{"filename":"scripts/serve.py","content":"#!/usr/bin/env python3\n\"\"\"Zero-dependency live-reload server for Tufte reports.\"\"\"\nimport http.server, hashlib, os, sys, threading, time, webbrowser, socket, base64\nfrom pathlib import Path\n\nPORT = int(os.environ.get(\"TUFTE_PORT\", 8042))\nWS_PORT = PORT + 1\n\nINJECT = f'''\u003cscript>\n(function(){{\n var ws=new WebSocket(\"ws://localhost:{WS_PORT}\");\n ws.onmessage=function(e){{if(e.data===\"reload\"){{var y=window.scrollY;sessionStorage.setItem(\"_ts\",y);location.reload()}}}};\n ws.onclose=function(){{setTimeout(function(){{location.reload()}},2000)}};\n window.addEventListener(\"load\",function(){{var y=sessionStorage.getItem(\"_ts\");if(y){{window.scrollTo(0,parseInt(y));sessionStorage.removeItem(\"_ts\")}}}});\n}})();\n\u003c/script>'''\n\n_html_path = None\n\nclass Handler(http.server.SimpleHTTPRequestHandler):\n def do_GET(self):\n if self.path in (\"/\", f\"/{_html_path.name}\"):\n content = _html_path.read_text()\n content = content.replace(\"\u003c/body>\", INJECT + \"\u003c/body>\")\n data = content.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", len(data))\n self.end_headers()\n self.wfile.write(data)\n else:\n super().do_GET()\n def log_message(self, fmt, *args): pass\n\ndef ws_server():\n srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n srv.bind((\"localhost\", WS_PORT))\n srv.listen(5)\n clients = []\n def accept_loop():\n while True:\n conn, _ = srv.accept()\n data = conn.recv(4096).decode()\n key = \"\"\n for line in data.split(\"\\r\\n\"):\n if line.startswith(\"Sec-WebSocket-Key:\"):\n key = line.split(\": \", 1)[1].strip()\n accept = base64.b64encode(\n hashlib.sha1((key + \"258EAFA5-E914-47DA-95CA-5AB5DC11650A\").encode()).digest()\n ).decode()\n conn.send(f\"HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: {accept}\\r\\n\\r\\n\".encode())\n clients.append(conn)\n threading.Thread(target=accept_loop, daemon=True).start()\n last = hashlib.md5(_html_path.read_bytes()).hexdigest()\n while True:\n time.sleep(0.5)\n try:\n cur = hashlib.md5(_html_path.read_bytes()).hexdigest()\n except FileNotFoundError:\n continue\n if cur != last:\n last = cur\n frame = b\"\\x81\" + bytes([len(b\"reload\")]) + b\"reload\"\n dead = []\n for c in clients:\n try: c.send(frame)\n except Exception: dead.append(c)\n for c in dead: clients.remove(c)\n print(f\" ↻ reloaded ({time.strftime('%H:%M:%S')})\")\n\ndef main():\n global _html_path\n if len(sys.argv) \u003c 2:\n print(\"Usage: serve.py \u003creport.html>\"); sys.exit(1)\n _html_path = Path(sys.argv[1]).resolve()\n if not _html_path.exists():\n print(f\"Not found: {_html_path}\"); sys.exit(1)\n os.chdir(_html_path.parent)\n threading.Thread(target=ws_server, daemon=True).start()\n server = http.server.HTTPServer((\"localhost\", PORT), Handler)\n url = f\"http://localhost:{PORT}/\"\n print(f\" Serving {_html_path.name} on {url}\")\n print(f\" Watching for changes... (Ctrl+C to stop)\")\n webbrowser.open(url)\n try: server.serve_forever()\n except KeyboardInterrupt: print(\"\\n Stopped.\")\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3569,"content_sha256":"1da98d60d709c6c6b8e65f8005de288e4b64e2edf4f140a73e1d734a947917a8"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Tufte Report — Data-Driven Infographic Skill","type":"text"}]},{"type":"paragraph","content":[{"text":"Create standalone HTML reports that combine editorial narrative with interactive data visualization in Edward Tufte's style: high information density, minimal chart junk, typography-first design.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Design Philosophy","type":"text"}]},{"type":"paragraph","content":[{"text":"Tufte's core principles drive every decision:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Data-ink ratio","type":"text","marks":[{"type":"strong"}]},{"text":": every pixel of ink should represent data, not decoration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Small multiples","type":"text","marks":[{"type":"strong"}]},{"text":": repeat a design to show comparison, not animation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sparklines","type":"text","marks":[{"type":"strong"}]},{"text":": word-sized graphics that live inside prose","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Layering","type":"text","marks":[{"type":"strong"}]},{"text":": overview first, then detail on demand","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Integration","type":"text","marks":[{"type":"strong"}]},{"text":": text and graphics share the same visual space (sidenotes, not footnotes)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The report should feel like a well-edited magazine feature — you read it top to bottom, narrative carries you through the data, and every chart earns its space by answering a specific question.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Onboarding — Ask Before Building","type":"text"}]},{"type":"paragraph","content":[{"text":"Before writing ANY code, ask these questions. Do not proceed until all are answered:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"What data sources do you have?","type":"text","marks":[{"type":"strong"}]},{"text":" (CSV, JSON, SQLite, API endpoint, or raw numbers)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"What is the primary question this report should answer?","type":"text","marks":[{"type":"strong"}]},{"text":" (one sentence — this becomes the title and drives all design decisions)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"How many sections do you need?","type":"text","marks":[{"type":"strong"}]},{"text":" (cap at 8 — push back if more are requested; each section should answer one sub-question)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"What's the output format?","type":"text","marks":[{"type":"strong"}]},{"text":" (standalone HTML file, or embedded component)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Time budget?","type":"text","marks":[{"type":"strong"}]},{"text":" Provide an estimate:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"1-2 sections with tables only: ~200 LOC, ~5 min bypass / ~10 min manual","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"3-4 sections with 2-3 charts: ~500 LOC, ~15 min bypass / ~25 min manual","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"5-8 sections with charts + health data + sparklines: ~1200 LOC, ~30 min bypass / ~50 min manual","type":"text"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Scope Protection","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill enforces hard limits to prevent scope creep:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Max 8 sections","type":"text","marks":[{"type":"strong"}]},{"text":" — if the user asks for more, suggest combining related topics","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Max 2 chart types per section","type":"text","marks":[{"type":"strong"}]},{"text":" — a section gets one primary chart and optionally one supporting chart or table. More than that means the section should split","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Max 3 colors per chart","type":"text","marks":[{"type":"strong"}]},{"text":" — beyond that, use small multiples instead of rainbow legends","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No 3D charts, no pie charts, no donut charts","type":"text","marks":[{"type":"strong"}]},{"text":" — these violate Tufte principles","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No gratuitous animation","type":"text","marks":[{"type":"strong"}]},{"text":" — scroll-reveal on enter is fine; spinning, bouncing, or pulsing is not","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Every chart must have a caption","type":"text","marks":[{"type":"strong"}]},{"text":" — if you can't write a one-sentence caption explaining what the chart shows, the chart shouldn't exist","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When the user asks for something outside these limits, respond with: \"That would take the report from [current LOC estimate] to [new estimate]. The extra complexity adds [X] but risks [Y]. Shall I proceed, or can we [simpler alternative]?\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Architecture","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"report.html (standalone, no build step)\n├── Google Fonts CDN (EB Garamond)\n├── jsDelivr CDN (Monaspace Argon woff2)\n├── jsDelivr CDN (Chart.js 4.x UMD)\n├── Inline \u003cstyle> (design system CSS)\n├── Inline HTML (semantic structure)\n└── Inline \u003cscript> (data + Chart.js configs + sparklines + scroll-reveal)","type":"text"}]},{"type":"paragraph","content":[{"text":"No build tools, no frameworks, no npm. One file, opens in any browser.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Design System","type":"text"}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"references/design-tokens.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the complete CSS variables, typography scale, and color palette.","type":"text"}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"references/components.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the HTML+CSS snippet of every reusable component.","type":"text"}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"references/charts.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for Chart.js configuration patterns and inline SVG sparkline code.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Report Structure Template","type":"text"}]},{"type":"paragraph","content":[{"text":"Every report follows this skeleton:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"1. Title + subtitle + data source tags (monospace, subtle)\n2. [Optional] Status dashboard (4-column KPI strip)\n3. Overview narrative with inline sparklines + TOC sidebar\n4. Summary cards (2-4 KPI tiles with sparklines)\n5. Sections (each: state-line → chart+narrative aside → table+narrative aside)\n6. [Optional] Decision register (threshold table with status colors)\n7. Footer (generation date, sources)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Section Pattern","type":"text"}]},{"type":"paragraph","content":[{"text":"Each section follows this rhythm:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"\u003ch2> with ↑ back-to-top link\n\u003cp class=\"state-line\"> — one italic sentence, the takeaway\n\u003cdiv class=\"aside-container\"> — chart on left, narrative on right\n\u003cdiv class=\"aside-container\"> — table on left, interpretation on right","type":"text"}]},{"type":"paragraph","content":[{"text":"The alternation of chart→narrative→table→narrative creates visual breathing room and prevents \"wall of data\" fatigue.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Rules for Narrative Text","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"State-lines","type":"text","marks":[{"type":"strong"}]},{"text":" (the italic intro under each heading): one sentence, max 20 words, states the conclusion not the topic. \"HRV down 13%, steps down 42%\" not \"This section covers health metrics\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Aside narratives","type":"text","marks":[{"type":"strong"}]},{"text":": 3-4 short paragraphs, each starting with a bold keyword. Written like a newspaper sidebar — facts first, interpretation second","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Flyouts","type":"text","marks":[{"type":"strong"}]},{"text":": reserved for actionable insights or methodology notes. The ✦ symbol marks them as \"pay attention\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No \"tells its own story\"","type":"text","marks":[{"type":"strong"}]},{"text":" or similar filler. Every sentence should contain a number or a decision","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Dual-Font Strategy","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Context","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Font","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Why","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"All body text, headers, captions","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"EB Garamond","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Classical editorial feel, excellent readability","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"All numbers in tables","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Monaspace Argon","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tabular figures align in columns, monospace scannability","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Big numbers in cards/dashboards","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Monaspace Argon","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Visual weight, distinct from prose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Status indicators, trend percentages","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Monaspace Argon","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Precision signaling","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Data source tags, code references","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Monaspace Argon","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Technical register","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ornament separators (:::)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Monaspace Argon with ligatures","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Programming aesthetic, replaces floral Unicode","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Color Principles","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"--ink","type":"text","marks":[{"type":"code_inline"}]},{"text":" (near-black) for text, ","type":"text"},{"text":"--bg","type":"text","marks":[{"type":"code_inline"}]},{"text":" (warm white) for background. Chart colors must be semantically meaningful — don't assign colors randomly:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Orange","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"--spark-claude","type":"text","marks":[{"type":"code_inline"}]},{"text":", #c45a28): primary data stream, effort/work metrics","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Green","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"--spark-wispr","type":"text","marks":[{"type":"code_inline"}]},{"text":", #2a7a5a): growth, positive health signals, English language","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Purple","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"--spark-social","type":"text","marks":[{"type":"code_inline"}]},{"text":", #5a5aaa): social/communication metrics","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Blue","type":"text","marks":[{"type":"strong"}]},{"text":" (rgba(42,80,140)): secondary overlay lines on charts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Red","type":"text","marks":[{"type":"strong"}]},{"text":" (#a02a2a): alerts, negative trends, declining metrics","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Amber","type":"text","marks":[{"type":"strong"}]},{"text":" (#c89000): warnings, watch-level signals","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Green","type":"text","marks":[{"type":"strong"}]},{"text":" (#2a7a3a): healthy baselines, positive trends","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Never use more than 3 colors in a single chart. If you need more, use opacity/saturation variations of the same hue.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Session Lessons (What Goes Wrong)","type":"text"}]},{"type":"paragraph","content":[{"text":"Based on building the reference report, these are the recurring problems:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Chart.js CDN version","type":"text","marks":[{"type":"strong"}]},{"text":": Use ","type":"text"},{"text":"@4","type":"text","marks":[{"type":"code_inline"}]},{"text":" not a specific patch version — specific versions may not exist","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Chart.js defaults","type":"text","marks":[{"type":"strong"}]},{"text":": Set individual properties, never replace entire objects (","type":"text"},{"text":"Chart.defaults.scale.grid.color = '#eee'","type":"text","marks":[{"type":"code_inline"}]},{"text":" not ","type":"text"},{"text":"Chart.defaults.scale.grid = {color: '#eee'}","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Legend circles","type":"text","marks":[{"type":"strong"}]},{"text":": Use ","type":"text"},{"text":"usePointStyle: false","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"boxWidth: 8, boxHeight: 8, borderRadius: 4","type":"text","marks":[{"type":"code_inline"}]},{"text":" for true circles. ","type":"text"},{"text":"usePointStyle: true","type":"text","marks":[{"type":"code_inline"}]},{"text":" creates ovals","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"file:// protocol","type":"text","marks":[{"type":"strong"}]},{"text":": Charts won't load CDN scripts via file://. Always test via localhost","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Back-to-back charts","type":"text","marks":[{"type":"strong"}]},{"text":": Always separate consecutive charts with narrative, a table, or an ornament. Two charts in a row = \"wall of data\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Table overflow on mobile","type":"text","marks":[{"type":"strong"}]},{"text":": Wrap in ","type":"text"},{"text":".table-wrapper","type":"text","marks":[{"type":"code_inline"}]},{"text":" and add ","type":"text"},{"text":".hide-mobile","type":"text","marks":[{"type":"code_inline"}]},{"text":" to secondary columns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Dual-axis charts","type":"text","marks":[{"type":"strong"}]},{"text":": Use sparingly — they invite false visual equivalence. Always label both axes clearly","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Narrative overreach","type":"text","marks":[{"type":"strong"}]},{"text":": Don't claim correlations without computing them. \"r = 0.10\" is more trustworthy than \"strong relationship\"","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Universal Data Adapter","type":"text"}]},{"type":"paragraph","content":[{"text":"When the user provides data from any source (CSV, JSON, SQLite, API, raw numbers), normalize it into the standard ","type":"text"},{"text":"ReportData","type":"text","marks":[{"type":"strong"}]},{"text":" intermediate format before generating HTML. This decouples data ingestion from report rendering.","type":"text"}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"references/data-adapter.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the ReportData JSON schema, field reference, and adapter instructions for each source type.","type":"text"}]},{"type":"paragraph","content":[{"text":"Workflow:","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User provides data → identify source type","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Transform into ReportData JSON (ask user for ","type":"text"},{"text":"meta.question","type":"text","marks":[{"type":"code_inline"}]},{"text":" and desired sections)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Confirm the normalized structure with the user","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Generate HTML from the ReportData using the block library","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Composable Block Library","type":"text"}]},{"type":"paragraph","content":[{"text":"Reports are assembled from typed blocks, each with a defined data contract. This replaces ad-hoc HTML generation with a systematic approach.","type":"text"}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"references/blocks.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the complete block catalog: sparkline-row, kpi-card, trend-chart, data-table, correlation-matrix, narrative, heatmap, strip-chart.","type":"text"}]},{"type":"paragraph","content":[{"text":"Each block defines:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Data contract","type":"text","marks":[{"type":"strong"}]},{"text":" (what JSON shape it expects)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"HTML template","type":"text","marks":[{"type":"strong"}]},{"text":" (copy-paste ready)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Composition rules","type":"text","marks":[{"type":"strong"}]},{"text":" (how blocks pair and sequence)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Preview Server","type":"text"}]},{"type":"paragraph","content":[{"text":"For iterative development, use the built-in live-reload server:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 ~/.claude/skills/tufte-report/scripts/serve.py report.html","type":"text"}]},{"type":"paragraph","content":[{"text":"Serves on ","type":"text"},{"text":"localhost:8042","type":"text","marks":[{"type":"code_inline"}]},{"text":", auto-reloads on file change with scroll position preserved. Zero dependencies — Python stdlib only.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Read ","type":"text"},{"text":"references/preview-server.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for details. After generating a report, offer to start the preview server for the user.","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"tufte-report","author":"@skillopedia","source":{"stars":237,"repo_name":"claude-skills","origin_url":"https://github.com/glebis/claude-skills/blob/HEAD/tufte-report/SKILL.md","repo_owner":"glebis","body_sha256":"25edee6bfd63cfc1a95137d881702d81a00c00ffd9c65341f06cac8b5d6e0f8d","cluster_key":"1f0a98f787c89e841d0a5d9a538d2c02eb1b1e67cdfd6faff37d14c8fa44ae8e","clean_bundle":{"format":"clean-skill-bundle-v1","source":"glebis/claude-skills/tufte-report/SKILL.md","attachments":[{"id":"d2cf7020-aa52-5023-851a-a7adf441efe3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d2cf7020-aa52-5023-851a-a7adf441efe3/attachment.md","path":"references/blocks.md","size":7689,"sha256":"4e08c9ad2dcec46ccb90d28009e3e57c0ff49f5ff4dd65c0ff60621bf79092c9","contentType":"text/markdown; charset=utf-8"},{"id":"7acdd02b-beb7-5b29-8ed9-c5f011efb698","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7acdd02b-beb7-5b29-8ed9-c5f011efb698/attachment.md","path":"references/charts.md","size":7540,"sha256":"ee8abb2aaed647fbfdc8474a46453393cead8ab589e3644c2b8b517b7f4d196f","contentType":"text/markdown; charset=utf-8"},{"id":"49dccb6b-c79e-5d48-adc1-5b001ea89fc4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/49dccb6b-c79e-5d48-adc1-5b001ea89fc4/attachment.md","path":"references/components.md","size":5257,"sha256":"7b363508f2cef55a92c4909bb2464e2c93bd92aab169f2c166af06f3a8f25a20","contentType":"text/markdown; charset=utf-8"},{"id":"1221590d-8e34-54c9-8ab7-e6cce644050b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1221590d-8e34-54c9-8ab7-e6cce644050b/attachment.md","path":"references/data-adapter.md","size":4582,"sha256":"17527ce82e5aaa9dd5e7c7bf732c4cc7e3cc6eeb6582272769464c220037ff93","contentType":"text/markdown; charset=utf-8"},{"id":"dc555db4-df58-505a-a24d-c9154d82c81f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dc555db4-df58-505a-a24d-c9154d82c81f/attachment.md","path":"references/design-tokens.md","size":3444,"sha256":"47901184d8d2714bbfefeaf56e7a67b6e016e3c01ac0427b5fad534684e46235","contentType":"text/markdown; charset=utf-8"},{"id":"d0b54f43-eb31-554b-94b4-d927bb37ace4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d0b54f43-eb31-554b-94b4-d927bb37ace4/attachment.md","path":"references/preview-server.md","size":5272,"sha256":"4fd435590f8635923173dc8a7f88212cdef80feb2fcf9955c6357046aead2137","contentType":"text/markdown; charset=utf-8"},{"id":"5cf02d52-beaf-5e2a-9926-a244504970ac","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5cf02d52-beaf-5e2a-9926-a244504970ac/attachment.py","path":"scripts/serve.py","size":3569,"sha256":"1da98d60d709c6c6b8e65f8005de288e4b64e2edf4f140a73e1d734a947917a8","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"9273f49fe4113d533e74301ea24287aee5fbd7ef7c669950e05c6ab493b57e2c","attachment_count":7,"text_attachments":7,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"tufte-report/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"web-development","import_tag":"clean-skills-v1","description":"Create Tufte-inspired data reports and infographic dashboards as standalone HTML files. Uses EB Garamond for text, Monaspace Argon for numbers, Chart.js for interactive charts, and inline SVG sparklines. Produces publication-quality reports with 2-column narrative+data layouts, status dashboards, scroll animations, and responsive mobile support. Use this skill whenever the user wants to create a data report, activity dashboard, infographic, personal analytics page, health tracker visualization, or any document that combines narrative text with interactive charts and tables. Also triggers for \"make a report like Tufte\", \"create an infographic\", \"build a dashboard\", \"visualize my data\", or requests for beautiful data-driven documents."}},"renderedAt":1782981052637}

Tufte Report — Data-Driven Infographic Skill Create standalone HTML reports that combine editorial narrative with interactive data visualization in Edward Tufte's style: high information density, minimal chart junk, typography-first design. Design Philosophy Tufte's core principles drive every decision: - Data-ink ratio : every pixel of ink should represent data, not decoration - Small multiples : repeat a design to show comparison, not animation - Sparklines : word-sized graphics that live inside prose - Layering : overview first, then detail on demand - Integration : text and graphics share…