Create Visualizations

VerifiedCaution

Generate TSVC-branded PNG chart visualizations from research data, with bilingual options, and insert image references into articles.

Sby Skills Guide Bot
ContentAdvanced
007/25/2026
Claude CodeCursorWindsurf
#data-visualization#chart-generation#content-pipeline#tsvc#screenshot

Recommended for

Our review

Generates branded PNG chart visualizations for TSVC articles by extracting data from research files, creating bilingual HTML templates, and taking screenshots via Playwright.

Strengths

  • Automates creation of branded, bilingual (English+Spanish) charts
  • Enforces design constraints and density limits for readability
  • Integrates with article pipeline to insert image references automatically
  • Uses local assets and no external CDN for reliability

Limitations

  • Requires Playwright and Node.js setup
  • Only works for TSVC brand and specific slug structure
  • Limited to max 5 bars, 3 cards, etc., may need splitting complex data
When to use it

When you need to add professional, branded charts to a TSVC article from research data quickly and consistently.

When not to use it

For simple text-based articles or when data is too large for the density limits.

Security analysis

Caution
Quality score92/100

The skill uses Bash to run a screenshot script that launches a local HTTP server and uses Playwright to capture chart images. No destructive or exfiltrating commands are present, and all operations are scoped to generating visualizations within the article output directory. The risk is low but present due to tool usage.

Findings
  • Uses Bash to execute screenshot.js, which starts a local HTTP server and uses Playwright to capture chart images; this is a legitimate purpose but involves running system commands.

Examples

Startup Accelerator Programs
Use the create-visualizations skill for slug 'startup-accelerator-programs' to generate charts from the research data.
Fundraising Tips Article
I have an article slug 'fundraising-tips'. Please run the visualization pipeline on it.
Market Sizing Guide
Execute the create-visualizations pipeline for the slug 'market-sizing-guide' to produce bilingual charts and insert them.

name: create-visualizations description: Generate TSVC-branded PNG chart visualizations (English + Spanish) from an article's research data. Use after write-article has produced output/<slug>/. Scans research files, presents 3-5 chart options, generates bilingual HTML templates, screenshots to PNG via Playwright in a single pass, and inserts image references into the article. argument-hint: [slug] allowed-tools: Task, Read, Write, Edit, Bash, Glob

Create Visualizations — TSVC Chart Pipeline

You are a visualization pipeline for The Startup VC (thestartupvc.com). Given an article slug, you extract chart-worthy data from research files, present options, generate branded HTML charts, screenshot them to PNG, and insert image references into the final article.

Slug: $ARGUMENTS


Design Philosophy

Clarity over cleverness. These charts help entrepreneurs make business decisions. If data is hard to parse at a glance, the design has failed.

  • One insight per visualization. If you need a second comparison, make a second chart.
  • Data hierarchy. The most important number is the largest, boldest element.
  • Generous whitespace. 20px inside containers, 16px between elements, 12px inside cards minimum.
  • Restrained decoration. One gradient, light shadows, thin accent borders. No layered effects.
  • Readable at every size. Minimum 13px body text, 14px for chart numbers, 11px absolute floor.

Hard Constraints

These constraints are NON-NEGOTIABLE. Every HTML template must satisfy all of them.

| Constraint | Required Value | |------------|---------------| | Body width | Exactly 600px | | Font stack | 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif | | FA CSS path | ../node_modules/@fortawesome/fontawesome-free/css/all.min.css (relative to html/ dir) | | Icons | Font Awesome <i class="fa-solid fa-[icon]"></i> only — NO emoji | | External CDN | None — all assets must be local | | Container class | .container on outer div (required by screenshot.js) | | Logo | ../logo.svg — local file, copied to images/ during setup (served by screenshot.js HTTP server) | | Footer URL | www.thestartupvc.com |


Brand Colors

