030 / D3.JS / NETWORK

D3 Force-Directed Network

D3.js

Force-directed network with a settling physics simulation, labeled nodes, and automatic centering.

SPEC
#030
ENGINE
D3.js
FAMILY
Network
RENDERER
svg
STATUS
beta

Installation

PLOTCN/REGISTRY/D3-FORCE-NETWORK/SOURCE
pnpm dlx shadcn@latest add @plotcn/d3-force-network

Checking public registry…

View local registry JSON
REGISTRY direct URL·ENGINE D3.js·FILES 1·DEPENDENCIES 3

Copied as source into your project (requires d3-force).

× 340Container width
Measuring preview...
D3 · SVG · 0 × 340pxMotion enabled · ResizeObserver
Component Specifications
BEST FOR
D3 Force-Directed Network visual analytics and network category telemetry.
DATA MODEL
Normalized network records matching canonical dataShape.
INTERACTION
Static presentation
RESPONSIVE
Container-aware ResizeObserver
ANIMATION
Interpolated transitions
RUNTIME
D3.js svg
01 / Component Usage

Basic & Interactive Integration

Import D3ForceNetwork directly into your React client component. Provide an array of observations conforming to the data shape.

page.tsx
import { D3ForceNetwork } from "@/components/charts/d3/d3-force-network"

const data = [
  {
    "label": "Jan",
    "value": 186
  },
  {
    "label": "Feb",
    "value": 305
  },
  {
    "label": "Mar",
    "value": 237
  }
]

export default function ChartDemo() {
  return (
    <div className="w-full max-w-xl h-80">
      <D3ForceNetwork data={data} color="#10b981" />
    </div>
  )
}

Requires a client context ('use client') for DOM lifecycle and container measurement.

Expected ResultLive 220px
Container-aware width · 200px height
02 / Data Contract

Input Schema & Coordinates

Input data format for D3 Force-Directed Network. Records should provide required coordinates and metrics.

Chart data contract
FieldTypeRequiredMeaning
labelstringYesCategory label or horizontal axis timestamp
valuenumberYesMetric numerical magnitude
Null / Missing Value Policy:

Missing values are gracefully handled or skipped depending on interpolation rules.

Coordinate Ordering Policy:

Records should follow monotonic sorting for sequential coordinate plots.

Example Observation Payload:
[
  {
    "label": "Jan",
    "value": 186
  },
  {
    "label": "Feb",
    "value": 305
  },
  {
    "label": "Mar",
    "value": 237
  }
]
03 / Component API & Styling

Props Reference & Interactive Prop Explorer

Inspect every component property, customize semantic color roles live with instant visual feedback, and copy production-ready code with active prop configurations.

Colors & Appearance Configuration

Customize primary, reference, or annotation series colors. Defaults derive from Plotcn theme tokens.

Generated Usage Code (Live Props):
<Chart
  data={data}
  xKey="date"
  seriesKey="value"
/>
Interactive Prop Preview Lab
colorstring

Primary theme stroke or fill color. Accepts CSS variables or color values.

Select value to preview live:#10b981
Active: color="#10b981"Default: "var(--chart-1, #10b981)"
All Properties (4)
Component properties
PropertyTypeDefaultRequiredDescription
dataReq
Record<string, any>[]Yes

Array of structured observations or datum objects to visualize.

Best for: Primary dataset

string"var(--chart-1, #10b981)"No

Primary theme stroke or fill color. Accepts CSS variables or color values.

Best for: Theming and visual branding

number | string320No

Container height in pixels or standard CSS dimension strings.

stringundefinedNo

Tailwind CSS classes or custom stylesheet class applied to container wrapper.

04 / Cookbook & States

Component Variants & Edge States

Production cookbooks showcasing configuration variants alongside verified handling of loading, empty data, and network error states.

D3 Force-Directed Network (Default)

Standard presentation with Plotcn dark theme tokens.

<D3ForceNetwork nodes={nodes} links={links} />
Lifecycle & Exception States
01. Loading State

Skeletons indicate runtime fetch or pending data queries.

02. Empty Data State

Handles empty collections ([]) gracefully without crashing.

03. Error State

Graceful failure banner when data source or script fails.

05 / Responsive Lab

Container-Driven Breakpoints

Scales gracefully according to container width, recalculating layout bounds.

Desktop
1100px+

Full horizontal scale and complete axis tick labeling.

Tablet
768px

Tighter margins with optimized coordinate grid spacing.

Mobile
390px

Compact labels and touch-friendly interaction.

Tablet Preview (768px Container Constraint)
Mobile Preview (390px Container Constraint)
06 / Assistive Technology

Accessibility & Navigation Standards

Accessible visualization presenting D3 Force-Directed Network data with semantic contrast.

Semantic Role & Landmark

Container mounts as figure[role="region"] with explicit assistive label.

Color-Independent Legibility

Coordinate baselines and labels preserve data legibility regardless of color perception.

Screen Reader Summary

Embeds visually hidden summary (.sr-only) declaring: “D3 Force-Directed Network data visualization

Reduced Motion Support

Suppresses transitions when user prefers reduced motion.

Keyboard Interaction Model
Keyboard interaction model
KeyAction
TabFocus chart container and navigate through interactive regions.
07 / Source Anatomy

Internal Architecture & File Dependencies

Source-first ownership model. Inspect the exact component call tree, dependencies, and full implementation below.

Component Architecture Call Tree
D3ForceNetwork(Component Entry)
└──D3.js Engine Layer[svg]

Visualization calculations and rendering via D3.js.

