Skip to content

Visualization Cookbook

Recipes for building and integrating the seven D3 visualizations in the FCC React frontend. Each recipe follows the same structure: data shape, props interface, integration example, and accessibility notes.


Table of Contents

  1. The D3 Rosetta Pattern
  2. ForceGraph Recipe
  3. SankeyWorkflow Recipe
  4. VoronoiPersonaMap Recipe
  5. ChordDiagram Recipe
  6. HyperEdgeGraph Recipe
  7. ProvenanceFlow Recipe
  8. HeatmapMatrix Recipe
  9. Responsive Sizing Pattern
  10. Dark Mode Support
  11. Accessibility

The D3 Rosetta Pattern

Every FCC visualization follows the same architectural pattern:

  • React owns the container. The component renders an <svg ref={svgRef}> element and manages the component lifecycle.
  • D3 renders inside useRef. A useEffect hook clears the SVG and rebuilds the visualization whenever props change.
  • Shared utilities. All visualizations import from d3-utils.ts for consistent color scales, tooltips, and responsive SVG setup.
import { useRef, useEffect } from 'react'
import * as d3 from 'd3'
import { createColorScale, createTooltip, responsiveSvg } from './d3-utils'

interface MyVizProps {
  data: MyDataType[]
  width?: number
  height?: number
}

export default function MyVisualization({ data, width = 800, height = 600 }: MyVizProps) {
  const svgRef = useRef<SVGSVGElement>(null)

  useEffect(() => {
    if (!svgRef.current || data.length === 0) return

    const svg = d3.select(svgRef.current)
    svg.selectAll('*').remove()  // Clear previous render

    const container = svgRef.current.parentElement
    const tip = container ? createTooltip(container) : null

    // ... D3 rendering logic ...

    return () => { tip?.hide() }
  }, [data, width, height])

  return (
    <svg
      ref={svgRef}
      width={width}
      height={height}
      viewBox={`0 0 ${width} ${height}`}
      role="img"
      aria-label="My Visualization"
    />
  )
}

Shared Utilities in d3-utils.ts

Utility Purpose
FCC_COLORS Record mapping phases and categories to hex colors
createColorScale(categories) D3 ordinal scale from category names to FCC colors
createTooltip(container) Tooltip with show, hide, update methods
responsiveSvg(container, w, h) Creates a responsive SVG with viewBox
getFccColor(key, fallback?) Look up a single FCC color by phase or category
lightenColor(hex, amount?) Compute a lighter variant for fills

ForceGraph Recipe

Purpose: Render a force-directed network of persona relationships.

Data Shape

import { ForceNode, ForceLink } from '../visualizations/ForceGraph'

// Nodes: one per persona
const nodes: ForceNode[] = [
  { id: 'RC', name: 'Research Catalyst', category: 'core', phase: 'find' },
  { id: 'SA', name: 'Solution Architect', category: 'core', phase: 'create' },
  { id: 'QR', name: 'Quality Reviewer', category: 'core', phase: 'critique' },
]

// Links: cross-reference relationships
const links: ForceLink[] = [
  { source: 'RC', target: 'SA', type: 'handoff', strength: 'primary' },
  { source: 'SA', target: 'QR', type: 'handoff', strength: 'primary' },
  { source: 'QR', target: 'RC', type: 'feedback', strength: 'secondary' },
]

Props Interface

interface ForceGraphProps {
  nodes: ForceNode[]
  links: ForceLink[]
  width?: number   // default: 800
  height?: number  // default: 600
}

Integration Example

import ForceGraph from '../visualizations/ForceGraph'

function PersonaNetwork() {
  const { personas } = usePersonaRegistry()

  const nodes = personas.map(p => ({
    id: p.id,
    name: p.name,
    category: p.category,
    phase: p.phase,
  }))

  // Build links from cross-reference data
  const links = buildCrossReferenceLinks(personas)

  return <ForceGraph nodes={nodes} links={links} width={1000} height={700} />
}