Use THESE colors. Not the old Rewrite_TSVC colors (#28DF8D is WRONG).

:root {
    --primary: #00C9A7;          /* teal/green — main accent */
    --primary-dark: #00b396;     /* darker teal — gradients, borders */
    --success: #10B981;          /* emerald — secondary accent */
    --text: #333333;             /* default body text */
    --text-secondary: #666666;   /* secondary labels */
    --text-muted: #7f8c8d;       /* tertiary/footnote text */
    --border: #e8e8e8;           /* dividers */
    --bg-white: #ffffff;         /* card backgrounds */
    --bg-section: #f5f5f5;       /* alternate section backgrounds */
    --accent: #e6faf6;           /* light teal tint for callout boxes */
}

Density Limits

Never exceed these. If data exceeds the limit, split into two charts. Never shrink text to fit.

| Chart Type | Max Items | Notes | |------------|----------|-------| | Horizontal bar chart | 5 bars | Abbreviate values ($12K not $12,000) | | Side-by-side cards | 3 cards | One primary stat + 1–2 details each | | Stats dashboard | 4 stat cards | One number per card | | Timeline / process | 5 items | Labels: 2–3 words max | | Country comparison | 3 countries | More than 3 → use bar chart instead | | Donut / progress | 3 segments | CSS conic-gradient only, no JS |


Phase 1: Data Extraction

Requirement: VIZ-01

1.1 Resolve the Slug

The slug comes from $ARGUMENTS. If not provided, ask:

What is the article slug? (e.g. startup-accelerator-programs)

1.2 Resume Detection

Check output/<slug>/images/metadata.json. If it exists AND PNG files are present in output/<slug>/images/output/:

Images already exist for this article. Options:

  1. Skip to article insertion (Phase 4)
  2. Re-run from scratch (will overwrite existing images)

Wait for user reply. "1" or "skip" → jump to Phase 4. "2" or "re-run" → continue from Phase 1.

1.3 Read Article and Research Files

Read these files:

  1. output/<slug>/04-final.md — the finalized article (for H2 placement context)
  2. Every .md file in output/<slug>/02-research/ — use Glob to find all files
Use Glob: output/<slug>/02-research/*.md
Read each file found.

Critical rule: Chart data MUST come from 02-research/ files. Research files contain source-verified numbers. Article prose may paraphrase or round. Do not infer numbers from article prose alone.

1.4 Score Data Points for Chart Suitability

For each distinct data set found in research files, score it for visualization potential:

| Data Type | Best Chart Type | |-----------|----------------| | Cost comparisons (operating costs, salaries, fees) | Horizontal bar | | Market size stats (funding volumes, growth rates, deal counts) | Stats dashboard or bar | | Regulatory / tax rates (side-by-side across countries) | Country cards or bar | | Talent metrics (graduate counts, proficiency rates) | Stats dashboard | | Timeline / process (incorporation steps, milestones) | Timeline | | Investment sector breakdowns (≤3 categories) | Donut |

Select the top 5 candidates. Fewer is fine if data is sparse — do not force charts for weak data.


Phase 2: Present Options

Requirement: VIZ-02

Present options as a numbered ranked list. Format:

**Visualization Options for: [Article H1 Title]**

1. [Bar Chart] Operating Costs by Country
   Data: Colombia $8K/mo, Mexico $12K/mo, Argentina $6K/mo, Brazil $15K/mo
   Placement: Under "What Are the Operating Costs?" — supports cost comparison data in that section

2. [Stats Dashboard] Latin America VC Market Size
   Data: $4.2B total (2024), 312 deals, 38% YoY growth, 5 active unicorns
   Placement: Under "How Large Is the Market?" — supports market overview section

3. [Timeline] Incorporation Process Steps
   Data: Reserve name (1 day), File documents (3 days), Tax registration (5 days), Bank account (7 days), Operating permit (10 days)
   Placement: Under "How Do You Register a Company?" — illustrates the step-by-step process

[Reply with numbers (e.g. "1, 3") or "all" to proceed]

Rules for the options list:

  • Always include actual numbers from research in the Data field
  • Always include which H2 section and why in the Placement field
  • The thumbnail is generated automatically — do NOT list it as an option
  • Maximum 5 options

STOP. Wait for user reply.

  • Numbers (e.g. "1, 3" or "2") → generate those charts
  • "all" → generate all options
  • Any other reply → ask again with the same options list. Do NOT advance without a valid selection.

2.1 Write chart-manifest.json

After the user confirms their selection, write a chart-manifest file that the Phase 3 subagent will read. This decouples Phase 2 output from Phase 3 execution.

Write output/<slug>/images/chart-manifest.json:

{
  "slug": "<slug>",
  "project_root": "/home/felo/work/content/tsvc_new",
  "charts": [
    {
      "index": 1,
      "type": "bars",
      "title_en": "<English chart title>",
      "title_es": "<Spanish chart title>",
      "subtitle_en": "<Source line in English>",
      "subtitle_es": "<Source line in Spanish>",
      "filename_suffix": "bars",
      "data": "<concise data description — e.g. Colombia $8K, Mexico $12K, Argentina $6K>",
      "placement_h2": "<H2 section heading where chart will be inserted>",
      "chart_type_hint": "<chart type — bars | stats | timeline | cards | donut>",
      "key_insight_en": "<Key insight sentence in English>",
      "key_insight_es": "<Key insight sentence in Spanish>"
    }
  ]
}

Include one entry per selected chart. Do NOT include the thumbnail (it is always generated separately). Translate titles, subtitles, and key insights to Spanish at this stage — they are short strings (2-15 words) that are easy to translate with article context in scope.

This file is the handoff contract between Phase 2 (main context) and Phase 3 (subagent context).


Phase 3: HTML Templates + Screenshot

Requirement: VIZ-03

3.0 Spawn Phase 3 Subagent

After Phase 2.1 (chart-manifest.json is written), spawn a Task subagent with this prompt:


You are the Phase 3 HTML generation agent for the TSVC visualization pipeline.

Read these files to load your context:

  • /home/felo/work/content/tsvc_new/.claude/skills/create-visualizations/SKILL.md (sections 3.1–3.6 for HTML format reference)
  • /home/felo/work/content/tsvc_new/output/<slug>/images/chart-manifest.json (chart specs)

Your job:

  1. Run Phase 3.1 setup (single chained Bash command — see section 3.1)
  2. For each chart in chart-manifest.json AND for the thumbnail, spawn all image agents as parallel sub-Task agents using the Task tool. All sub-Tasks run simultaneously in one message — do NOT wait for one to finish before spawning the next.

Thumbnail sub-Task prompt:


Generate the featured thumbnail for The Startup VC article using Sharp compositing.

Slug: <slug> Working directory: /home/felo/work/content/tsvc_new/output/<slug>/images/

Step 1 — Derive search query: Read output/<slug>/04-final.md to get the article H1. Derive a concise 2-3 word search query from the article topic (use the subject matter, not the tagline). Examples: "startup accelerator funding" → "accelerator programs"; "SaaS valuation guide" → "software valuation".

Step 2 — Read API keys from .env:

UNSPLASH_KEY=$(grep UNSPLASH_ACCESS_KEY /home/felo/work/content/tsvc_new/.env | cut -d= -f2 | tr -d '[:space:]')
PEXELS_KEY=$(grep PEXELS_API_KEY /home/felo/work/content/tsvc_new/.env | cut -d= -f2 | tr -d '[:space:]')

Step 3 — Fetch stock photo (3-tier):

Create images directory if needed:

mkdir -p /home/felo/work/content/tsvc_new/output/<slug>/images

Set PHOTO_DOWNLOADED=false. Attempt in order:

Tier 1 — Unsplash API (primary):

if [[ -n "$UNSPLASH_KEY" ]]; then
  UNSPLASH_RESPONSE=$(curl -s \
    -H "Authorization: Client-ID ${UNSPLASH_KEY}" \
    "https://api.unsplash.com/search/photos?query={search-query}&orientation=landscape&per_page=1")

  PHOTO_URL=$(echo "$UNSPLASH_RESPONSE" | jq -r '.results[0].urls.regular // empty')
  DOWNLOAD_TRIGGER=$(echo "$UNSPLASH_RESPONSE" | jq -r '.results[0].links.download_location // empty')

  if [[ -n "$PHOTO_URL" && "$PHOTO_URL" != "null" ]]; then
    # REQUIRED by Unsplash API terms — trigger download to credit the photographer
    curl -s -H "Authorization: Client-ID ${UNSPLASH_KEY}" "${DOWNLOAD_TRIGGER}" > /dev/null

    curl -L "${PHOTO_URL}" -o /home/felo/work/content/tsvc_new/output/<slug>/images/featured-photo.jpg
    FILE_SIZE=$(stat -c%s "/home/felo/work/content/tsvc_new/output/<slug>/images/featured-photo.jpg" 2>/dev/null || echo 0)
    [[ "$FILE_SIZE" -gt 1000 ]] && PHOTO_DOWNLOADED=true
  fi
fi

Tier 2 — Pexels API (fallback): Use when Unsplash is unavailable or returns no results.

if [[ "$PHOTO_DOWNLOADED" == "false" && -n "$PEXELS_KEY" ]]; then
  PEXELS_RESPONSE=$(curl -s \
    -H "Authorization: ${PEXELS_KEY}" \
    "https://api.pexels.com/v1/search?query={search-query}&orientation=landscape&per_page=1")

  PHOTO_URL=$(echo "$PEXELS_RESPONSE" | jq -r '.photos[0].src.landscape // .photos[0].src.large2x // empty')

  if [[ -n "$PHOTO_URL" && "$PHOTO_URL" != "null" ]]; then
    curl -L "${PHOTO_URL}" -o /home/felo/work/content/tsvc_new/output/<slug>/images/featured-photo.jpg
    FILE_SIZE=$(stat -c%s "/home/felo/work/content/tsvc_new/output/<slug>/images/featured-photo.jpg" 2>/dev/null || echo 0)
    [[ "$FILE_SIZE" -gt 1000 ]] && PHOTO_DOWNLOADED=true
  fi
fi

Note: Pexels Authorization is Authorization: {KEY} — NOT Bearer {KEY}.

Tier 3 — CDN fallback pool (last resort):

if [[ "$PHOTO_DOWNLOADED" == "false" ]]; then
  FALLBACK_URLS=(
    "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=1200&h=630&fit=crop&auto=format&q=85"
    "https://images.unsplash.com/photo-1497366216548-37526070297c?w=1200&h=630&fit=crop&auto=format&q=85"
    "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=1200&h=630&fit=crop&auto=format&q=85"
    "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=1200&h=630&fit=crop&auto=format&q=85"
    "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=1200&h=630&fit=crop&auto=format&q=85"
  )
  curl -L "${FALLBACK_URLS[0]}" -o /home/felo/work/content/tsvc_new/output/<slug>/images/featured-photo.jpg
  PHOTO_DOWNLOADED=true
fi

Verify download:

ls -lh /home/felo/work/content/tsvc_new/output/<slug>/images/featured-photo.jpg

If file is missing or < 1KB, report failure: "ERROR: featured-photo.jpg download failed — all three tiers exhausted."

Step 4 — Run generate-thumbnail.js:

cd /home/felo/work/content/tsvc_new/output/<slug>/images && node generate-thumbnail.js <slug>

Step 5 — Verify output:

ls /home/felo/work/content/tsvc_new/output/<slug>/images/output/<slug>-thumbnail*.png

When done, report: "Thumbnail complete: <slug>-thumbnail.png and <slug>-thumbnail-es.png (1200x630, stock photo background)"

Chart sub-Task prompt template (customize per chart — one sub-Task per chart, all spawned in parallel):


Generate two HTML chart files for The Startup VC.

Read /home/felo/work/content/tsvc_new/.claude/skills/create-visualizations/SKILL.md sections 3.2–3.6 for HTML format requirements (boilerplate, brand colors, constraints, chart-specific snippets).

Chart spec:

  • Slug: <slug>
  • Chart type: <chart.chart_type_hint>
  • Filename suffix: <chart.filename_suffix>
  • English title: <chart.title_en>
  • Spanish title: <chart.title_es>
  • English subtitle: <chart.subtitle_en>
  • Spanish subtitle: <chart.subtitle_es>
  • Data: <chart.data>
  • Key insight (EN): <chart.key_insight_en>
  • Key insight (ES): <chart.key_insight_es>

Files to create:

  • /home/felo/work/content/tsvc_new/output/<slug>/images/html/<slug>-<chart.filename_suffix>-en.html
  • /home/felo/work/content/tsvc_new/output/<slug>/images/html/<slug>-<chart.filename_suffix>-es.html

Requirements:

  • Use exact boilerplate from SKILL.md section 3.3
  • Insert chart-specific HTML from SKILL.md section 3.4 for the chart type
  • Body width: exactly 600px
  • FA CSS path: ../node_modules/@fortawesome/fontawesome-free/css/all.min.css
  • No emoji — Font Awesome icons only
  • .container class on outer div
  • Logo in footer: <img src="../logo.svg" alt="The Startup VC"> (served locally via HTTP server)
  • Footer URL: www.thestartupvc.com

When done, report: "Chart <index> complete: <en-filename> and <es-filename>"

  1. Wait for ALL parallel sub-Task agents to complete before proceeding.
  2. Run Phase 3.7 validation (see section 3.7): cd /home/felo/work/content/tsvc_new/output/<slug>/images && node validate-html.js --html-dir html/
    • Fix any FAIL items by editing the relevant HTML files directly
    • Re-run validate-html.js after fixes until all checks pass
  3. Run Phase 3.7 screenshot (see section 3.7): cd /home/felo/work/content/tsvc_new/output/<slug>/images && node screenshot.js
  4. Verify all PNGs exist: ls /home/felo/work/content/tsvc_new/output/<slug>/images/output/
  5. Report back to main session: list of all PNG filenames generated (one per line)

Wait for the subagent to complete and return the PNG list. The subagent streams its output directly to the conversation.

After the subagent completes, continue to Phase 4 (article insertion) using the PNG list returned by the subagent.

Important: Do NOT generate any HTML files inline in the main session. All HTML generation happens inside the subagent. The main session's only Phase 3 action is spawning the subagent and waiting for its result.

3.1 Setup: Copy Tooling Files

Run this as a single chained Bash command — all steps execute in the same shell, guaranteeing the directory exists before npm runs:

mkdir -p output/<slug>/images/html output/<slug>/images/output \
  && ([ ! -f output/<slug>/images/screenshot.js ] && cp .claude/skills/create-visualizations/screenshot.js output/<slug>/images/screenshot.js || true) \
  && ([ ! -f output/<slug>/images/validate-html.js ] && cp .claude/skills/create-visualizations/validate-html.js output/<slug>/images/validate-html.js || true) \
  && ([ ! -f output/<slug>/images/generate-thumbnail.js ] && cp .claude/skills/create-visualizations/generate-thumbnail.js output/<slug>/images/generate-thumbnail.js || true) \
  && ([ ! -f output/<slug>/images/package.json ] && cp .claude/skills/create-visualizations/package.json output/<slug>/images/package.json || true) \
  && ([ ! -f output/<slug>/images/logo.svg ] && cp .claude/skills/create-visualizations/logo.svg output/<slug>/images/logo.svg || true) \
  && cd output/<slug>/images \
  && ([ ! -d node_modules ] && npm install || true) \
  && ([ ! -d node_modules/.cache/ms-playwright ] && npx playwright install chromium || true)

Critical: Do NOT split this into separate Bash tool calls. The Bash tool does not persist working directory between calls. If mkdir and cd run in separate tool calls, the cd will fail because the shell state is reset. This single-command approach is the only reliable pattern.

npx playwright install chromium must run after npm install — Playwright stores Chromium in node_modules/.cache/ms-playwright/.

3.2 Filename Convention

| Chart type | Filename suffix | |------------|----------------| | Horizontal bar | bars | | Stats dashboard | stats | | Timeline / process | timeline | | Donut / progress | donut | | Country cards | cards |

Full filename: <slug>-<type>-en.html (English) and <slug>-<type>-es.html (Spanish)

If multiple charts of the same type: append number — bars-1-en, bars-1-es.

Thumbnail: <slug>-thumbnail.png (EN) and <slug>-thumbnail-es.png (ES) — generated directly by generate-thumbnail.js, no HTML file involved

3.3 HTML Template Structure

Every chart HTML file MUST use this exact boilerplate. Replace only the placeholder content.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>[Visualization Title]</title>
    <!-- CRITICAL: path must be exactly ../node_modules/... — do NOT change this path -->
    <link rel="stylesheet" href="../node_modules/@fortawesome/fontawesome-free/css/all.min.css">
    <style>
        :root {
            --primary: #00C9A7;
            --primary-dark: #00b396;
            --success: #10B981;
            --text: #333333;
            --text-secondary: #666666;
            --text-muted: #7f8c8d;
            --border: #e8e8e8;
            --bg-white: #ffffff;
            --bg-section: #f5f5f5;
            --accent: #e6faf6;
        }
        * { box-sizing: border-box; }
        body {
            font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: transparent;
            margin: 0;
            padding: 0;
            width: 600px;
        }
        .container {
            background: var(--bg-white);
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
            overflow: hidden;
        }
        .header {
            background: linear-gradient(135deg, var(--primary-dark), var(--primary));
            color: #ffffff;
            padding: 24px 28px;
            text-align: center;
        }
        .header h1 {
            font-size: 20px;
            font-weight: 700;
            margin: 0 0 6px;
            color: #ffffff;
            line-height: 1.3;
        }
        .header .subtitle {
            font-size: 13px;
            opacity: 0.9;
            margin: 0;
        }
        .content {
            padding: 24px 28px;
        }
        .key-insight {
            background: var(--accent);
            border-left: 3px solid var(--primary);
            padding: 14px 18px;
            border-radius: 0 8px 8px 0;
            margin-top: 20px;
            font-size: 13px;
            color: var(--text);
            line-height: 1.5;
        }
        .key-insight strong {
            color: var(--primary-dark);
        }
        .footer {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 12px 28px;
            border-top: 1px solid var(--border);
            font-size: 11px;
            color: var(--text-muted);
        }
        .footer-url {
            font-weight: 600;
            letter-spacing: 0.02em;
        }
        .footer-logo img {
            height: 22px;
            opacity: 0.75;
        }
        /* Add chart-specific styles below */
    </style>
</head>
<body>
    <div class="container">

        <!-- Header -->
        <div class="header">
            <h1>[Chart Title]</h1>
            <p class="subtitle">[Subtitle — data source and year, e.g. "Source: 2024 Latin America VC Report"]</p>
        </div>

        <!-- Visualization area -->
        <div class="content">

            <!-- INSERT CHART HTML HERE -->

            <!-- Key insight callout (required) -->
            <div class="key-insight">
                <p><strong>Key insight:</strong> [One concrete takeaway sentence — e.g. "Colombia's operating costs are 47% lower than Brazil's, making it the most cost-efficient market in the region."]</p>
            </div>

        </div>

        <!-- Footer (required) -->
        <div class="footer">
            <span class="footer-url">www.thestartupvc.com</span>
            <div class="footer-logo">
                <img src="../logo.svg" alt="The Startup VC">
            </div>
        </div>

    </div>
</body>
</html>

3.4 Chart-Specific HTML Snippets

Insert these snippets into the visualization area of the boilerplate. Add accompanying CSS inside the <style> block.

Horizontal Bar Chart

/* Bar chart styles */
.chart-area { margin-bottom: 8px; }
.bar-row {
    display: flex;
    align-items: center;
    margin-bottom: 14px;
}
.bar-label {
    width: 130px;
    font-size: 13px;
    font-weight: 500;
    color: var(--text);
    flex-shrink: 0;
    padding-right: 12px;
}
.bar-track {
    flex: 1;
    background: var(--bg-section);
    border-radius: 4px;
    height: 30px;
    overflow: hidden;
}
.bar-fill {
    height: 30px;
    border-radius: 4px;
    background: linear-gradient(90deg, var(--primary-dark), var(--primary));
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding-right: 10px;
    min-width: 40px;
    transition: none;
}
.bar-value {
    font-size: 12px;
    font-weight: 700;
    color: #ffffff;
    white-space: nowrap;
}
<!-- Horizontal bar example (max 5 bars) -->
<div class="chart-area">
    <div class="bar-row">
        <div class="bar-label">[Country/Item]</div>
        <div class="bar-track">
            <div class="bar-fill" style="width: 85%;">
                <span class="bar-value">$XX,XXX</span>
            </div>
        </div>
    </div>
    <!-- Repeat for up to 5 bars -->
</div>

Bar width calculation: Use the highest value as 95% and scale others proportionally. The bar with the widest fill is visually dominant.

Stats Dashboard

/* Stats dashboard styles */
.stats-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 16px;
    margin-bottom: 8px;
}
.stat-card {
    background: var(--bg-section);
    border-radius: 10px;
    padding: 18px 16px;
    text-align: center;
    border: 1px solid var(--border);
}
.stat-icon {
    font-size: 22px;
    color: var(--primary);
    margin-bottom: 10px;
}
.stat-number {
    font-size: 26px;
    font-weight: 700;
    color: var(--text);
    line-height: 1;
    margin-bottom: 6px;
}
.stat-label {
    font-size: 12px;
    color: var(--text-secondary);
    font-weight: 500;
    margin-bottom: 4px;
}
.stat-context {
    font-size: 11px;
    color: var(--text-muted);
}
<!-- Stats dashboard (max 4 stat cards, 2x2 grid) -->
<div class="stats-grid">
    <div class="stat-card">
        <div class="stat-icon"><i class="fa-solid fa-chart-line"></i></div>
        <div class="stat-number">$4.2B</div>
        <div class="stat-label">Total VC Funding 2024</div>
        <div class="stat-context">+38% vs 2023</div>
    </div>
    <!-- Repeat for up to 4 cards -->
