| name | data-visualization |
| description | Data visualization implementation: chart type selection framework (when to use bar/line/scatter/pie/heatmap/treemap), D3.js patterns, Recharts/Chart.js/Victory integration, accessible charts (ARIA roles, color-blind safe palettes), responsive SVG patterns, and performance for large datasets. Use when implementing any chart or graph. |
Data Visualization Skill
When to Activate
- Implementing any chart, graph, or data visualization component
- Choosing the right chart type for a dataset
- Adding accessibility (ARIA, color-blind-safe palettes) to existing charts
- Optimizing chart performance for large datasets
- Making SVG charts responsive
- Deciding between SVG and Canvas rendering based on data volume and interaction needs
- Selecting a React chart library (Recharts vs. Victory vs. Chart.js vs. D3) for a new feature
Chart Type Selection
Decision table
| Goal | Chart Type | When NOT to use |
|---|
| Compare values across categories | Bar chart (vertical) | More than ~15 categories |
| Compare many categories | Horizontal bar | — |
| Show trend over time | Line chart | Non-continuous data |
| Show distribution | Histogram / Box plot | Fewer than 30 data points |
| Show correlation between two variables | Scatter plot | — |
| Show part-to-whole (≤5 segments) | Pie / Donut chart | More than 5 segments → use stacked bar |
| Show part-to-whole with many segments | Stacked bar chart | — |
| Show hierarchical data | Treemap / Sunburst | More than 3 levels deep |
| Show density across two dimensions | Heatmap | — |
| Show geographic data | Choropleth map | Non-geographic comparisons |
Anti-patterns
- 3D charts — distort perception of value; never use
- Pie with >5 segments — use horizontal bar instead
- Dual Y-axis — misleads; prefer two separate charts
- Truncated Y-axis — starting Y above 0 exaggerates differences; only acceptable for line charts showing trend
D3.js Patterns
Data Join (enter/update/exit)
import * as d3 from 'd3';
const svg = d3.select('#chart').append('svg').attr('width', width).attr('height', height);
const bars = svg.selectAll('rect').data(data, d => d.id);
bars.enter()
.append('rect')
.attr('x', d => xScale(d.category))
.attr('y', d => yScale(d.value))
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value))
.attr('fill', '#4f46e5');
bars
.attr('y', => (d.))
.(, height - (d.));
bars.().();
Scale types
const xScale = d3.scaleLinear().domain([0, maxValue]).range([0, width]);
const xScale = d3.scaleTime().domain([startDate, endDate]).range([0, width]);
const xScale = d3.scaleBand()
.domain(categories)
.range([0, width])
.padding(0.1);
const colorScale = d3.scaleOrdinal()
.domain(categories)
.range(d3.schemeTableau10);
Axis setup
const xAxis = d3.axisBottom(xScale).ticks(5).tickFormat(d3.format('.2s'));
const yAxis = d3.axisLeft(yScale).tickFormat(d => `${d}%`);
svg.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
svg.append('g').attr('class', 'y-axis').call(yAxis);
Responsive SVG (viewBox)
const svg = d3.select('#chart')
.append('svg')
.attr('viewBox', `0 0 ${width} ${height}`)
.attr('preserveAspectRatio', 'xMidYMid meet')
.style('width', '100%')
.style('height', 'auto');
Transitions
bars.transition()
.duration(300)
.ease(d3.easeQuadOut)
.attr('height', d => height - yScale(d.value))
.attr('y', d => yScale(d.value));
React Chart Libraries
| Library | Best for | Trade-offs |
|---|
| Recharts | Dashboards, simple charts, React-native API | Less customizable than D3 |
| Victory | Animated charts, React Native support | Larger bundle |
| Chart.js + react-chartjs-2 | Canvas-based, performance with many points | Canvas rendering |
| Observable Plot | Data exploration, R-style API | Not production-component-focused |
| D3 | Custom charts, complex interactions | High learning curve |
Recharts — minimal bar chart
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
function SalesChart({ data }: { data: { month: string; sales: number }[] }) {
return (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data} margin={{ top: 8, right: 16, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis tickFormatter={(v) => `$${v.toLocaleString()}`} />
<Tooltip formatter={(v: number) => [`$${v.toLocaleString()}`, 'Sales']} />
<Bar dataKey="sales" = = , , ]} />
);
}
Accessibility
ARIA on SVG
<svg
role="img"
aria-label="Monthly sales from January to December 2024, showing 40% growth"
viewBox="0 0 800 400"
>
{}
<title>Monthly Sales 2024</title>
<desc>Bar chart showing monthly sales revenue. January: $12,000. February: $15,000...</desc>
{}
</svg>
Color-blind-safe palettes
Avoid encoding information in red/green alone. Use these tested palettes:
const OKABE_ITO = [
'#E69F00',
'#56B4E9',
'#009E73',
'#F0E442',
'#0072B2',
'#D55E00',
'#CC79A7',
'#000000',
];
const scale = d3.scaleSequential(d3.interpolateViridis).domain([0, maxValue]);
Texture for critical distinctions
When color alone cannot be relied on, add patterns:
<defs>
<pattern id="hatch" patternUnits="userSpaceOnUse" width="4" height="4">
<path d="M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2" stroke="#4f46e5" strokeWidth="1" />
</pattern>
</defs>
<rect fill="url(#hatch)" />
Responsive Charts
ResizeObserver pattern
import { useEffect, useRef, useState } from 'react';
function useChartDimensions(ref: React.RefObject<HTMLDivElement>) {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (!ref.current) return;
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect;
setDimensions({ width, height });
});
observer.observe(ref.current);
return () => observer.disconnect();
}, [ref]);
return dimensions;
}
Mobile breakpoints for axis labels
const tickAngle = width < 400 ? -45 : 0;
const textAnchor = width < 400 ? 'end' : 'middle';
Performance for Large Datasets
Canvas vs. SVG threshold
| Dataset size | Recommendation |
|---|
| < 500 points | SVG (better accessibility, hover events) |
| 500–5000 points | SVG with path aggregation or simplification |
| > 5000 points | Canvas rendering (Chart.js canvas, PixiJS) |
Aggregation on server
Do not render 100,000 points — aggregate first:
const binned = d3.bin()
.domain(xScale.domain())
.thresholds(200)(data.map(d => d.value));
Debounced resize
import { useMemo } from 'react';
import { debounce } from 'lodash-es';
const debouncedResize = useMemo(
() => debounce(setDimensions, 100),
[]
);
Checklist