Key Configuration

  • Collision radius: Prevents node overlap via d3.forceCollide()
  • Category color mapping: Uses createColorScale with FCC_COLORS fallback
  • Link strength: primary = 0.7, secondary = 0.4, tertiary = 0.2
  • Link distance: 100px default

Accessibility Notes

  • Add role="img" and aria-label to the SVG element
  • Tooltip content is surfaced on hover and focus
  • Nodes are keyboard-focusable when the SVG receives focus

SankeyWorkflow Recipe

Purpose: Render workflow transitions as a Sankey diagram with FCC phase columns.

Data Shape

import { SankeyNode, SankeyLink } from '../visualizations/SankeyWorkflow'

const nodes: SankeyNode[] = [
  { id: 'research', name: 'Research', phase: 'Find' },
  { id: 'design', name: 'Design', phase: 'Create' },
  { id: 'review', name: 'Review', phase: 'Critique' },
  { id: 'build', name: 'Build', phase: 'Build' },
  { id: 'deploy', name: 'Deploy', phase: 'Ops' },
]

const links: SankeyLink[] = [
  { source: 'research', target: 'design', value: 3 },
  { source: 'design', target: 'review', value: 3 },
  { source: 'review', target: 'build', value: 2 },
  { source: 'review', target: 'research', value: 1 },  // feedback loop
  { source: 'build', target: 'deploy', value: 2 },
]

Props Interface

interface SankeyWorkflowProps {
  nodes: SankeyNode[]
  links: SankeyLink[]
  width?: number   // default: 800
  height?: number  // default: 600
}

Integration Example

import SankeyWorkflow from '../visualizations/SankeyWorkflow'

function WorkflowView({ graphName }: { graphName: string }) {
  const { nodes, links } = useWorkflowGraph(graphName)
  return <SankeyWorkflow nodes={nodes} links={links} />
}

Phase Column Order

The layout function uses this predefined order:

const phaseOrder = ['Find', 'Create', 'Critique', 'Build', 'Ops']

Nodes without a recognized phase default to the "Find" column.

FCC_COLORS for Phases

Phase Color Hex
Find Blue #1E88E5
Create Green #43A047
Critique Red #E53935
Build Orange #FB8C00
Ops Purple #8E24AA

Accessibility Notes

  • Curved link paths include aria-label attributes describing the transition
  • Phase column headers act as group labels
  • Tooltip shows node name and flow value

VoronoiPersonaMap Recipe

Purpose: Render Voronoi tessellation of personas clustered by category.

Data Shape

import { VoronoiPersona } from '../visualizations/VoronoiPersonaMap'

const personas: VoronoiPersona[] = [
  { id: 'RC', name: 'Research Catalyst', category: 'core' },
  { id: 'SIO', name: 'Systems Integration Officer', category: 'integration' },
  { id: 'EGO', name: 'Ethics & Governance Officer', category: 'governance' },
]

Props Interface

interface VoronoiPersonaMapProps {
  personas: VoronoiPersona[]
  width?: number   // default: 800
  height?: number  // default: 600
}

Integration Example

import VoronoiPersonaMap from '../visualizations/VoronoiPersonaMap'

function TerritoryView() {
  const { personas } = usePersonaRegistry()
  const voronoiData = personas.map(p => ({
    id: p.id,
    name: p.name,
    category: p.category,
  }))
  return <VoronoiPersonaMap personas={voronoiData} />
}

Layout Algorithm

  1. Categories are distributed angularly around the center (equal spacing).
  2. A force simulation nudges personas toward their category centroid with forceX and forceY (strength 0.3).
  3. forceCollide with radius 12 prevents overlap.
  4. 120 simulation ticks run synchronously for deterministic output.
  5. d3.Delaunay computes Voronoi cells from final positions.

Accessibility Notes

  • Voronoi cells have role="listitem" for screen readers
  • Hover tooltips show persona ID, name, and category
  • Category legend provides group context

ChordDiagram Recipe

Purpose: Render cross-reference interactions between persona categories.