</div>

Timeline / Process

/* Timeline styles */
.timeline {
    position: relative;
    padding-left: 32px;
    margin-bottom: 8px;
}
.timeline::before {
    content: '';
    position: absolute;
    left: 10px;
    top: 8px;
    bottom: 8px;
    width: 2px;
    background: var(--border);
}
.timeline-item {
    position: relative;
    margin-bottom: 18px;
    display: flex;
    align-items: flex-start;
    gap: 14px;
}
.timeline-item:last-child { margin-bottom: 0; }
.timeline-dot {
    position: absolute;
    left: -26px;
    width: 14px;
    height: 14px;
    background: var(--primary);
    border-radius: 50%;
    border: 2px solid #ffffff;
    box-shadow: 0 0 0 2px var(--primary);
    flex-shrink: 0;
    margin-top: 2px;
}
.timeline-content { flex: 1; }
.timeline-step {
    font-size: 12px;
    font-weight: 700;
    color: var(--primary-dark);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: 2px;
}
.timeline-label {
    font-size: 14px;
    font-weight: 600;
    color: var(--text);
    margin-bottom: 2px;
}
.timeline-detail {
    font-size: 12px;
    color: var(--text-secondary);
}
<!-- Timeline (max 5 items) -->
<div class="timeline">
    <div class="timeline-item">
        <div class="timeline-dot"></div>
        <div class="timeline-content">
            <div class="timeline-step">Step 1</div>
            <div class="timeline-label">[Step Name]</div>
            <div class="timeline-detail">[Duration or key detail]</div>
        </div>
    </div>
    <!-- Repeat for up to 5 steps -->
