skills/data-viz-2025/SKILL.md
State-of-the-art data visualization for React/Next.js/TypeScript with Tailwind CSS. Creates compelling, tested, and accessible visualizations following Tufte principles and NYT Graphics standards. Activate on "data viz", "chart", "graph", "visualization", "dashboard", "plot", "Recharts", "Nivo", "D3". NOT for static images, print graphics, or basic HTML tables.
npx skillsauth add curiositech/windags-skills data-viz-2025Install this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Create visualizations that marry NYT Graphics rigor with modern web performance and accessibility standards.
What type of chart are you building?
├─ Exploratory analysis / rapid prototyping
│ ├─ Data size < 5K points → Observable Plot
│ └─ Data size > 5K points → D3.js with sampling
│
├─ Standard business charts (bars, lines, areas)
│ ├─ Need it fast & simple → Recharts
│ ├─ Premium aesthetics required → Nivo
│ └─ Custom interactions needed → Visx
│
├─ Complex/novel visualizations
│ ├─ React component structure preferred → Visx
│ └─ Maximum control needed → D3.js
│
└─ Dashboard with many charts
├─ Tailwind design system → Tremor
└─ Consistent theming → Nivo
How many data points?
├─ < 1K points
│ ├─ Need crisp scaling → SVG rendering
│ └─ Need interactivity → SVG with event handlers
│
├─ 1K - 10K points
│ ├─ Simple shapes → SVG (acceptable)
│ ├─ Complex animations → Canvas
│ └─ Mobile performance critical → Canvas
│
└─ > 10K points
├─ Static display → Canvas with aggregation
├─ Interactive exploration → WebGL (via deck.gl)
└─ Real-time updates → Canvas with data sampling
Is this for production use?
├─ Yes → Must implement ALL accessibility features
│ ├─ Keyboard navigation for all interactive elements
│ ├─ Screen reader support with data tables
│ ├─ Color-blind safe palettes (test with simulators)
│ └─ Respect prefers-reduced-motion
│
└─ Internal tool/prototype → Implement core features
├─ Alt text for chart images
├─ Sufficient color contrast (4.5:1 minimum)
└─ Keyboard access for primary interactions
What's the screen breakpoint?
├─ Mobile (< 640px)
│ ├─ Many data points → Aggregate to top 5-7 items
│ ├─ Time series → Show last 30 days, add "View All" button
│ ├─ Multi-series → Use small multiples instead of overlays
│ └─ Complex legends → Replace with direct labels
│
├─ Tablet (640px - 1024px)
│ ├─ Reduce axis labels by 50%
│ ├─ Simplify gridlines (remove minor ticks)
│ └─ Increase touch targets to 44px minimum
│
└─ Desktop (> 1024px)
└─ Show full detail, all interactions enabled
Symptom: Chart has 8+ colors, tiny legend, impossible to distinguish series Diagnosis: Trying to show too many categories simultaneously Fix:
Symptom: Bar chart with Y-axis starting at 95 instead of 0, making 2% difference look like 200% Diagnosis: Truncated axes exaggerating small differences Fix:
Symptom: Desktop chart squished to mobile width, text unreadable, interactions broken Diagnosis: No responsive design strategy, one-size-fits-all approach Fix:
Symptom: White screen for 3+ seconds, then chart appears suddenly Diagnosis: No skeleton loading, blocking data fetch Fix:
Symptom: Screen reader announces "image" with no context, keyboard navigation broken Diagnosis: Visual-only design, no programmatic access to data Fix:
Scenario: Executive dashboard showing revenue by 8 product lines over 12 months, needs to work on mobile.
Decision Process:
Desktop Implementation:
<LineChart data={monthlyData} width="100%" height={400}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Legend />
{products.map((product, index) => (
<Line
key={product}
dataKey={product}
stroke={colors[index]}
strokeWidth={2}
/>
))}
</LineChart>
Mobile Adaptation:
const isMobile = useMediaQuery('(max-width: 640px)');
const topProducts = isMobile
? products.slice(0, 3) // Show only top 3
: products;
<ResponsiveContainer height={isMobile ? 250 : 400}>
<LineChart data={monthlyData}>
<XAxis
dataKey="month"
interval={isMobile ? 2 : 0} // Every 3rd month on mobile
tick={{ fontSize: isMobile ? 10 : 12 }}
/>
<YAxis tick={isMobile ? false : true} />
<Tooltip />
{!isMobile && <Legend />}
{topProducts.map((product, index) => (
<Line
key={product}
dataKey={product}
stroke={colors[index]}
strokeWidth={isMobile ? 3 : 2} // Thicker lines for mobile
/>
))}
</LineChart>
</ResponsiveContainer>
Key Decisions Made:
Scenario: User journey flow with 50K user sessions, 12 touchpoints, needs real-time updates.
Decision Process:
Implementation Strategy:
// Data aggregation first - group similar paths
const aggregatedFlows = useMemo(() => {
return rawUserJourney
.reduce((acc, session) => {
const pathKey = session.path.join('→');
acc[pathKey] = (acc[pathKey] || 0) + session.count;
return acc;
}, {})
// Keep only paths with 100+ users
.filter(([path, count]) => count >= 100);
}, [rawUserJourney]);
// Canvas rendering for performance
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
// Clear and render
ctx.clearRect(0, 0, width, height);
// Use Web Workers for heavy calculations
const worker = new Worker('/sankeyWorker.js');
worker.postMessage({ flows: aggregatedFlows, dimensions: { width, height } });
worker.onmessage = (e) => {
const { nodes, links } = e.data;
drawSankey(ctx, nodes, links);
};
}, [aggregatedFlows]);
Performance Optimizations Applied:
Before shipping any data visualization, verify ALL conditions:
This skill should NOT be used for:
<table> markup with CSS stylingWhen to delegate to other skills:
Library-specific boundaries:
tools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.