Data Shape

import { ChordCategory, ChordInteraction } from '../visualizations/ChordDiagram'

const categories: ChordCategory[] = [
  { name: 'core', count: 5 },
  { name: 'integration', count: 8 },
  { name: 'governance', count: 4 },
  { name: 'stakeholder', count: 5 },
]

const interactions: ChordInteraction[] = [
  { source: 'core', target: 'integration', value: 12 },
  { source: 'core', target: 'governance', value: 8 },
  { source: 'integration', target: 'governance', value: 6 },
  { source: 'stakeholder', target: 'core', value: 10 },
]

Props Interface

interface ChordDiagramProps {
  categories: ChordCategory[]
  interactions: ChordInteraction[]
  width?: number   // default: 600
  height?: number  // default: 600
}

Integration Example

import ChordDiagram from '../visualizations/ChordDiagram'

function CrossReferenceView() {
  const { categories, interactions } = useCrossReferenceMatrix()
  return <ChordDiagram categories={categories} interactions={interactions} />
}

Matrix Construction

The component builds an NxN adjacency matrix from the interactions array. Interactions are symmetric: source->target value is added to both matrix[si][ti] and matrix[ti][si] (unless source equals target).

Accessibility Notes

  • Arc labels use <text> elements readable by screen readers
  • Ribbons have aria-label describing the source, target, and value
  • d3.chord().padAngle(0.04) ensures arcs have visible gaps for clarity

HyperEdgeGraph Recipe

Purpose: Render champion-to-persona orchestration as hyperedges (convex hulls).

Data Shape

import { ChampionNode } from '../visualizations/HyperEdgeGraph'

const nodes: ChampionNode[] = [
  { id: 'CHF', name: 'Champion of Find', category: 'champion', isChampion: true,
    orchestrates: ['RC', 'KS', 'DBO'] },
  { id: 'RC', name: 'Research Catalyst', category: 'core', isChampion: false },
  { id: 'KS', name: 'Knowledge Synthesizer', category: 'core', isChampion: false },
  { id: 'DBO', name: 'Data Bridge Officer', category: 'integration', isChampion: false },
]

Props Interface

interface HyperEdgeGraphProps {
  nodes: ChampionNode[]
  width?: number   // default: 800
  height?: number  // default: 600
}

Integration Example

import HyperEdgeGraph from '../visualizations/HyperEdgeGraph'

function ChampionView() {
  const nodes = buildChampionNodes(personas, crossReferences)
  return <HyperEdgeGraph nodes={nodes} />
}

Hyperedge Rendering

For each champion node with orchestrates entries:

  1. Identify the champion and its orchestrated persona positions.
  2. Compute the convex hull of all positions in the group.
  3. Render the hull as a semi-transparent filled polygon.
  4. Champion nodes are rendered at 1.5x the base node radius.

Accessibility Notes

  • Champion nodes have a distinct visual marker (larger radius, bold label)
  • Hyperedge polygons have aria-label listing the champion and group members
  • Tooltip on hull hover shows the full orchestration group

ProvenanceFlow Recipe

Purpose: Render project phase timeline as a horizontal DAG flow.

Data Shape

import { ProvenancePhase } from '../visualizations/ProvenanceFlow'

const phases: ProvenancePhase[] = [
  { id: 'p1', name: 'Phase 8', phase: 'find',
    startDate: '2025-06-01', endDate: '2025-08-15',
    milestones: ['Registry v1', 'Cross-ref matrix'] },
  { id: 'p2', name: 'Phase 9', phase: 'create',
    startDate: '2025-08-15', endDate: '2025-11-01',
    milestones: ['Event bus', 'Collaboration engine'] },
  { id: 'p3', name: 'Phase 10', phase: 'create',
    startDate: '2025-11-01', endDate: '2026-01-15' },
]

Props Interface

interface ProvenanceFlowProps {
  phases: ProvenancePhase[]
  width?: number   // default: 900
  height?: number  // default: 300
}

Integration Example

