Claude Code for Data Visualization: Charts, Dashboards, and D3.js — Claude Skills 360 Blog
Blog / Development / Claude Code for Data Visualization: Charts, Dashboards, and D3.js
Development

Claude Code for Data Visualization: Charts, Dashboards, and D3.js

Published: July 30, 2026
Read time: 9 min read
By: Claude Skills 360

Data visualization requires two skills: picking the right chart type for the data, and implementing it correctly with accessibility, responsive sizing, and performance for large datasets. Claude Code generates chart components with proper ARIA labels, responsive containers, and the D3 patterns for custom visualizations that library components can’t produce.

Recharts for React

Build a dashboard with: line chart (revenue over time),
bar chart (orders by category), and a pie/donut chart (traffic sources).
All charts should be responsive and show tooltips on hover.
// components/Dashboard/RevenueChart.tsx
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { format } from 'date-fns';

interface RevenueDataPoint {
  date: string;
  revenue: number;
  refunds: number;
}

const CustomTooltip = ({ active, payload, label }: any) => {
  if (!active || !payload?.length) return null;
  return (
    <div role="tooltip" aria-label="Revenue details" className="chart-tooltip">
      <p className="label">{format(new Date(label), 'MMM d, yyyy')}</p>
      {payload.map((entry: any) => (
        <p key={entry.name} style={{ color: entry.color }}>
          {entry.name}: ${entry.value.toLocaleString()}
        </p>
      ))}
    </div>
  );
};

export function RevenueChart({ data }: { data: RevenueDataPoint[] }) {
  const totalRevenue = data.reduce((sum, d) => sum + d.revenue, 0);

  return (
    // role="img" + aria-label makes the chart accessible to screen readers
    <figure role="img" aria-label={`Revenue line chart showing $${totalRevenue.toLocaleString()} total over ${data.length} days`}>
      <figcaption className="sr-only">
        Revenue trend from {data[0]?.date} to {data[data.length - 1]?.date}.
        Total revenue: ${totalRevenue.toLocaleString()}.
      </figcaption>
      <ResponsiveContainer width="100%" height={300}>
        <LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
          <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
          <XAxis
            dataKey="date"
            tickFormatter={(date) => format(new Date(date), 'MMM d')}
            tick={{ fontSize: 12 }}
          />
          <YAxis
            tickFormatter={(value) => `$${(value / 1000).toFixed(0)}k`}
            tick={{ fontSize: 12 }}
          />
          <Tooltip content={<CustomTooltip />} />
          <Legend />
          <Line
            type="monotone"
            dataKey="revenue"
            stroke="#2563eb"
            strokeWidth={2}
            dot={false}
            name="Revenue"
            activeDot={{ r: 6 }}
          />
          <Line
            type="monotone"
            dataKey="refunds"
            stroke="#ef4444"
            strokeWidth={2}
            dot={false}
            name="Refunds"
            strokeDasharray="4 4"
          />
        </LineChart>
      </ResponsiveContainer>
    </figure>
  );
}
// components/Dashboard/CategoryBarChart.tsx
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';

const COLORS = ['#2563eb', '#7c3aed', '#059669', '#d97706', '#dc2626'];

export function CategoryBarChart({ data }: { data: { category: string; orders: number }[] }) {
  const sortedData = [...data].sort((a, b) => b.orders - a.orders);

  return (
    <figure role="img" aria-label="Orders by category bar chart">
      <figcaption className="sr-only">
        Bar chart showing order counts by category.
        Top category: {sortedData[0]?.category} with {sortedData[0]?.orders} orders.
      </figcaption>
      <ResponsiveContainer width="100%" height={250}>
        <BarChart data={sortedData} layout="vertical" margin={{ left: 80 }}>
          <XAxis type="number" tick={{ fontSize: 12 }} />
          <YAxis dataKey="category" type="category" tick={{ fontSize: 12 }} width={80} />
          <Tooltip formatter={(value: number) => [`${value} orders`, 'Orders']} />
          <Bar dataKey="orders" radius={[0, 4, 4, 0]}>
            {sortedData.map((_, index) => (
              <Cell key={index} fill={COLORS[index % COLORS.length]} />
            ))}
          </Bar>
        </BarChart>
      </ResponsiveContainer>
    </figure>
  );
}

D3.js Custom Visualization

Build a force-directed graph showing relationships between our products
and the customers who buy them together.
// components/ProductRelationshipGraph.tsx
import { useEffect, useRef } from 'react';
import * as d3 from 'd3';

interface Node extends d3.SimulationNodeDatum {
  id: string;
  label: string;
  type: 'product' | 'customer';
  value: number; // Orders or revenue
}

