Part of Claude Code operator skills: a collection of skills for running a real Claude Code setup.
Infoviz: branded data visualization skill
Generate editorial-quality LinkedIn post images (1200x627) using HTML/CSS rendered via Playwright.
Design philosophy: data IS the point. Every pixel communicates information. Exaggerated sizes, big beautiful serif numbers, tinted labels. Think editorial magazine, not data science notebook.
Pipeline
- Get the data: pull from your own data source (see below), use data provided in conversation, or derive it from context
- Pick a chart type: bars, grid, columns, newspaper, or quote+stat
- Generate HTML: write a self-contained HTML file to
/tmp/infoviz-chart.html
- Screenshot:
python3 ~/.claude/skills/qq-infoviz/screenshot.py /tmp/infoviz-chart.html /tmp/chart-linkedin.png 1200 627 (pass width/height explicitly: the script defaults to a 1800x1800 square, which doesn’t match the 1200x627 canvas this skill designs for)
- Show the result: open
/tmp/chart-linkedin.png in your OS’s image viewer (open on macOS, xdg-open on Linux) so a human actually looks at it. Don’t just read the file as terminal text.
- Iterate: adjust and re-screenshot
Brand system
Canvas
- Dimensions: 1200x627px (LinkedIn optimal 1.91:1)
- Background:
#0f172a (dark navy / slate-900)
- Padding: 48px left/right (symmetric, both sides must match)
Fonts
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,300;8..60,400;8..60,600;8..60,700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
Typography scale (converged through 5 rounds)
| Element |
Font |
Size |
Weight |
Notes |
| Title |
Source Serif 4 |
46px |
300 (light), 600 (bold keywords) |
Centered, fills ~10/12 cols width |
| Numbers (grid/cols) |
Source Serif 4 |
56-58px |
700 |
Colored per stat |
| Numbers (bars) |
Source Serif 4 |
40px |
700 |
In separate column |
| Numbers (hero) |
Source Serif 4 |
100px |
700 |
Dominant element |
| Labels |
Inter |
18-19px |
700 |
Tinted with parent hue color |
| Bar labels |
Inter |
16px |
600 |
Sentence-flow (reads as continuation of number) |
Color palette
Stat Number Accent Label (tinted)
Red #f87171 #ef4444 #fecaca
Orange #fb923c #f97316 #fed7aa
Amber #fbbf24 #eab308 #fef08a
Blue #60a5fa #3b82f6 #bfdbfe
Purple #a78bfa #8b5cf6 #ddd6fe
Indigo #818cf8 #6366f1 #c7d2fe
Teal #2dd4bf #14b8a6 (derive)
Green #4ade80 #22c55e (derive)
Fill patterns (4 winners from pattern battle)
All fills go bottom-up. Pattern applies only to the filled area, not the entire cell.
/* 1. diag, standard diagonal stripes */
background: repeating-linear-gradient(-45deg,
rgba(R,G,B,0.12) 0px, rgba(R,G,B,0.12) 3px,
rgba(R,G,B,0.28) 3px, rgba(R,G,B,0.28) 8px);
/* 2. diag-med, medium diagonal (denser) */
background: repeating-linear-gradient(-45deg,
rgba(R,G,B,0.11) 0px, rgba(R,G,B,0.11) 2px,
rgba(R,G,B,0.27) 2px, rgba(R,G,B,0.27) 5.5px);
/* 3. diag-wide, bold diagonal bands */
background: repeating-linear-gradient(-45deg,
rgba(R,G,B,0.08) 0px, rgba(R,G,B,0.08) 5px,
rgba(R,G,B,0.26) 5px, rgba(R,G,B,0.26) 14px);
/* 4. dots, dense stipple */
background: radial-gradient(circle,
rgba(R,G,B,0.4) 1.5px, rgba(R,G,B,0.1) 1.5px);
background-size: 6px 6px;
/* flat, solid fill (no pattern) */
background: rgba(R,G,B,0.28);
Wavy edge (optional)
Apply via clip-path on the fill element:
clip-path: polygon(
0% 10%, 12% 5%, 25% 12%, 38% 3%, 50% 9%,
62% 2%, 75% 11%, 88% 4%, 100% 8%,
100% 100%, 0% 100%
);
When using wavy, add ~5% extra height to the fill to compensate for the wave.
Accent stripe
- 4px wide, left edge of cell/bar, full height
- Straight (not curved), clipped by parent’s border-radius via
overflow: hidden
- Full opacity accent color
Footer
- Height: 48px, no HR/border above it
- Left: source label (9px uppercase, slate-600) + detail (8px, slate-700)
- Right: your site or handle (11px, slate-500) + your logo or headshot (52px circle, margin-top -8px, no border)
Logo/headshot embedding
Always base64-encode (cloud-synced file paths, e.g. iCloud or Google Drive, break in Playwright since it can’t authenticate to fetch them):
import base64
from pathlib import Path
logo_path = Path('/path/to/your/logo-or-headshot.png')
logo_b64 = base64.b64encode(logo_path.read_bytes()).decode()
Chart types
1. Horizontal bars
Best for: ranked metrics, 4-6 stats, “what’s missing” stories.
- Sentence-flow labels: read as continuation of the number, “85% don’t name a single machine they run”
- Number in separate column (110px, right-aligned), bar track fills remaining width
- Bar track:
rgba(255,255,255,0.03), 6px border-radius
- Bar fill: reversed gradient (transparent to color), rounded right corners (
border-radius: 0 6px 6px 0)
- 4px accent stripe on left edge of track
- Gaps: 2px between rows
- Padding: symmetric left and right
2. Stat grid (3x2 or 2x3)
Best for: 6 equal-weight stats as a scorecard.
- Grid of cells, 6px gaps, 8px border-radius
- Each cell: unfilled bg
rgba(255,255,255,0.03), fill element inside (bottom-up, pattern or flat)
- Fill height = percentage value
- 4px accent stripe, left edge
- Number + tinted label anchored to bottom of cell
- Bottom-up fill preferred over left-right
3. Vertical columns (staircase)
Best for: showing relative scale, descending data.
- 6 columns, descending height = percentage
- Each column: full-height container with
rgba(255,255,255,0.04) unfilled bg
- Fill element inside: height = percentage, pattern or flat
- Number + label inside the fill, anchored to top of fill
- No accent stripe on columns
- 6px gaps between columns, 8px border-radius
4. Newspaper (hero + strip)
Best for: one dominant stat + supporting context.
- Top: hero cell (bigger, ~1.2fr) with the headline stat, label, and subtitle
- Bottom: 5-column strip of supporting stats
- All fills bottom-up
- Bottom row: numbers baseline-aligned (position: absolute, fixed bottom offset) so labels don’t push numbers around
5. Quote + stat strip
Best for: provocative copy backed by data.
- Top: pull quote (Source Serif 4, 34px) with italic red emphasis word
- Bottom: 3-column stat strip with accent stripes and fills
- Quote mark: Source Serif 4, 56px, slate-800
6. Top 3 bold
Best for: 3 hero stats, no bar chart needed, maximum visual impact.
- 3 equal columns, each with massive number (90px), full sentence label, optional subtitle
- Vertical gradient fills (bottom-up)
- Accent stripes on left edge
Data sources
Your own data source
If you keep a local database with the stats you want to chart (SQLite, a CSV, an analytics export, whatever), query it directly rather than guessing at numbers. Adapt table and column names to your own schema. Example pattern using SQLite:
import sqlite3
conn = sqlite3.connect('/path/to/your/data.db')
cursor = conn.execute("SELECT column_a, column_b FROM your_table WHERE ...")
Flag for whoever adopts this skill: the original version of this pipeline step queried one specific private database with a hardcoded absolute path and a fixed table schema. That’s a real functional dependency, not just prose, so it can’t be copied as-is; wire it to wherever your own numbers actually live.
Manual data
Data provided directly in conversation: percentages, counts, labels.
Anti-patterns
- No matplotlib: HTML/CSS pipeline only
- No warm/artisanal aesthetics: professional services feel
- No “data science notebook” aesthetic: editorial/magazine
- No small labels: push toward “too big,” data IS the point
- No top-down fills: always bottom-up
- No soft/blurred fill edges: clean or wavy only
- No diagonal fill edges: wavy or flat only
- No footer HR: clean separation
- No headshot border: just shadow
- Don’t guess at data: query your data source or ask for the real numbers
- Don’t use local file paths for images: always base64 encode
- Don’t dump charts as terminal text: always open the image in a viewer
Design principles
- Every pixel communicates: minimize whitespace, maximize information density
- Grid adherence: structured layouts, consistent spacing, symmetric padding
- Info hierarchy: title > numbers > labels > source. Each level visually distinct.
- Define spaces, anchor elements: title centered in its space, padding matches on both sides
- Sentence-flow labels: “85% don’t name their machines,” not “85% | No machines named”
- Exaggerated scales: big fonts show font beauty, data IS the point
- LinkedIn thumb test: must be readable and eye-catching at small size while scrolling
- Fewer stats bigger: 3 huge stats beat 6 tiny stats for LinkedIn engagement
Quality checklist
1---2name: qq-infoviz3description: Generate branded LinkedIn data visualizations (1200x627) using HTML/CSS and Playwright screenshots. Supports bar charts, stat grids, vertical columns, newspaper layouts, and quote+stat formats.4---56> Part of [Claude Code operator skills](https://github.com/lee-fuhr/claude-operator-skills): a collection of skills for running a real Claude Code setup.78# Infoviz: branded data visualization skill910Generate editorial-quality LinkedIn post images (1200x627) using HTML/CSS rendered via Playwright.1112**Design philosophy:** data IS the point. Every pixel communicates information. Exaggerated sizes, big beautiful serif numbers, tinted labels. Think editorial magazine, not data science notebook.1314## Pipeline15161. **Get the data**: pull from your own data source (see below), use data provided in conversation, or derive it from context172. **Pick a chart type**: bars, grid, columns, newspaper, or quote+stat183. **Generate HTML**: write a self-contained HTML file to `/tmp/infoviz-chart.html`194. **Screenshot**: `python3 ~/.claude/skills/qq-infoviz/screenshot.py /tmp/infoviz-chart.html /tmp/chart-linkedin.png 1200 627` (pass width/height explicitly: the script defaults to a 1800x1800 square, which doesn’t match the 1200x627 canvas this skill designs for)205. **Show the result**: open `/tmp/chart-linkedin.png` in your OS’s image viewer (`open` on macOS, `xdg-open` on Linux) so a human actually looks at it. Don’t just read the file as terminal text.216. **Iterate**: adjust and re-screenshot2223## Brand system2425### Canvas26- **Dimensions:** 1200x627px (LinkedIn optimal 1.91:1)27- **Background:** `#0f172a` (dark navy / slate-900)28- **Padding:** 48px left/right (symmetric, both sides must match)2930### Fonts31```html32<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,300;8..60,400;8..60,600;8..60,700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">33```3435### Typography scale (converged through 5 rounds)36| Element | Font | Size | Weight | Notes |37|---------|------|------|--------|-------|38| **Title** | Source Serif 4 | 46px | 300 (light), 600 (bold keywords) | Centered, fills ~10/12 cols width |39| **Numbers (grid/cols)** | Source Serif 4 | 56-58px | 700 | Colored per stat |40| **Numbers (bars)** | Source Serif 4 | 40px | 700 | In separate column |41| **Numbers (hero)** | Source Serif 4 | 100px | 700 | Dominant element |42| **Labels** | Inter | 18-19px | 700 | Tinted with parent hue color |43| **Bar labels** | Inter | 16px | 600 | Sentence-flow (reads as continuation of number) |4445### Color palette46```47Stat Number Accent Label (tinted)48Red #f87171 #ef4444 #fecaca49Orange #fb923c #f97316 #fed7aa50Amber #fbbf24 #eab308 #fef08a51Blue #60a5fa #3b82f6 #bfdbfe52Purple #a78bfa #8b5cf6 #ddd6fe53Indigo #818cf8 #6366f1 #c7d2fe54Teal #2dd4bf #14b8a6 (derive)55Green #4ade80 #22c55e (derive)56```5758### Fill patterns (4 winners from pattern battle)59All fills go **bottom-up**. Pattern applies **only to the filled area**, not the entire cell.6061```css62/* 1. diag, standard diagonal stripes */63background: repeating-linear-gradient(-45deg,64 rgba(R,G,B,0.12) 0px, rgba(R,G,B,0.12) 3px,65 rgba(R,G,B,0.28) 3px, rgba(R,G,B,0.28) 8px);6667/* 2. diag-med, medium diagonal (denser) */68background: repeating-linear-gradient(-45deg,69 rgba(R,G,B,0.11) 0px, rgba(R,G,B,0.11) 2px,70 rgba(R,G,B,0.27) 2px, rgba(R,G,B,0.27) 5.5px);7172/* 3. diag-wide, bold diagonal bands */73background: repeating-linear-gradient(-45deg,74 rgba(R,G,B,0.08) 0px, rgba(R,G,B,0.08) 5px,75 rgba(R,G,B,0.26) 5px, rgba(R,G,B,0.26) 14px);7677/* 4. dots, dense stipple */78background: radial-gradient(circle,79 rgba(R,G,B,0.4) 1.5px, rgba(R,G,B,0.1) 1.5px);80background-size: 6px 6px;8182/* flat, solid fill (no pattern) */83background: rgba(R,G,B,0.28);84```8586### Wavy edge (optional)87Apply via clip-path on the fill element:88```css89clip-path: polygon(90 0% 10%, 12% 5%, 25% 12%, 38% 3%, 50% 9%,91 62% 2%, 75% 11%, 88% 4%, 100% 8%,92 100% 100%, 0% 100%93);94```95When using wavy, add ~5% extra height to the fill to compensate for the wave.9697### Accent stripe98- 4px wide, left edge of cell/bar, full height99- Straight (not curved), clipped by parent’s border-radius via `overflow: hidden`100- Full opacity accent color101102### Footer103- Height: 48px, no HR/border above it104- Left: source label (9px uppercase, slate-600) + detail (8px, slate-700)105- Right: your site or handle (11px, slate-500) + your logo or headshot (52px circle, margin-top -8px, no border)106107### Logo/headshot embedding108Always base64-encode (cloud-synced file paths, e.g. iCloud or Google Drive, break in Playwright since it can’t authenticate to fetch them):109```python110import base64111from pathlib import Path112logo_path = Path('/path/to/your/logo-or-headshot.png')113logo_b64 = base64.b64encode(logo_path.read_bytes()).decode()114```115116## Chart types117118### 1. Horizontal bars119Best for: ranked metrics, 4-6 stats, “what’s missing” stories.120121- **Sentence-flow labels**: read as continuation of the number, “85% don’t name a single machine they run”122- Number in separate column (110px, right-aligned), bar track fills remaining width123- Bar track: `rgba(255,255,255,0.03)`, 6px border-radius124- Bar fill: reversed gradient (transparent to color), **rounded right corners** (`border-radius: 0 6px 6px 0`)125- 4px accent stripe on left edge of track126- Gaps: 2px between rows127- Padding: symmetric left and right128129### 2. Stat grid (3x2 or 2x3)130Best for: 6 equal-weight stats as a scorecard.131132- Grid of cells, 6px gaps, 8px border-radius133- Each cell: unfilled bg `rgba(255,255,255,0.03)`, fill element inside (bottom-up, pattern or flat)134- Fill height = percentage value135- 4px accent stripe, left edge136- Number + tinted label anchored to bottom of cell137- Bottom-up fill preferred over left-right138139### 3. Vertical columns (staircase)140Best for: showing relative scale, descending data.141142- 6 columns, descending height = percentage143- Each column: full-height container with `rgba(255,255,255,0.04)` unfilled bg144- Fill element inside: height = percentage, pattern or flat145- Number + label inside the fill, anchored to top of fill146- No accent stripe on columns147- 6px gaps between columns, 8px border-radius148149### 4. Newspaper (hero + strip)150Best for: one dominant stat + supporting context.151152- Top: hero cell (bigger, ~1.2fr) with the headline stat, label, and subtitle153- Bottom: 5-column strip of supporting stats154- All fills bottom-up155- Bottom row: **numbers baseline-aligned** (position: absolute, fixed bottom offset) so labels don’t push numbers around156157### 5. Quote + stat strip158Best for: provocative copy backed by data.159160- Top: pull quote (Source Serif 4, 34px) with italic red emphasis word161- Bottom: 3-column stat strip with accent stripes and fills162- Quote mark: Source Serif 4, 56px, slate-800163164### 6. Top 3 bold165Best for: 3 hero stats, no bar chart needed, maximum visual impact.166167- 3 equal columns, each with massive number (90px), full sentence label, optional subtitle168- Vertical gradient fills (bottom-up)169- Accent stripes on left edge170171## Data sources172173### Your own data source174175If you keep a local database with the stats you want to chart (SQLite, a CSV, an analytics export, whatever), query it directly rather than guessing at numbers. Adapt table and column names to your own schema. Example pattern using SQLite:176177```python178import sqlite3179conn = sqlite3.connect('/path/to/your/data.db')180cursor = conn.execute("SELECT column_a, column_b FROM your_table WHERE ...")181```182183**Flag for whoever adopts this skill:** the original version of this pipeline step queried one specific private database with a hardcoded absolute path and a fixed table schema. That’s a real functional dependency, not just prose, so it can’t be copied as-is; wire it to wherever your own numbers actually live.184185### Manual data186187Data provided directly in conversation: percentages, counts, labels.188189## Anti-patterns190- No matplotlib: HTML/CSS pipeline only191- No warm/artisanal aesthetics: professional services feel192- No “data science notebook” aesthetic: editorial/magazine193- No small labels: push toward “too big,” data IS the point194- No top-down fills: always bottom-up195- No soft/blurred fill edges: clean or wavy only196- No diagonal fill edges: wavy or flat only197- No footer HR: clean separation198- No headshot border: just shadow199- Don’t guess at data: query your data source or ask for the real numbers200- Don’t use local file paths for images: always base64 encode201- Don’t dump charts as terminal text: always open the image in a viewer202203## Design principles2041. **Every pixel communicates**: minimize whitespace, maximize information density2052. **Grid adherence**: structured layouts, consistent spacing, symmetric padding2063. **Info hierarchy**: title > numbers > labels > source. Each level visually distinct.2074. **Define spaces, anchor elements**: title centered in its space, padding matches on both sides2085. **Sentence-flow labels**: “85% don’t name their machines,” not “85% | No machines named”2096. **Exaggerated scales**: big fonts show font beauty, data IS the point2107. **LinkedIn thumb test**: must be readable and eye-catching at small size while scrolling2118. **Fewer stats bigger**: 3 huge stats beat 6 tiny stats for LinkedIn engagement212213## Quality checklist214- [ ] 1200x627px exact215- [ ] Title fills ~10/12 cols width (46px)216- [ ] Labels are BIG (18-19px) and tinted with parent hue217- [ ] Numbers are beautiful (Source Serif 4, 700 weight)218- [ ] Padding symmetric left/right219- [ ] Fills bottom-up, pattern only in filled area220- [ ] Accent stripes full-height, clipped by parent radius221- [ ] Footer: no HR, headshot has no border, 48px height222- [ ] Opened in an image viewer for a human to actually look at (not just read as terminal text)223- [ ] Visually verified via the Read tool before treating it as done