import ProvenanceFlow from '../visualizations/ProvenanceFlow'

function ProjectTimeline() {
  const phases = useProjectPhases()
  return <ProvenanceFlow phases={phases} width={1100} height={350} />
}

Date Parsing

Dates are parsed with d3.timeParse('%Y-%m-%d'). If parsing fails, the component falls back to new Date(dateString). The x-axis extent is computed from all start and end dates.

Accessibility Notes

  • Phase bars have aria-label with name and date range
  • Milestone markers include text labels
  • Time axis uses readable date formatting

HeatmapMatrix Recipe

Purpose: Render a matrix heatmap of dimension scores across personas.

Data Shape

import { HeatmapCell } from '../visualizations/HeatmapMatrix'

const labels = ['RC', 'SA', 'QR', 'CE', 'KS']

const cells: HeatmapCell[] = [
  { source: 'RC', target: 'curiosity', value: 92 },
  { source: 'RC', target: 'leadership', value: 78 },
  { source: 'SA', target: 'curiosity', value: 85 },
  { source: 'SA', target: 'leadership', value: 88 },
  // ... 0-100 scale
]

Props Interface

interface HeatmapMatrixProps {
  labels: string[]                // Row and column labels
  cells: HeatmapCell[]            // Cell values
  width?: number                  // default: 600
  height?: number                 // default: 600
  colorRange?: [string, string]   // default: ['#f5f5f5', '#1565C0']
}

Integration Example

import HeatmapMatrix from '../visualizations/HeatmapMatrix'

function DimensionHeatmap() {
  const { labels, cells } = useDimensionScores()
  return (
    <HeatmapMatrix
      labels={labels}
      cells={cells}
      colorRange={['#f5f5f5', '#1565C0']}
    />
  )
}

Color Scale

The default color range interpolates from light gray (#f5f5f5) to deep blue (#1565C0) using d3.interpolateRgb. The scale domain is [0, maxValue] where maxValue is the highest cell value in the dataset.

Cell Sizing

Cell size is auto-computed as:

const cellSize = Math.min(innerWidth / labels.length, innerHeight / labels.length)

This ensures cells are square and fit within the available space.

Accessibility Notes

  • Each cell has aria-label in the format "PersonaID / Dimension: Score"
  • Column labels are rotated 45 degrees for readability
  • Color scale legend shows the value range

Responsive Sizing Pattern

All visualizations use the same responsive pattern via d3-utils.ts:

import { responsiveSvg } from './d3-utils'

// In useEffect:
const svg = responsiveSvg(container, width, height)
// Sets: viewBox="0 0 {width} {height}"
//       preserveAspectRatio="xMidYMid meet"
//       max-width: 100%
//       height: auto

To make a visualization fill its parent container, pass the container's clientWidth and clientHeight as width and height props.


Dark Mode Support

Dark mode is toggled via the ThemeToggle component. Visualizations adapt through:

  1. CSS custom properties -- Background and text colors are theme-aware.
  2. D3 color utilities -- Use lightenColor from d3-utils.ts for fill colors that remain visible in both modes.
  3. Tooltip styling -- The createTooltip function uses a dark semi-transparent background (rgba(0, 0, 0, 0.85)) with white text, which works in both light and dark themes.

Accessibility

All visualizations follow these accessibility practices:

ARIA Labels

Every SVG root element includes role="img" and a descriptive aria-label. Interactive elements (nodes, cells, arcs) include aria-label attributes describing their content.

Keyboard Navigation

Focusable SVG elements can be navigated with Tab. The tooltip show and hide functions are triggered on both mouseenter/mouseleave and focus/blur events.

Color Contrast

The FCC color palette is designed for WCAG AA contrast against both light and dark backgrounds. Use getFccColor rather than hardcoding hex values.

Screen Reader Support

  • Tooltip content is mirrored in aria-label attributes
  • Group elements use role="list" and role="listitem" where applicable
  • Data tables can be provided as a hidden <table> alternative for complex visualizations

See Also