</div>

Country Comparison Cards

/* Country cards styles */
.country-cards {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 14px;
    margin-bottom: 8px;
}
.country-card {
    background: var(--bg-section);
    border-radius: 10px;
    padding: 16px 14px;
    text-align: center;
    border: 1px solid var(--border);
}
.country-card.highlight {
    border-color: var(--primary);
    background: var(--accent);
}
.country-flag {
    font-size: 20px;
    color: var(--text-secondary);
    margin-bottom: 8px;
}
.country-name {
    font-size: 13px;
    font-weight: 700;
    color: var(--text);
    margin-bottom: 10px;
}
.country-stat {
    font-size: 22px;
    font-weight: 700;
    color: var(--primary-dark);
    line-height: 1;
    margin-bottom: 4px;
}
.country-label {
    font-size: 11px;
    color: var(--text-secondary);
    margin-bottom: 8px;
}
.country-note {
    font-size: 11px;
    color: var(--text-muted);
    border-top: 1px solid var(--border);
    padding-top: 8px;
    margin-top: 8px;
}
<!-- Country comparison (max 3 countries) -->
<!-- Use class="country-card highlight" for the recommended/best option -->
<div class="country-cards">
    <div class="country-card highlight">
        <div class="country-flag"><i class="fa-solid fa-location-dot"></i></div>
        <div class="country-name">[Country]</div>
        <div class="country-stat">$XX</div>
        <div class="country-label">[Metric Label]</div>
        <div class="country-note">[Distinguishing detail]</div>
    </div>
    <!-- Repeat for 2-3 countries total -->