Involved Source Files & Registry Assets
registry/d3/d3-force-network.tsx
Primary component source implementation.
registry/d3/d3-force-network.tsx
"use client"import * as React from "react"import { forceSimulation, forceManyBody, forceCenter, forceLink, type SimulationNodeDatum } from "d3-force"import { useChartReducedMotion } from "../shared/use-chart-reduced-motion"import { ChartEmptyState, ChartErrorState } from "../shared/chart-state"import { cn } from "@/lib/utils"export interface NetworkNode extends SimulationNodeDatum {  id: string  label: string  group?: number}export interface NetworkLink {  source: string | NetworkNode  target: string | NetworkNode  value?: number}export interface SimulationLink {  source: NetworkNode  target: NetworkNode  value?: number}export interface D3ForceNetworkProps {  nodes: NetworkNode[]  links: NetworkLink[]  width?: number  height?: number  motion?: boolean  className?: string}/** * D3 Force-Directed Network Graph * D3 calculates physics coordinates (repulsion, centering, link springs); React renders the SVG graph. */export function D3ForceNetwork({  nodes: initialNodes,  links: initialLinks,  width = 600,  height = 360,  className,  motion = true,}: D3ForceNetworkProps) {  const reducedMotion = useChartReducedMotion()  const [nodes, setNodes] = React.useState<NetworkNode[]>([])  const [links, setLinks] = React.useState<SimulationLink[]>([])  React.useEffect(() => {    const nodesCopy = initialNodes.map((n) => ({ ...n }))    const linksCopy = initialLinks.map((l) => ({ ...l }))    const simulation = forceSimulation(nodesCopy)      .force("charge", forceManyBody().strength(-120))      .force("center", forceCenter(width / 2, height / 2))      .force(        "link",        forceLink<NetworkNode, SimulationLink>(linksCopy as unknown as SimulationLink[])          .id((d) => d.id)          .distance(60)      )    const publish = () => {      setNodes([...nodesCopy])      setLinks([...(linksCopy as unknown as SimulationLink[])])    }    let frame = 0    if (!motion || reducedMotion) {      cancelAnimationFrame(frame)      simulation.stop().tick(180)      frame = requestAnimationFrame(publish)    } else simulation.on("tick", publish)    return () => {      simulation.stop()    }  }, [initialNodes, initialLinks, width, height, motion, reducedMotion])  const [hoveredNode, setHoveredNode] = React.useState<NetworkNode | null>(null)  const invalidData = initialNodes.some((node) => !node.id || !node.label)  if (!initialNodes.length) return <ChartEmptyState />  if (invalidData) return <ChartErrorState description="Every node requires an id and label." />  return (    <div className={cn("plotcn-chart relative w-full overflow-hidden rounded-xl border border-[var(--chart-border)] bg-[var(--chart-background)] p-4 select-none", className)}>      <svg        viewBox={`0 0 ${width} ${height}`}        className="w-full h-auto"        aria-label="Force-directed network visualization"      >        <g>          {links.map((link, idx) => (            <line              key={idx}              x1={link.source.x}              y1={link.source.y}              x2={link.target.x}              y2={link.target.y}              stroke="var(--chart-grid)"              strokeWidth={1.5}            />          ))}        </g>        <g>          {nodes.map((node) => {            const isHovered = hoveredNode?.id === node.id            return (              <g                key={node.id}                transform={`translate(${node.x || 0},${node.y || 0})`}                onMouseEnter={() => setHoveredNode(node)}                onMouseLeave={() => setHoveredNode(null)}                className="cursor-pointer"              >                <circle                  r={isHovered ? 11 : 8}                  fill={isHovered ? "var(--chart-background)" : "var(--chart-1)"}                  stroke={isHovered ? "var(--chart-1)" : "var(--chart-background)"}                  strokeWidth={isHovered ? 3 : 2}                  className="transition-all duration-150"                />                <text                  dy={isHovered ? 20 : 16}                  textAnchor="middle"                  fill={isHovered ? "var(--chart-foreground)" : "var(--chart-axis)"}                  className="text-[10px] font-mono select-none transition-colors"                >                  {node.label}                </text>              </g>            )          })}        </g>      </svg>      {/* Floating Theme Tooltip */}      {hoveredNode && hoveredNode.x !== undefined && hoveredNode.y !== undefined && (        <div          className="absolute pointer-events-none z-30 -translate-x-1/2 -translate-y-full transition-all duration-75"          style={{            left: `${((hoveredNode.x) / width) * 100}%`,            top: `${((hoveredNode.y) / height) * 100}%`,            marginTop: "-16px",          }}        >          <div className="plotcn-chart-tooltip">            <div className="mb-1 flex items-center justify-between gap-2 border-b border-[var(--chart-tooltip-border)] pb-1 text-xs font-semibold text-[var(--chart-tooltip-foreground)]">              <span>{hoveredNode.label}</span>              <span className="size-2 rounded-full bg-[var(--chart-1)]" />            </div>            <div className="flex items-center justify-between font-mono text-[11px] text-[var(--chart-tooltip-muted)]">              <span>Node ID</span>              <span className="text-[var(--chart-tooltip-foreground)]">{hoveredNode.id}</span>            </div>            {hoveredNode.group !== undefined && (              <div className="mt-1 flex items-center justify-between font-mono text-[11px] text-[var(--chart-tooltip-muted)]">                <span>Cluster</span>                <span className="font-mono font-semibold text-[var(--chart-tooltip-foreground)]">Group {hoveredNode.group}</span>              </div>            )}          </div>        </div>      )}    </div>  )}