interface Link extends d3.SimulationLinkDatum<Node> {
  strength: number; // How often bought together
}

export function ProductRelationshipGraph({ nodes, links }: { nodes: Node[]; links: Link[] }) {
  const svgRef = useRef<SVGSVGElement>(null);

  useEffect(() => {
    const svg = d3.select(svgRef.current!);
    const { width, height } = svgRef.current!.getBoundingClientRect();

    svg.selectAll('*').remove(); // Clear on re-render

    const g = svg.append('g');

    // Zoom and pan
    svg.call(d3.zoom<SVGSVGElement, unknown>().on('zoom', ({ transform }) => {
      g.attr('transform', transform);
    }));

    // Force simulation
    const simulation = d3.forceSimulation<Node>(nodes)
      .force('link', d3.forceLink<Node, Link>(links)
        .id(d => d.id)
        .distance(d => 80 / d.strength) // Stronger links → shorter distance
        .strength(d => d.strength)
      )
      .force('charge', d3.forceManyBody().strength(-200))
      .force('center', d3.forceCenter(width / 2, height / 2))
      .force('collision', d3.forceCollide(30));

    // Links
    const link = g.append('g')
      .selectAll('line')
      .data(links)
      .join('line')
      .attr('stroke', '#94a3b8')
      .attr('stroke-opacity', 0.5)
      .attr('stroke-width', d => Math.sqrt(d.strength) * 2);

    // Nodes
    const node = g.append('g')
      .selectAll('circle')
      .data(nodes)
      .join('circle')
      .attr('r', d => Math.sqrt(d.value) + 5)
      .attr('fill', d => d.type === 'product' ? '#2563eb' : '#7c3aed')
      .attr('stroke', '#fff')
      .attr('stroke-width', 2)
      .attr('tabindex', 0)
      .attr('aria-label', d => `${d.type}: ${d.label}`)
      .call(d3.drag<SVGCircleElement, 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;
        })
      );

    // Labels
    const label = g.append('g')
      .selectAll('text')
      .data(nodes)
      .join('text')
      .text(d => d.label)
      .attr('font-size', 10)
      .attr('dy', d => -Math.sqrt(d.value) - 8)
      .attr('text-anchor', 'middle')
      .attr('fill', '#374151');

    // Update positions on tick
    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('cx', d => d.x!).attr('cy', d => d.y!);
      label.attr('x', d => d.x!).attr('y', d => d.y!);
    });

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

  return (
    <figure role="img" aria-label="Force-directed graph showing product relationships">
      <figcaption className="sr-only">
        Interactive graph with {nodes.length} nodes and {links.length} connections.
        Drag nodes to reposition. Scroll to zoom.
      </figcaption>
      <svg
        ref={svgRef}
        width="100%"
        height={500}
        style={{ background: '#f8fafc', borderRadius: 8 }}
      />
    </figure>
  );
}

Real-Time Dashboard Updates

The dashboard should update live as new orders come in.
Use WebSocket for the order count, polling for the revenue chart.
// hooks/useLiveMetrics.ts
import { useState, useEffect, useCallback } from 'react';

export function useLiveOrderCount() {
  const [count, setCount] = useState<number | null>(null);

  useEffect(() => {
    const ws = new WebSocket(`${process.env.NEXT_PUBLIC_WS_URL}/metrics`);

    ws.onmessage = (event) => {
      const { type, data } = JSON.parse(event.data);
      if (type === 'order_count_update') {
        setCount(data.count);
      }
    };

    ws.onerror = () => {
      // Fall back to polling on WebSocket error
    };

    return () => ws.close();
  }, []);

  return count;
}

export function useRevenueData(days = 30) {
  const [data, setData] = useState<RevenueDataPoint[]>([]);
  const [loading, setLoading] = useState(true);

  const fetchData = useCallback(async () => {
    const res = await fetch(`/api/metrics/revenue?days=${days}`);
    const newData = await res.json();
    setData(newData);
    setLoading(false);
  }, [days]);

  useEffect(() => {
    fetchData();
    // Refresh every 5 minutes
    const interval = setInterval(fetchData, 5 * 60 * 1000);
    return () => clearInterval(interval);
  }, [fetchData]);

  return { data, loading, refresh: fetchData };
}

For building the API endpoints that feed these charts, see the API design guide. For the real-time WebSocket connections that power live updates, see the WebSocket scaling guide. The Claude Skills 360 bundle includes data visualization skill sets for Recharts, D3, and accessible dashboard patterns. Start with the free tier to try chart component generation.

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