D3 Visualization Patterns¶
This guide covers the "D3 Rosetta" pattern used in the FCC React frontend, where React owns the DOM container and D3 renders inside it. It includes component recipes for the 7 built-in visualization types, color scheme reference, accessibility considerations, and testing strategies.
Table of Contents¶
- D3 Rosetta Pattern
- Component Template
- Shared Utilities: d3-utils.ts
- Component Recipes
- Color Schemes
- Accessibility
- Dark Mode Support
- Responsive Design
- Testing
- Related Documentation
D3 Rosetta Pattern¶
The FCC frontend follows the "D3 Rosetta" pattern, which cleanly separates React's responsibilities from D3's:
- React manages the component lifecycle, props, state, and owns the SVG
container element via
useRef. - D3 handles all rendering inside the SVG element -- selections, scales, axes, transitions, and layout algorithms.
- Data flows one way: React passes data down as props; D3 renders it in a
useEffectthat depends on those props.
This approach avoids the common pitfall of React and D3 fighting over DOM ownership.
Key rules¶
- React creates the
<svg>element; D3 never callsdocument.createElement. - D3 renders inside the SVG using
d3.select(svgRef.current). - On every data change, D3 clears and re-renders (or uses enter/update/exit).
- The
useEffectcleanup function stops any running simulations or timers.
Module locations¶
All visualization components live under frontend/src/visualizations/:
frontend/src/visualizations/
d3-utils.ts -- shared color scales, tooltips, responsive SVG
ForceGraph.tsx -- force-directed persona interaction graph
SankeyWorkflow.tsx -- Sankey diagram for workflow flows
VoronoiPersonaMap.tsx -- Voronoi tessellation persona map
ChordDiagram.tsx -- chord diagram for cross-references
HyperEdgeGraph.tsx -- hyperedge graph for multi-party interactions
ProvenanceFlow.tsx -- provenance tracking flow diagram
HeatmapMatrix.tsx -- heatmap matrix for dimension/score data
NanoCubeExplorer.tsx -- NanoCube data exploration visualization
Component Template¶
Every D3 visualization component follows this TypeScript template:
import { useRef, useEffect } from 'react';
import * as d3 from 'd3';
interface MyVizProps {
data: DataItem[];
width?: number;
height?: number;
}
const MyViz: React.FC<MyVizProps> = ({
data,
width = 800,
height = 600,
}) => {
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
if (!svgRef.current || data.length === 0) return;
// 1. Select and clear the SVG
const svg = d3.select(svgRef.current);
svg.selectAll('*').remove();
// 2. Set up scales, layouts, etc.
const xScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value) ?? 0])
.range([0, width]);
// 3. Binddata and render
svg.selectAll('rect')
.data(data)
.join('rect')
.attr('x', 0)
.attr('y', (_, i) => i * 25)
.attr('width', d => xScale(d.value))
.attr('height', 20)
.attr('fill', '#1976D2');
// 4. Cleanup (for simulations, timers, etc.)
return () => {
// Stop simulations, cancel timers
};
}, [data, width, height]);
// 5. Empty state
if (data.length === 0) {
return (
<div
className="flex items-center justify-center bg-gray-50
dark:bg-gray-800 rounded-lg"
style={{ width, height }}
data-testid="my-viz-empty"
>
<p className="text-gray-400">No data available</p>
</div>
);
}
// 6. Render SVG container
return (
<div className="relative" data-testid="my-viz">
<svg
ref={svgRef}
width={width}
height={height}
className="bg-white dark:bg-gray-900 rounded-lg
border border-gray-200 dark:border-gray-700"
role="img"
aria-label="My visualization description"
/>
</div>
);
};
export default MyViz;
Shared Utilities: d3-utils.ts¶
The frontend/src/visualizations/d3-utils.ts module provides four shared
utilities used across all visualization components.
FCC_COLORS¶
A Record<string, string> mapping FCC phases and persona categories to
hex colors:
import { FCC_COLORS } from './d3-utils';
FCC_COLORS.find; // '#1E88E5'
FCC_COLORS.create; // '#43A047'
FCC_COLORS.critique; // '#E53935'
FCC_COLORS.core; // '#607D8B'
FCC_COLORS.governance; // '#7B1FA2'
FCC_COLORS.data_engineering; // '#6D4C41'
createColorScale¶
Creates a D3 ordinal color scale from category names, using FCC_COLORS with
a d3.schemeCategory10 fallback for unknown categories:
import { createColorScale } from './d3-utils';
const categories = ['core', 'integration', 'governance'];
const scale = createColorScale(categories);
scale('core'); // '#607D8B'
scale('unknown'); // falls back to schemeCategory10
createTooltip¶
Creates a positioned HTML tooltip attached to a container element:
import { createTooltip } from './d3-utils';
const container = svgRef.current!.parentElement!;
const tip = createTooltip(container);
// Show tooltip near the mouse
tip.show(event.offsetX, event.offsetY, '<strong>RC</strong><br/>Core');
// Update position on mousemove
tip.update(event.offsetX, event.offsetY, content);
// Hide on mouseout
tip.hide();
responsiveSvg¶
Creates a responsive SVG element with viewBox and preserveAspectRatio:
import { responsiveSvg } from './d3-utils';
const svg = responsiveSvg(container, 800, 600);
// svg has viewBox="0 0 800 600" and preserveAspectRatio="xMidYMid meet"
getFccColor and lightenColor¶
import { getFccColor, lightenColor } from './d3-utils';
getFccColor('governance'); // '#7B1FA2'
getFccColor('unknown', '#cccccc'); // '#cccccc' (fallback)
lightenColor('#1976D2', 0.3); // lighter variant for fills
Component Recipes¶
1. ForceGraph¶
File: frontend/src/visualizations/ForceGraph.tsx
Force-directed graph showing persona interactions. Nodes represent personas; edges represent cross-reference relationships.
import ForceGraph from './visualizations/ForceGraph';
<ForceGraph
nodes={[
{ id: 'RC', name: 'Research Coordinator', category: 'core', phase: 'find' },
{ id: 'BC', name: 'Build Champion', category: 'core', phase: 'create' },
]}
links={[
{ source: 'RC', target: 'BC', type: 'upstream', strength: 'primary' },
]}
width={800}
height={600}
/>
Features: zoom/pan, drag nodes, arrow markers, category-colored nodes, hover tooltips, category legend overlay.
2. SankeyWorkflow¶
File: frontend/src/visualizations/SankeyWorkflow.tsx
Sankey diagram showing flow volumes between workflow nodes (Find, Create, Critique phases).
3. VoronoiPersonaMap¶
File: frontend/src/visualizations/VoronoiPersonaMap.tsx
Voronoi tessellation where each cell represents a persona, sized by a metric (e.g., action count, dimension score).
4. ChordDiagram¶
File: frontend/src/visualizations/ChordDiagram.tsx
Chord diagram showing bilateral interaction strengths between persona categories from the cross-reference matrix.
5. HyperEdgeGraph¶
File: frontend/src/visualizations/HyperEdgeGraph.tsx
Hyperedge graph for visualizing multi-party interactions where a single relationship connects more than two personas.
6. ProvenanceFlow¶
File: frontend/src/visualizations/ProvenanceFlow.tsx
Flow diagram tracking the provenance of artifacts through the FCC workflow, showing which persona produced or modified each deliverable.
7. HeatmapMatrix¶
File: frontend/src/visualizations/HeatmapMatrix.tsx
Grid heatmap for displaying dimension scores, discernment ratings, or synergy matrices. Rows and columns can represent personas, dimensions, or projects.
Color Schemes¶
Primary Palettes¶
Two color maps are available, one on the frontend and one on the backend:
Frontend (d3-utils.ts -- FCC_COLORS):
| Key | Hex | Usage |
|---|---|---|
find |
#1E88E5 |
Find phase |
create |
#43A047 |
Create phase |
critique |
#E53935 |
Critique phase |
build |
#FB8C00 |
Build phase |
ops |
#8E24AA |
Operations phase |
core |
#607D8B |
Core category |
integration |
#00ACC1 |
Integration category |
governance |
#7B1FA2 |
Governance category |
stakeholder |
#F4511E |
Stakeholder category |
champion |
#FFB300 |
Champion category |
Backend Streamlit (apps/streamlit/_shared.py -- FCC_COLORS and
CATEGORY_COLORS):
The backend color maps follow the same structure with slightly different hex
values optimized for Plotly charts. Use CATEGORY_COLORS for per-category
coloring across all 17+ categories.
Color-Blind Friendly Alternatives¶
For accessibility, consider supplementing colors with patterns or shapes.
The d3.schemeCategory10 fallback uses colors that are reasonably
distinguishable for common forms of color vision deficiency.
For critical visualizations, use the Viridis or Cividis sequential scales:
Accessibility¶
ARIA Labels¶
Every SVG container should have role="img" and an aria-label attribute
describing the visualization:
<svg
ref={svgRef}
role="img"
aria-label="Force-directed graph showing persona interactions across 5 categories"
/>
Keyboard Navigation¶
Add keyboard support for interactive elements:
nodeGroup
.attr('tabindex', 0)
.attr('role', 'button')
.attr('aria-label', (d: SimNode) => `${d.name}, category ${d.category}`)
.on('keydown', (event: KeyboardEvent, d: SimNode) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleNodeSelect(d);
}
});
Screen Reader Support¶
Add visually hidden text summarizing the data:
<div className="sr-only">
Graph showing {nodes.length} personas connected by {links.length} relationships
across {categories.length} categories.
</div>
Color-Blind Friendly Palettes¶
Supplement color coding with secondary visual channels:
// Use different shapes per category
const symbolScale = d3.scaleOrdinal<string, d3.SymbolType>()
.domain(categories)
.range([
d3.symbolCircle,
d3.symbolSquare,
d3.symbolTriangle,
d3.symbolDiamond,
d3.symbolStar,
d3.symbolCross,
]);
// Or use patterns
nodeGroup
.attr('fill', (d) => colorScale(d.category))
.attr('stroke-dasharray', (d) =>
d.category === 'champion' ? '4,2' : 'none'
);
Dark Mode Support¶
Detecting Theme¶
The FCC frontend uses Tailwind CSS dark mode. Detect the current theme in D3 code:
const isDark = document.documentElement.classList.contains('dark');
const textColor = isDark ? '#e5e7eb' : '#374151';
const gridColor = isDark ? '#374151' : '#e5e7eb';
const bgColor = isDark ? '#111827' : '#ffffff';
Adjusting Colors¶
// Lighten or darken based on theme
import { lightenColor } from './d3-utils';
const fillColor = isDark
? lightenColor(getFccColor(category), 0.5)
: getFccColor(category);
Tailwind Integration¶
Use Tailwind's dark: prefix on the container and legend elements:
<svg
ref={svgRef}
className="bg-white dark:bg-gray-900 rounded-lg
border border-gray-200 dark:border-gray-700"
/>
ThemeToggle Component¶
The frontend/src/components/ThemeToggle.tsx component handles theme
switching. Visualizations should re-render when the theme changes by
including a theme prop or using a React context:
const { theme } = useTheme();
useEffect(() => {
// Re-render D3 content when theme changes
}, [data, width, height, theme]);
Responsive Design¶
ResizeObserver¶
Use ResizeObserver to dynamically resize visualizations:
useEffect(() => {
const container = svgRef.current?.parentElement;
if (!container) return;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width: w, height: h } = entry.contentRect;
setDimensions({ width: w, height: h });
}
});
observer.observe(container);
return () => observer.disconnect();
}, []);
viewBox and preserveAspectRatio¶
All SVGs should use viewBox for intrinsic responsive scaling:
<svg
ref={svgRef}
viewBox={`0 0 ${width} ${height}`}
preserveAspectRatio="xMidYMid meet"
style={{ maxWidth: '100%', height: 'auto' }}
/>
The responsiveSvg utility in d3-utils.ts sets these attributes
automatically.
Mobile Considerations¶
- Hide legends and controls on small screens or move them to a collapsible panel.
- Increase touch target sizes for interactive elements (minimum 44x44px).
- Disable zoom/pan on very small viewports where gestures conflict with scrolling.
Testing¶
Jest Snapshots¶
Each visualization component has a corresponding test file in
frontend/__tests__/visualizations/. Snapshot tests verify that the
component renders the expected DOM structure:
import { render } from '@testing-library/react';
import ForceGraph from '../../src/visualizations/ForceGraph';
test('renders empty state when no nodes', () => {
const { getByTestId } = render(
<ForceGraph nodes={[]} links={[]} />
);
expect(getByTestId('force-graph-empty')).toBeInTheDocument();
});
test('renders SVG when nodes provided', () => {
const nodes = [
{ id: 'RC', name: 'Research Coordinator', category: 'core', phase: 'find' },
];
const { getByTestId } = render(
<ForceGraph nodes={nodes} links={[]} />
);
expect(getByTestId('force-graph')).toBeInTheDocument();
});
Mock D3 Selections¶
For unit testing D3 logic in isolation, mock the D3 selection API:
jest.mock('d3', () => ({
...jest.requireActual('d3'),
select: jest.fn().mockReturnValue({
selectAll: jest.fn().mockReturnThis(),
remove: jest.fn().mockReturnThis(),
append: jest.fn().mockReturnThis(),
attr: jest.fn().mockReturnThis(),
data: jest.fn().mockReturnThis(),
join: jest.fn().mockReturnThis(),
}),
}));
d3-utils Tests¶
The shared utilities have their own test file at
frontend/__tests__/visualizations/d3-utils.test.ts, covering color scale
creation, tooltip lifecycle, and responsive SVG attributes.
Related Documentation¶
- WebSocket Architecture -- event delivery to the frontend
- Protocol Bridge Patterns -- backend event sources
- Extending A2A Agent Cards -- data model for persona nodes
- Custom MCP Tools -- MCP integration
- Understanding the FCC Ecosystem -- ecosystem context