Claude Code for D3.js Advanced: Custom Data Visualizations — Claude Skills 360 Blog
Blog / Frontend / Claude Code for D3.js Advanced: Custom Data Visualizations
Frontend

Claude Code for D3.js Advanced: Custom Data Visualizations

Published: June 18, 2027
Read time: 7 min read
By: Claude Skills 360

D3.js provides powerful primitives for custom data visualizations — d3.select(ref.current) selects DOM elements. d3.scaleLinear().domain([0, max]).range([0, width]) creates linear scales. d3.axisBottom(xScale).ticks(6).tickFormat(d3.format(".0s")) generates axes. d3.line<D>().x(accessor).y(accessor).curve(d3.curveMonotoneX) builds line generators. d3.area<D>() builds area generators with .x0()/.x1()/.y0()/.y1(). d3.pie<D>().value(d => d.count) computes pie slices. d3.arc() renders arc paths. d3.hierarchy(root) creates tree data. d3.treemap().size([w, h]) computes treemap rects. d3.forceSimulation(nodes) with .force("link", d3.forceLink(links)) builds network graphs. d3.zoom().on("zoom", handler) adds pan/zoom. d3.brushX().on("end", handler) adds range brushes. selection.transition().duration(600).attr(...) animates changes. Claude Code generates D3 custom charts, force graphs, treemaps, and animated dashboards.

CLAUDE.md for D3.js Advanced

## D3.js Advanced Stack
- Version: d3 >= 7.9 (full bundle) or individual @d3/* packages
- React integration: const ref = useRef<SVGSVGElement>(null); useEffect(() => { const svg = d3.select(ref.current); /* draw */ return () => svg.selectAll("*").remove() }, [data])
- Scale: const x = d3.scaleLinear().domain(d3.extent(data, d => d.x) as [number, number]).range([margin.left, width - margin.right])
- Axis: svg.append("g").attr("transform", `translate(0,${height - margin.bottom})`).call(d3.axisBottom(x).ticks(5))
- Transition: selection.transition().duration(400).ease(d3.easeQuadOut).attr("y", d => y(d.value))
- Zoom: const zoom = d3.zoom<SVGSVGElement, unknown>().scaleExtent([0.5, 8]).on("zoom", (e) => g.attr("transform", e.transform))

useD3 Hook

// lib/hooks/useD3.ts — React hook for D3 DOM integration
import { useRef, useEffect, type RefObject } from "react"
import type * as d3 from "d3"

type D3Selection = d3.Selection<SVGSVGElement, unknown, null, undefined>

