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

Claude Code for D3.js: Custom Data Visualizations

Published: March 23, 2027
Read time: 8 min read
By: Claude Skills 360

D3.js maps data to visual marks with full control — scaleLinear().domain([min, max]).range([0, width]) maps values to pixels. scaleBand() handles categorical axes with padding. scaleTime() maps Date objects to SVG positions. d3.axisBottom(scale) and d3.axisLeft(scale) render tick marks with selection.call(axis). d3.line(), d3.area(), and d3.arc() are path generators. d3.zoom() adds pan/zoom behavior. d3.brush() creates range selectors. d3.forceSimulation() positions network nodes. Transitions animate attribute changes: selection.transition().duration(750).attr("x", newX). In React, the useD3 pattern gives D3 a ref to an SVG element while React manages mount/unmount. Claude Code generates D3 scales, axis rendering, path generators, force layouts, zooming, brushing, and animated transitions for custom SVG data visualizations.

CLAUDE.md for D3.js

## D3.js Stack
- Version: d3 >= 7.9 — import { scaleLinear, select, axisBottom } from "d3"
- Scales: scaleLinear().domain([0, max]).range([height, 0]) — note: y-axis inverted
- Axes: svg.append("g").call(d3.axisBottom(xScale)) — call with selection
- Line: d3.line<D>().x(d => xScale(d.date)).y(d => yScale(d.value))(data)
- React: useD3(ref => { d3.select(ref.current)... }, [deps]) — ref-based D3 in React
- Transition: selection.transition().duration(500).attr("y", yScale(d.value))
- Resize: viewBox="0 0 width height" + preserveAspectRatio for responsive SVG

React + D3 Hook Pattern

// hooks/useD3.ts — bridge D3 and React
import { useRef, useEffect, type RefObject } from "react"
import * as d3 from "d3"