</div>

Donut Chart

/* Donut chart styles */
.donut-wrapper {
    display: flex;
    align-items: center;
    gap: 28px;
    justify-content: center;
    margin-bottom: 8px;
}
.donut {
    width: 180px;
    height: 180px;
    border-radius: 50%;
    /* conic-gradient: each segment = percentage of 360deg */
    background: conic-gradient(
        var(--primary) 0% [SEGMENT_1_%],
        var(--success) [SEGMENT_1_%] [SEGMENT_1+2_%],
        var(--border) [SEGMENT_1+2_%] 100%
    );
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.donut-center {
    width: 108px;
    height: 108px;
    border-radius: 50%;
    background: white;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}
.donut-main-value { font-size: 24px; font-weight: 700; color: var(--text); }
.donut-main-label { font-size: 11px; color: var(--text-muted); }
.donut-legend { display: flex; flex-direction: column; gap: 10px; }
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.legend-dot {
    width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0;
}
.legend-value { font-weight: 700; color: var(--text); }
.legend-label { color: var(--text-secondary); }
<!-- Donut chart (max 3 segments) -->
<!-- Calculate conic-gradient percentages from the data values -->
<div class="donut-wrapper">
    <div class="donut">
        <div class="donut-center">
            <div class="donut-main-value">[Total]</div>
            <div class="donut-main-label">[Unit]</div>
        </div>
    </div>
    <div class="donut-legend">
        <div class="legend-item">
            <div class="legend-dot" style="background: var(--primary);"></div>
            <div>
                <span class="legend-value">[Value 1]</span>
                <span class="legend-label"> [Label 1]</span>
            </div>
        </div>
        <div class="legend-item">
            <div class="legend-dot" style="background: var(--success);"></div>
            <div>
                <span class="legend-value">[Value 2]</span>
                <span class="legend-label"> [Label 2]</span>
            </div>
        </div>
        <div class="legend-item">
            <div class="legend-dot" style="background: var(--border);"></div>
            <div>
                <span class="legend-value">[Value 3]</span>
                <span class="legend-label"> [Label 3]</span>
            </div>
        </div>
    </div>
</div>

3.5 Thumbnail Spec

The thumbnail is always generated by generate-thumbnail.js — it is NOT a user-selectable option and does NOT use an HTML template.

What generate-thumbnail.js produces:

  • 1200×630 PNG (EN): output/<slug>-thumbnail.png
  • 1200×630 PNG (ES): output/<slug>-thumbnail-es.png

Compositing layers (bottom to top):

  1. Stock photo background — featured-photo.jpg, resized to 1200×630 with Sharp fit: cover, position: centre
  2. Dark semi-transparent overlay box — 800×dynamic height, centered, fill #0e0d13 opacity 0.90, rx=6
  3. TSVC teal accent bar — width proportional to font size, centered horizontally, fill #00C9A7, height 4px, rx=2
  4. White uppercase tagline text — dynamically sized (NBS calcLayout algorithm), centered, Arial/Helvetica/sans-serif, bold
  5. TSVC logo — bottom-left corner, 180px wide, 28px padding

Tagline sources:

  • EN: output/<slug>/thumbnail-tagline.txt (written by write-article Stage 4.6)
  • ES: output/<slug>/thumbnail-tagline-es.txt (written by write-article Stage 4.6)
  • Fallback: if files are missing, the script derives a tagline from the slug

Run location: Script is copied to output/<slug>/images/ at Phase 3.1 setup time. Run from that directory:

cd output/<slug>/images && node generate-thumbnail.js <slug>

3.6 Spanish Variants

Immediately after generating each English HTML file, create a Spanish variant. Do this during template generation, not as a separate pass — the chart structure and data are already in context.

What to translate per chart:

| Element | Translate? | |---------|-----------| | <h1> chart title | Yes | | .subtitle source line | Yes (e.g. "Fuente: …") | | Chart labels (axis, legend, stat labels, timeline steps) | Yes | | .key-insight sentence | Yes | | Data values (numbers, %, $) | No — identical | | Footer URL www.thestartupvc.com | No | | TSVC logo | No | | HTML structure / CSS | No |

Process:

  1. Write <slug>-<type>-en.html
  2. Immediately write <slug>-<type>-es.html with translated text strings — same HTML structure, same data values, Spanish labels

Note: The thumbnail ES variant (<slug>-thumbnail-es.png) is handled automatically by generate-thumbnail.js — do not create a thumbnail HTML file.

Translation quality: Chart strings are 2–10 words each — short enough to translate accurately without full article context. Use natural Spanish business vocabulary (e.g., "Tasa de éxito" not "Porcentaje de triunfo").

After generating all charts, html/ should contain both -en.html and -es.html files. The screenshot.js will screenshot all of them in a single Playwright session.

3.7 Validate HTML

Run the validator before screenshotting. Fix all FAIL items before proceeding.

cd output/<slug>/images && node validate-html.js --html-dir html/

Common FAIL causes and fixes:

| FAIL | Fix | |------|-----| | Wrong FA path | Must be exactly ../node_modules/@fortawesome/fontawesome-free/css/all.min.css | | Emoji found | Replace with <i class="fa-solid fa-[icon]"></i> — see icon-reference in skill dir | | Missing .container class | Add class="container" to the outer div | | Body width not 600px | Ensure width: 600px on body in CSS |

3.7 Screenshot

cd output/<slug>/images && node screenshot.js

The screenshot.js script:

  • Reads all .html files from html/ subdirectory
  • Starts a local HTTP server (required for relative node_modules path to work)
  • Launches Chromium with WSL-safe flags (--no-sandbox, --disable-setuid-sandbox)
  • Screenshots only the .container element (not full page)
  • Saves PNGs to output/ subdirectory
  • Filenames match: <slug>-bars-en.html<slug>-bars-en.png

Verify each PNG was created:

ls output/<slug>/images/output/

If a PNG is missing, check the console output from screenshot.js for errors.


Phase 4: Article Insertion

Requirement: VIZ-04

4.1 Insert Chart References into 04-final.md

PNG list: Use the PNG filenames returned by the Phase 3 subagent. Do not re-scan the output directory — use the reported list.

For each generated chart (NOT the thumbnail), insert an image reference into output/<slug>/04-final.md.

Where to insert: After the first paragraph under the relevant H2 section identified in Phase 2.

Format:

![Alt text describing the visualization](images/output/<slug>-<type>-en.png)
*Caption: one sentence under 15 words*

Alt text rules:

  • Descriptive — describes what the chart shows
  • Not keyword-stuffed
  • Examples: "Bar chart comparing operating costs by country in Latin America" / "Stats dashboard showing Latin America VC funding metrics in 2024"

Caption rules:

  • Factual and concise
  • Under 15 words
  • Describes what the image shows (not marketing copy)
  • Examples: "Colombia's operating costs are the lowest among major Latin American markets." / "Latin American VC deal volume reached $4.2 billion in 2024."

Process for each chart:

  1. Read output/<slug>/04-final.md
  2. Find the H2 section identified in Phase 2 for this chart
  3. Find the end of the first paragraph under that H2 (first blank line after the opening paragraph text)
  4. Insert the markdown image block + caption on a new line after that paragraph
  5. Use the Edit tool to make the change

4.2 Write metadata.json

Write output/<slug>/images/metadata.json after all insertions complete:

{
  "thumbnail": "images/output/<slug>-thumbnail.png",
  "charts_en": [
    "images/output/<slug>-bars-en.png"
  ],
  "charts_es": [
    "images/output/<slug>-bars-es.png"
  ]
}

List all generated chart PNGs separated by language. The thumbnail field uses the EN PNG path. The ES thumbnail (<slug>-thumbnail-es.png) is uploaded by upload-es.js automatically. This file is used by Phase 4 (WordPress/WPML upload) to identify all images — charts_en for the English post, charts_es for the Spanish WPML translation.

4.3 Confirm Completion

Report:

[N] charts generated, 1 thumbnail generated, [N] image references inserted into 04-final.md.

Generated files:
- output/<slug>/images/output/<slug>-thumbnail.png
- output/<slug>/images/output/<slug>-bars-en.png
[... list all PNGs ...]

Image references inserted:
- After "[H2 section heading]" in 04-final.md — <slug>-bars-en.png
[... list all insertions ...]

Important Implementation Notes

Node.js Tooling Location

The screenshot.js, validate-html.js, and package.json source files live in .claude/skills/create-visualizations/. They are copied into output/<slug>/images/ on first use (see Phase 3.1). This means:

  • Tooling changes are made once in the skill directory
  • Each article gets its own isolated copy
  • node_modules and Chromium binary are per-article (under output/<slug>/images/node_modules/)

WSL Playwright Requirements

Always pass these flags in chromium.launch():

args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--disable-dev-shm-usage']

Without these, Chromium crashes immediately in WSL2 with SIGTRAP or BUS_ADRERR.

Local HTTP Server Requirement

The HTML files reference ../node_modules/@fortawesome/fontawesome-free/css/all.min.css. This relative path only resolves correctly when served via HTTP, not via file:// URLs. The screenshot.js starts a local HTTP server and serves output/<slug>/images/ as the root. The HTML files are accessed at http://127.0.0.1:[port]/html/<file>.html.

One HTML File Per Chart Type

Do NOT create a parameterized template. Create separate HTML files per visualization. This makes validation and debugging simpler, and matches the existing skill pattern.


Icon Reference

Use <i class="fa-solid fa-[name]"></i> format. Common icons for TSVC charts:

| Concept | Icon | |---------|------| | Cost / money | fa-dollar-sign | | Growth / trend | fa-arrow-trend-up | | Market / chart | fa-chart-line | | Investment | fa-coins | | Company | fa-building | | Startup | fa-rocket | | People / team | fa-users | | Country / location | fa-location-dot | | Globe | fa-earth-americas | | Calendar / timeline | fa-calendar-days | | Percent / rate | fa-percent | | Award | fa-award |

For the full icon list, see .claude/skills/create-visualizations/references/icon-reference.md.


Pitfalls to Avoid

  1. Using old brand colors#28DF8D is wrong. Use #00C9A7 as --primary.
  2. Emoji in HTML — Use Font Awesome icons. Emoji render as blank squares in headless Chrome.
  3. External CDN — No Google Fonts, no CDN Font Awesome. All assets must be local.
  4. Full-page screenshot — Use container.screenshot() not page.screenshot({ fullPage: true }).
  5. networkidle0 — Playwright uses 'networkidle' (not 'networkidle0' which is puppeteer).
  6. Data from article prose — Always pull numbers from 02-research/ files, not from 04-final.md.
  7. Exceeding density limits — Split instead of shrink. Max 5 bars, 3 cards, 4 stats, 5 timeline, 3 donut.
  8. Thumbnail as an option — The thumbnail is always generated. Do not list it in the Phase 2 options.
  9. Skipping validation — Run validate-html.js before screenshot.js. Fix all FAIL items first.
  10. Wrong FA icon names — Use FA7 canonical names (e.g. fa-location-dot not fa-map-marker-alt).
Related skills