export function useD3(
  renderFn: (svg: D3Selection) => (() => void) | void,
  deps: React.DependencyList,
): RefObject<SVGSVGElement | null> {
  const ref = useRef<SVGSVGElement | null>(null)

  useEffect(() => {
    if (!ref.current) return
    // Dynamic import to avoid SSR issues
    import("d3").then((d3) => {
      const svg = d3.select(ref.current!)
      const cleanup = renderFn(svg as any)
      return () => {
        cleanup?.()
        svg.selectAll("*").remove()
      }
    })
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps)

  return ref
}

Multi-Series Line Chart

// components/viz/MultiLineChart.tsx — D3 line chart with zoom and brush
"use client"
import { useRef, useEffect, useCallback } from "react"
import * as d3 from "d3"

type DataPoint = { date: Date; value: number }
type Series = { name: string; color: string; data: DataPoint[] }

const MARGIN = { top: 24, right: 32, bottom: 48, left: 56 }

interface MultiLineChartProps {
  series: Series[]
  width?: number
  height?: number
  title?: string
}

export function MultiLineChart({ series, width = 700, height = 380, title }: MultiLineChartProps) {
  const svgRef = useRef<SVGSVGElement>(null)
  const innerW = width - MARGIN.left - MARGIN.right
  const innerH = height - MARGIN.top - MARGIN.bottom
  const brushH = 60

  const draw = useCallback(() => {
    if (!svgRef.current || series.length === 0) return
    const svg = d3.select(svgRef.current)
    svg.selectAll("*").remove()

    // All data points combined for global domain
    const allData = series.flatMap((s) => s.data)

    const xDomain = d3.extent(allData, (d) => d.date) as [Date, Date]
    const yMax = d3.max(allData, (d) => d.value) ?? 0

    const xFull = d3.scaleTime().domain(xDomain).range([0, innerW])
    const y = d3.scaleLinear().domain([0, yMax * 1.05]).range([innerH - brushH - 24, 0])
    const xBrush = d3.scaleTime().domain(xDomain).range([0, innerW])
    const yBrush = d3.scaleLinear().domain([0, yMax]).range([brushH, 0])

    // Clip path
    svg.append("defs").append("clipPath").attr("id", "chart-clip")
      .append("rect").attr("width", innerW).attr("height", innerH - brushH - 24)

    const root = svg.append("g").attr("transform", `translate(${MARGIN.left},${MARGIN.top})`)
    const chartArea = root.append("g").attr("clip-path", "url(#chart-clip)")

    // ── Gridlines
    root.append("g").attr("class", "grid")
      .call(d3.axisLeft(y).ticks(5).tickSize(-innerW).tickFormat(() => ""))
      .call((g) => g.select(".domain").remove())
      .call((g) => g.selectAll(".tick line").attr("stroke", "#e5e7eb").attr("stroke-dasharray", "3,3"))

    // ── Axes
    const xAxis = root.append("g")
      .attr("transform", `translate(0,${innerH - brushH - 24})`)
      .call(d3.axisBottom(xFull).ticks(6).tickFormat(d3.timeFormat("%b %Y") as any))

    root.append("g").call(
      d3.axisLeft(y).ticks(5).tickFormat((v) => d3.format(".2s")(v as number)),
    ).call((g) => g.select(".domain").remove())

    // ── Line generator
    const lineGen = (xs: d3.ScaleTime<number, number>) =>
      d3.line<DataPoint>().x((d) => xs(d.date)).y((d) => y(d.value)).curve(d3.curveMonotoneX)

    const areaGen = (xs: d3.ScaleTime<number, number>) =>
      d3.area<DataPoint>().x((d) => xs(d.date)).y0(innerH - brushH - 24).y1((d) => y(d.value)).curve(d3.curveMonotoneX)

    // ── Series
    const seriesGroups = chartArea.selectAll(".series")
      .data(series)
      .join("g")
      .attr("class", "series")

    // Area fills (lighter)
    seriesGroups.append("path")
      .attr("fill", (d) => d.color)
      .attr("fill-opacity", 0.08)
      .attr("d", (d) => areaGen(xFull)(d.data))

    // Lines
    seriesGroups.append("path")
      .attr("fill", "none")
      .attr("stroke", (d) => d.color)
      .attr("stroke-width", 2)
      .attr("d", (d) => lineGen(xFull)(d.data))
      .attr("stroke-dashoffset", function () { return (this as SVGPathElement).getTotalLength() })
      .attr("stroke-dasharray", function () { return (this as SVGPathElement).getTotalLength() })
      .transition().duration(900).ease(d3.easeQuadOut)
      .attr("stroke-dashoffset", 0)

    // ── Brush
    const brushGroup = root.append("g").attr("transform", `translate(0,${innerH - brushH + 8})`)

    // Mini lines in brush
    brushGroup.selectAll(".mini-line")
      .data(series)
      .join("path")
      .attr("fill", "none")
      .attr("stroke", (d) => d.color)
      .attr("stroke-width", 1)
      .attr("opacity", 0.5)
      .attr("d", (d) => d3.line<DataPoint>().x((p) => xBrush(p.date)).y((p) => yBrush(p.value)).curve(d3.curveMonotoneX)(d.data))

    brushGroup.append("g").call(d3.axisBottom(xBrush).ticks(4).tickFormat(d3.timeFormat("%Y") as any))
      .attr("transform", `translate(0,${brushH})`)

    const brush = d3.brushX()
      .extent([[0, 0], [innerW, brushH]])
      .on("end", (event) => {
        if (!event.selection) return xAxis.call(d3.axisBottom(xFull).ticks(6).tickFormat(d3.timeFormat("%b %Y") as any))
        const [x0, x1] = (event.selection as [number, number]).map(xBrush.invert)
        const xNew = xFull.copy().domain([x0, x1])
        xAxis.call(d3.axisBottom(xNew).ticks(6).tickFormat(d3.timeFormat("%b %Y") as any))
        seriesGroups.select("path:nth-child(2)").attr("d", (d: any) => lineGen(xNew)(d.data))
        seriesGroups.select("path:nth-child(1)").attr("d", (d: any) => areaGen(xNew)(d.data))
      })

    brushGroup.append("g").call(brush)

    // ── Legend
    const legend = svg.append("g").attr("transform", `translate(${MARGIN.left},${height - 12})`)
    series.forEach((s, i) => {
      const g = legend.append("g").attr("transform", `translate(${i * 130},0)`)
      g.append("line").attr("x1", 0).attr("x2", 20).attr("y1", 0).attr("y2", 0)
        .attr("stroke", s.color).attr("stroke-width", 2)
      g.append("text").attr("x", 26).attr("y", 4).attr("font-size", 11).attr("fill", "#6b7280").text(s.name)
    })
  }, [series, width, height, innerW, innerH])

  useEffect(() => { draw() }, [draw])

  return (
    <div className="rounded-2xl border bg-card px-2 py-4">
      {title && <p className="text-sm font-semibold text-center mb-2">{title}</p>}
      <svg ref={svgRef} width={width} height={height} className="overflow-visible" />
    </div>
  )
}

Force-Directed Network Graph

// components/viz/NetworkGraph.tsx — D3 force simulation
"use client"
import { useRef, useEffect } from "react"
import * as d3 from "d3"

type GraphNode = { id: string; group: number; label: string; value?: number }
type GraphLink = { source: string; target: string; weight?: number }

interface NetworkGraphProps {
  nodes: GraphNode[]
  links: GraphLink[]
  width?: number
  height?: number
}

const GROUP_COLORS = ["#6366f1", "#ef4444", "#22c55e", "#f59e0b", "#3b82f6", "#8b5cf6"]

export function NetworkGraph({ nodes, links, width = 600, height = 500 }: NetworkGraphProps) {
  const svgRef = useRef<SVGSVGElement>(null)

  useEffect(() => {
    if (!svgRef.current) return
    const svg = d3.select(svgRef.current)
    svg.selectAll("*").remove()

    const sim = d3.forceSimulation<GraphNode>(nodes)
      .force("link", d3.forceLink<GraphNode, GraphLink>(links).id((d) => d.id).distance(80))
      .force("charge", d3.forceManyBody().strength(-200))
      .force("center", d3.forceCenter(width / 2, height / 2))
      .force("collision", d3.forceCollide<GraphNode>().radius((d) => nodeRadius(d) + 4))

    // Zoom
    const g = svg.append("g")
    svg.call(
      d3.zoom<SVGSVGElement, unknown>()
        .scaleExtent([0.3, 4])
        .on("zoom", (e) => g.attr("transform", e.transform)),
    )

    // Links
    const link = g.append("g")
      .selectAll("line")
      .data(links)
      .join("line")
      .attr("stroke", "#d1d5db")
      .attr("stroke-width", (d) => Math.sqrt(d.weight ?? 1))

    // Nodes
    const node = g.append("g")
      .selectAll("g")
      .data(nodes)
      .join("g")
      .call(d3.drag<SVGGElement, GraphNode>()
        .on("start", (event, d) => { if (!event.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y })
        .on("drag", (event, d) => { d.fx = event.x; d.fy = event.y })
        .on("end", (event, d) => { if (!event.active) sim.alphaTarget(0); d.fx = null; d.fy = null }),
      )

    node.append("circle")
      .attr("r", nodeRadius)
      .attr("fill", (d) => GROUP_COLORS[d.group % GROUP_COLORS.length])
      .attr("fill-opacity", 0.85)
      .attr("stroke", "#fff")
      .attr("stroke-width", 2)

    node.append("text")
      .text((d) => d.label)
      .attr("text-anchor", "middle")
      .attr("dy", (d) => nodeRadius(d) + 13)
      .attr("font-size", 10)
      .attr("fill", "#6b7280")

    node.append("title").text((d) => d.label)

    sim.on("tick", () => {
      link
        .attr("x1", (d) => (d.source as any).x)
        .attr("y1", (d) => (d.source as any).y)
        .attr("x2", (d) => (d.target as any).x)
        .attr("y2", (d) => (d.target as any).y)

      node.attr("transform", (d) => `translate(${d.x ?? 0},${d.y ?? 0})`)
    })

    return () => { sim.stop() }
  }, [nodes, links, width, height])

  return (
    <div className="rounded-2xl border bg-card overflow-hidden">
      <svg ref={svgRef} width={width} height={height} />
    </div>
  )
}

function nodeRadius(d: GraphNode) {
  return Math.max(8, Math.sqrt((d.value ?? 10) * 4))
}

For the Recharts alternative when a React-native chart component experience with a familiar JSX API, built-in chart types (Bar, Line, Area, Pie, Radar, Scatter), and less imperative DOM code is preferred — Recharts covers 90% of dashboard chart use cases without touching D3 directly, while D3 is indispensable for custom or physics-based visualizations, see the Recharts guide. For the Observable Plot alternative when a higher-level grammar-of-graphics API built on D3 (similar to Vega-Lite) with concise one-liner mark-based syntax and first-class support in Observable notebooks is preferred — Observable Plot is the fastest path to many standard charts while D3 provides the lowest-level control for completely custom SVG graphics, see the Observable Plot guide. The Claude Skills 360 bundle includes D3.js advanced skill sets covering force graphs, zooming, brushing, and animated transitions. Start with the free tier to try custom visualization generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free