export function useD3<T extends SVGElement>(
  renderFn: (svg: d3.Selection<T, unknown, null, undefined>) => void | (() => void),
  deps: React.DependencyList
): RefObject<T> {
  const ref = useRef<T>(null!)

  useEffect(() => {
    if (!ref.current) return
    const cleanup = renderFn(d3.select(ref.current))
    return cleanup ?? undefined
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps)

  return ref
}

Line Chart with Axes

// components/charts/D3LineChart.tsx — custom D3 line chart
"use client"
import * as d3 from "d3"
import { useD3 } from "@/hooks/useD3"

interface DataPoint {
  date: Date
  value: number
}

interface D3LineChartProps {
  data: DataPoint[]
  width?: number
  height?: number
  color?: string
}

export function D3LineChart({
  data,
  width = 600,
  height = 300,
  color = "#3b82f6",
}: D3LineChartProps) {
  const margin = { top: 20, right: 20, bottom: 40, left: 50 }
  const innerWidth = width - margin.left - margin.right
  const innerHeight = height - margin.top - margin.bottom

  const svgRef = useD3<SVGSVGElement>(
    svg => {
      svg.selectAll("*").remove()

      const g = svg
        .append("g")
        .attr("transform", `translate(${margin.left},${margin.top})`)

      // Scales
      const xScale = d3
        .scaleTime()
        .domain(d3.extent(data, d => d.date) as [Date, Date])
        .range([0, innerWidth])

      const yScale = d3
        .scaleLinear()
        .domain([0, d3.max(data, d => d.value)! * 1.1])
        .range([innerHeight, 0])
        .nice()

      // Gridlines
      g.append("g")
        .attr("class", "gridlines")
        .call(
          d3.axisLeft(yScale)
            .tickSize(-innerWidth)
            .tickFormat(() => "")
        )
        .call(sel => sel.select(".domain").remove())
        .call(sel => sel.selectAll(".tick line")
          .attr("stroke", "#e2e8f0")
          .attr("stroke-dasharray", "3,3"))

      // X Axis
      g.append("g")
        .attr("transform", `translate(0,${innerHeight})`)
        .call(
          d3.axisBottom(xScale)
            .ticks(6)
            .tickFormat(d3.timeFormat("%b %d") as (d: Date | d3.NumberValue) => string)
        )
        .call(sel => sel.select(".domain").attr("stroke", "#cbd5e1"))
        .call(sel => sel.selectAll(".tick line").attr("stroke", "#cbd5e1"))
        .call(sel => sel.selectAll(".tick text").attr("fill", "#64748b").attr("font-size", "12"))

      // Y Axis
      g.append("g")
        .call(
          d3.axisLeft(yScale)
            .ticks(5)
            .tickFormat(v => `$${(+v / 100).toFixed(0)}`)
        )
        .call(sel => sel.select(".domain").remove())
        .call(sel => sel.selectAll(".tick line").remove())
        .call(sel => sel.selectAll(".tick text").attr("fill", "#64748b").attr("font-size", "12"))

      // Area fill
      const area = d3.area<DataPoint>()
        .x(d => xScale(d.date))
        .y0(innerHeight)
        .y1(d => yScale(d.value))
        .curve(d3.curveMonotoneX)

      const defs = svg.append("defs")
      const gradient = defs.append("linearGradient")
        .attr("id", "area-gradient")
        .attr("gradientUnits", "userSpaceOnUse")
        .attr("x1", 0).attr("y1", 0)
        .attr("x2", 0).attr("y2", innerHeight)

      gradient.append("stop").attr("offset", "0%").attr("stop-color", color).attr("stop-opacity", 0.3)
      gradient.append("stop").attr("offset", "100%").attr("stop-color", color).attr("stop-opacity", 0)

      g.append("path")
        .datum(data)
        .attr("fill", "url(#area-gradient)")
        .attr("d", area)

      // Line
      const line = d3.line<DataPoint>()
        .x(d => xScale(d.date))
        .y(d => yScale(d.value))
        .curve(d3.curveMonotoneX)

      const path = g.append("path")
        .datum(data)
        .attr("fill", "none")
        .attr("stroke", color)
        .attr("stroke-width", 2)

      // Animate line drawing
      const totalLength = (path.node() as SVGPathElement).getTotalLength()
      path
        .attr("stroke-dasharray", `${totalLength} ${totalLength}`)
        .attr("stroke-dashoffset", totalLength)
        .attr("d", line)
        .transition()
        .duration(1000)
        .ease(d3.easeQuadOut)
        .attr("stroke-dashoffset", 0)
    },
    [data, width, height, color]
  )

  return (
    <svg
      ref={svgRef}
      viewBox={`0 0 ${width} ${height}`}
      className="w-full"
      style={{ height }}
    />
  )
}

Force-Directed Network Graph

// components/charts/ForceGraph.tsx — d3.forceSimulation
"use client"
import { useEffect, useRef } from "react"
import * as d3 from "d3"

interface Node extends d3.SimulationNodeDatum {
  id: string
  group: number
  label: string
  value: number
}

interface Link extends d3.SimulationLinkDatum<Node> {
  source: string | Node
  target: string | Node
  weight: number
}

interface ForceGraphProps {
  nodes: Node[]
  links: Link[]
  width?: number
  height?: number
}

export function ForceGraph({ nodes, links, width = 600, height = 400 }: ForceGraphProps) {
  const svgRef = useRef<SVGSVGElement>(null)

  useEffect(() => {
    if (!svgRef.current) return

    const svg = d3.select(svgRef.current)
    svg.selectAll("*").remove()

    const color = d3.scaleOrdinal(d3.schemeTableau10)

    // Force simulation
    const simulation = d3.forceSimulation(nodes)
      .force("link", d3.forceLink<Node, Link>(links)
        .id(d => d.id)
        .distance(80)
        .strength(d => d.weight))
      .force("charge", d3.forceManyBody().strength(-200))
      .force("center", d3.forceCenter(width / 2, height / 2))
      .force("collision", d3.forceCollide<Node>().radius(d => Math.sqrt(d.value) * 5 + 8))

    // Links
    const link = svg.append("g")
      .selectAll("line")
      .data(links)
      .join("line")
      .attr("stroke", "#cbd5e1")
      .attr("stroke-opacity", 0.6)
      .attr("stroke-width", d => Math.sqrt(d.weight) * 2)

    // Node groups
    const node = svg.append("g")
      .selectAll<SVGGElement, Node>("g")
      .data(nodes)
      .join("g")
      .call(
        d3.drag<SVGGElement, Node>()
          .on("start", (event, d) => {
            if (!event.active) simulation.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) simulation.alphaTarget(0)
            d.fx = null; d.fy = null
          })
      )

    node.append("circle")
      .attr("r", d => Math.sqrt(d.value) * 5 + 5)
      .attr("fill", d => color(String(d.group)))
      .attr("stroke", "#fff")
      .attr("stroke-width", 2)

    node.append("text")
      .text(d => d.label)
      .attr("text-anchor", "middle")
      .attr("dy", "0.35em")
      .attr("font-size", "10px")
      .attr("fill", "#1e293b")
      .attr("pointer-events", "none")

    node.append("title").text(d => `${d.label}: ${d.value}`)

    // Tick update
    simulation.on("tick", () => {
      link
        .attr("x1", d => (d.source as Node).x!)
        .attr("y1", d => (d.source as Node).y!)
        .attr("x2", d => (d.target as Node).x!)
        .attr("y2", d => (d.target as Node).y!)

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

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

  return <svg ref={svgRef} width={width} height={height} />
}

Zoom and Brush

// components/charts/ZoomableChart.tsx — d3.zoom with brush for range selection
"use client"
import { useEffect, useRef } from "react"
import * as d3 from "d3"

export function ZoomableBarChart({ data }: { data: { label: string; value: number }[] }) {
  const svgRef = useRef<SVGSVGElement>(null)
  const margin = { top: 20, right: 20, bottom: 60, left: 50 }
  const width = 600, height = 300
  const inner = { w: width - margin.left - margin.right, h: height - margin.top - margin.bottom }

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

    const xScale = d3.scaleBand()
      .domain(data.map(d => d.label))
      .range([0, inner.w])
      .padding(0.3)

    const yScale = d3.scaleLinear()
      .domain([0, d3.max(data, d => d.value)! * 1.1])
      .range([inner.h, 0])
      .nice()

    const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`)
    const clipId = "chart-clip"

    svg.append("defs").append("clipPath").attr("id", clipId)
      .append("rect").attr("width", inner.w).attr("height", inner.h)

    const chartArea = g.append("g").attr("clip-path", `url(#${clipId})`)

    // Bars
    const bars = chartArea.append("g").selectAll("rect")
      .data(data)
      .join("rect")
      .attr("x", d => xScale(d.label)!)
      .attr("y", d => yScale(d.value))
      .attr("width", xScale.bandwidth())
      .attr("height", d => inner.h - yScale(d.value))
      .attr("fill", "#3b82f6")
      .attr("rx", 2)

    const xAxis = g.append("g").attr("transform", `translate(0,${inner.h})`)
      .call(d3.axisBottom(xScale))

    g.append("g").call(d3.axisLeft(yScale).ticks(5))

    // Zoom behavior
    const zoom = d3.zoom<SVGSVGElement, unknown>()
      .scaleExtent([1, 8])
      .translateExtent([[0, 0], [inner.w, inner.h]])
      .extent([[0, 0], [inner.w, inner.h]])
      .on("zoom", event => {
        const newXScale = event.transform.rescaleX(xScale as unknown as d3.ScaleContinuousNumeric<number, number>)
        xAxis.call(d3.axisBottom(newXScale as unknown as d3.AxisScale<string>))
        bars.attr("x", d => (newXScale(xScale(d.label)!) as number))
          .attr("width", xScale.bandwidth() * event.transform.k)
      })

    svg.call(zoom)
  }, [data])

  return (
    <svg
      ref={svgRef}
      viewBox={`0 0 ${width} ${height}`}
      className="w-full border rounded-lg cursor-grab active:cursor-grabbing"
      style={{ height }}
    />
  )
}

For the Recharts alternative when React component composition and simpler chart types (line, bar, area, pie) are needed without D3’s low-level API — Recharts wraps D3 scales internally, providing a declarative component API that’s much faster to implement for standard charts at the cost of customization depth, see the Recharts guide. For the Observable Plot alternative when statistical chart types and mark-based grammar (dot plot, density, regression) are needed with semantic shorthand — Observable Plot uses D3 under the hood but provides a declarative plot({ marks: [] }) API without needing to manage SVG or scales directly, see the statistical visualization guide. The Claude Skills 360 bundle includes D3.js skill sets covering scales, axes, force graphs, and zoom behaviors. 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