Claude Code Plugins

Community-maintained marketplace

Feedback
0
0

Create interactive, production-grade charts with D3.js. Use this skill when the user asks to build D3.js visualizations including scatter plots, pie/donut charts, bar charts, bar chart races, line charts, line chart races, heatmaps, treemaps, or any SVG-based data visualization. Covers animated entry transitions, keyframe animations, progressive line reveals, hierarchical layouts, sequential color scales, theme-aware color schemes (with custom Streamlit-inspired palettes), tooltips, legends, and responsive design. Generates polished, accessible code that avoids generic AI aesthetics.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name d3-charts
description Create interactive, production-grade charts with D3.js. Use this skill when the user asks to build D3.js visualizations including scatter plots, pie/donut charts, bar charts, bar chart races, line charts, line chart races, heatmaps, treemaps, or any SVG-based data visualization. Covers animated entry transitions, keyframe animations, progressive line reveals, hierarchical layouts, sequential color scales, theme-aware color schemes (with custom Streamlit-inspired palettes), tooltips, legends, and responsive design. Generates polished, accessible code that avoids generic AI aesthetics.

D3.js Chart Patterns

Quick Reference

Chart Type Use Case Key APIs
Scatter Correlation, clusters, distributions scaleLinear, circle, symbol
Pie/Donut Part-to-whole, allocations pie, arc, scaleOrdinal
Bar Comparisons, rankings scaleBand, rect
Bar Race Animated rankings over time scaleBand, rect, keyframes
Line Time series, trends line, curveMonotoneX
Line Race Animated time series progression line, setInterval, dynamic domains
Heatmap Matrix data, correlations, calendars scaleBand, scaleSequential, rect
Treemap Hierarchical part-to-whole treemap, hierarchy, scaleOrdinal

Animation Patterns

Scatter Plot Entry Animation

Required behavior:

  1. X axis starts at domain [0, 0] with opacity: 0
  2. Dots positioned at x=0 (clustered left)
  3. X axis transitions to actual domain with fade-in (2s)
  4. Dots animate to final positions with staggered delay (i * 3ms)
  5. Markers/annotations appear after dots (delay 2.5s)
// 1. Initialize X scale collapsed
const xScale = d3.scaleLinear().domain([0, 0]).range([0, innerWidth]);

// 2. Create axis hidden
const xAxisGroup = g.append('g')
  .attr('transform', `translate(0, ${innerHeight})`)
  .attr('opacity', 0)
  .call(d3.axisBottom(xScale));

// 3. Add dots at initial position
const circles = chartArea.selectAll('.point')
  .data(data)
  .join('circle')
  .attr('cx', d => xScale(d.x))  // All at 0
  .attr('cy', d => yScale(d.y))
  .attr('r', 1.5)
  .attr('opacity', 0.6);

// 4. Update domain and animate
xScale.domain([xMin - padding, xMax + padding]);

xAxisGroup.transition().duration(2000)
  .attr('opacity', 1)
  .call(d3.axisBottom(xScale));

circles.transition()
  .delay((_, i) => i * 3)
  .duration(2000)
  .attr('cx', d => xScale(d.x))
  .attr('r', 3);

Pie Chart Smooth Transitions

Required behavior:

  1. Use key function for object constancy
  2. Store _current arc on DOM element
  3. Interpolate between old/new arc states
  4. Stable sort by key prevents slice jumping
const pie = d3.pie<DataItem>()
  .value(d => d.value)
  .sort((a, b) => d3.ascending(a.key, b.key));  // Stable order

svg.selectAll<SVGPathElement, PieArcDatum>('path.slice')
  .data(pie(data), d => d.data.key)  // Key function
  .join(
    enter => enter.append('path')
      .attr('class', 'slice')
      .each(function(d) { (this as any)._current = d; }),
    update => update,
    exit => exit.transition().duration(500).style('opacity', 0).remove()
  )
  .transition().duration(800)
  .attrTween('d', function(d) {
    const el = this as any;
    const interp = d3.interpolate(el._current || { startAngle: 0, endAngle: 0 }, d);
    el._current = d;
    return t => arc(interp(t)) || '';
  });

Bar Chart Race Animation

Required behavior:

  1. Data organized into keyframes (date + ranked items)
  2. Bars enter from previous rank, exit to next rank
  3. Labels animate position and interpolate values
  4. Ticker displays current time period
  5. X axis updates dynamically to top value
// === CONFIGURATION ===
const n = 12;              // Number of bars to show
const barSize = 48;        // Height per bar
const duration = 250;      // ms per keyframe
const margin = { top: 16, right: 6, bottom: 6, left: 0 };

// === SCALES ===
const x = d3.scaleLinear([0, 1], [margin.left, width - margin.right]);
const y = d3.scaleBand<number>()
  .domain(d3.range(n + 1))
  .rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
  .padding(0.1);

// === BARS UPDATE FUNCTION ===
function bars(svg: d3.Selection<SVGSVGElement, unknown, null, undefined>) {
  let bar = svg.append('g')
    .attr('fill-opacity', 0.6)
    .selectAll('rect');

  return ([date, data]: [Date, RankedItem[]], transition: d3.Transition<any, any, any, any>) => {
    bar = bar
      .data(data.slice(0, n), (d: any) => d.name)
      .join(
        enter => enter.append('rect')
          .attr('fill', d => color(d.category))
          .attr('height', y.bandwidth())
          .attr('x', x(0))
          .attr('y', d => y((prev.get(d) || d).rank)!)
          .attr('width', d => x((prev.get(d) || d).value) - x(0)),
        update => update,
        exit => exit.transition(transition).remove()
          .attr('y', d => y((next.get(d) || d).rank)!)
          .attr('width', d => x((next.get(d) || d).value) - x(0))
      )
      .call(bar => bar.transition(transition)
        .attr('y', d => y(d.rank)!)
        .attr('width', d => x(d.value) - x(0)));
  };
}

// === LABELS UPDATE FUNCTION ===
function labels(svg: d3.Selection<SVGSVGElement, unknown, null, undefined>) {
  let label = svg.append('g')
    .style('font', 'bold 12px var(--sans-serif)')
    .style('font-variant-numeric', 'tabular-nums')
    .attr('text-anchor', 'end')
    .selectAll('text');

  return ([date, data]: [Date, RankedItem[]], transition: d3.Transition<any, any, any, any>) => {
    label = label
      .data(data.slice(0, n), (d: any) => d.name)
      .join(
        enter => enter.append('text')
          .attr('transform', d => `translate(${x((prev.get(d) || d).value)},${y((prev.get(d) || d).rank)})`)
          .attr('y', y.bandwidth() / 2)
          .attr('x', -6)
          .attr('dy', '-0.25em')
          .text(d => d.name)
          .call(text => text.append('tspan')
            .attr('fill-opacity', 0.7)
            .attr('font-weight', 'normal')
            .attr('x', -6)
            .attr('dy', '1.15em')),
        update => update,
        exit => exit.transition(transition).remove()
          .attr('transform', d => `translate(${x((next.get(d) || d).value)},${y((next.get(d) || d).rank)})`)
      )
      .call(bar => bar.transition(transition)
        .attr('transform', d => `translate(${x(d.value)},${y(d.rank)})`)
        .call(g => g.select('tspan')
          .textTween((d: any) => (t: number) => 
            formatNumber(d3.interpolateNumber((prev.get(d) || d).value, d.value)(t))
          )
        )
      );
  };
}

// === TICKER (TIME DISPLAY) ===
function ticker(svg: d3.Selection<SVGSVGElement, unknown, null, undefined>) {
  const now = svg.append('text')
    .style('font', `bold ${barSize}px var(--sans-serif)`)
    .style('font-variant-numeric', 'tabular-nums')
    .attr('text-anchor', 'end')
    .attr('x', width - 6)
    .attr('y', margin.top + barSize * (n - 0.45))
    .attr('dy', '0.32em')
    .text(formatDate(keyframes[0][0]));

  return ([date]: [Date, RankedItem[]], transition: d3.Transition<any, any, any, any>) => {
    transition.end().then(() => now.text(formatDate(date)));
  };
}

// === MAIN ANIMATION LOOP ===
async function animate() {
  const updateBars = bars(svg);
  const updateLabels = labels(svg);
  const updateTicker = ticker(svg);

  for (const keyframe of keyframes) {
    const transition = svg.transition()
      .duration(duration)
      .ease(d3.easeLinear);

    x.domain([0, keyframe[1][0].value]);  // Top bar sets domain

    updateBars(keyframe, transition);
    updateLabels(keyframe, transition);
    updateTicker(keyframe, transition);

    await transition.end();
  }
}

Line Chart Race Animation

Required behavior:

  1. Data split into progressive chunks (frames)
  2. Line draws incrementally as time progresses
  3. Axes update dynamically to fit current data range
  4. Circles mark current data points with labels
  5. Time indicator shows current period
// === CONFIGURATION ===
const duration = 1000;  // ms per frame
const margin = { top: 60, right: 120, bottom: 60, left: 60 };

// === SCALES ===
const x = d3.scaleTime().range([0, innerWidth]);
const y = d3.scaleLinear().range([innerHeight, 0]);

// === LINE GENERATOR ===
const line = d3.line<DataPoint>()
  .x(d => x(d.date))
  .y(d => y(d.value))
  .curve(d3.curveMonotoneX);

// === AXES ===
const xAxis = d3.axisBottom(x).ticks(6);
const yAxis = d3.axisLeft(y).ticks(8);

// === GRADIENT DEFINITIONS ===
function createGradient(svg: d3.Selection<SVGGElement, unknown, null, undefined>, id: string, colors: string[]) {
  const gradient = svg.append('defs')
    .append('linearGradient')
    .attr('id', id)
    .attr('x1', '0%').attr('y1', '0%')
    .attr('x2', '100%').attr('y2', '0%');
  
  colors.forEach((color, i) => {
    gradient.append('stop')
      .attr('offset', `${(i / (colors.length - 1)) * 100}%`)
      .attr('stop-color', color);
  });
}

// === UPDATE FUNCTIONS ===
function updateAxis() {
  svg.select('.x-axis')
    .transition().ease(d3.easeLinear).duration(duration)
    .call(xAxis as any);
  
  svg.select('.y-axis')
    .transition().ease(d3.easeCubic).duration(duration)
    .call(yAxis as any);
}

function updateLine(data: DataPoint[], seriesIndex: number) {
  const path = svg.select(`.line-${seriesIndex}`)
    .datum(data)
    .attr('d', line);
  
  // Animate line drawing
  const totalLength = (path.node() as SVGPathElement).getTotalLength();
  path
    .attr('stroke-dasharray', `${totalLength} ${totalLength}`)
    .attr('stroke-dashoffset', totalLength)
    .transition().duration(duration).ease(d3.easeLinear)
    .attr('stroke-dashoffset', 0);
}

function updateCircle(data: DataPoint[], seriesIndex: number, color: string) {
  const lastPoint = data[data.length - 1];
  
  svg.select(`.circle-${seriesIndex}`)
    .transition().duration(duration)
    .attr('cx', x(lastPoint.date))
    .attr('cy', y(lastPoint.value))
    .attr('fill', color);
}

function updateLabel(data: DataPoint[], seriesIndex: number, label: string) {
  const lastPoint = data[data.length - 1];
  
  svg.select(`.label-${seriesIndex}`)
    .transition().duration(duration)
    .attr('x', x(lastPoint.date) + 10)
    .attr('y', y(lastPoint.value))
    .text(`${label}: ${formatValue(lastPoint.value)}`);
}

// === MAIN ANIMATION ===
function animateLineChart(frames: DataPoint[][], series: SeriesConfig[]) {
  let index = 0;
  
  function update() {
    if (index >= frames.length) {
      clearInterval(intervalId);
      return;
    }
    
    const currentData = frames[index];
    
    // Update domains
    x.domain(d3.extent(currentData, d => d.date) as [Date, Date]);
    y.domain([0, d3.max(currentData, d => d.value)! * 1.1]).nice();
    
    updateAxis();
    
    series.forEach((s, i) => {
      const seriesData = currentData.filter(d => d.series === s.key);
      updateLine(seriesData, i);
      updateCircle(seriesData, i, s.color);
      updateLabel(seriesData, i, s.label);
    });
    
    // Update time indicator
    svg.select('.time-indicator')
      .text(formatDate(currentData[currentData.length - 1].date));
    
    index++;
  }
  
  const intervalId = setInterval(update, duration);
}

Heatmap

Required behavior:

  1. Use scaleBand for both X and Y axes (categorical)
  2. Use scaleSequential with interpolator for color
  3. Rounded corners via rx/ry for modern look
  4. Interactive tooltip on hover with cell highlight
// === DATA FORMAT ===
interface HeatmapCell {
  group: string;    // X category (column)
  variable: string; // Y category (row)
  value: number;    // Cell value for color mapping
}

// === SCALES ===
const groups = Array.from(new Set(data.map(d => d.group)));
const variables = Array.from(new Set(data.map(d => d.variable)));

const x = d3.scaleBand<string>()
  .range([0, innerWidth])
  .domain(groups)
  .padding(0.05);

const y = d3.scaleBand<string>()
  .range([innerHeight, 0])
  .domain(variables)
  .padding(0.05);

// === COLOR SCALE (Sequential) ===
const colorScale = d3.scaleSequential()
  .interpolator(d3.interpolateRgbBasis(SEQUENTIAL_COLORS))  // Custom warm gradient
  .domain(d3.extent(data, d => d.value) as [number, number]);

// Alternative built-in interpolators:
// .interpolator(d3.interpolateInferno)
// .interpolator(d3.interpolateViridis)
// .interpolator(d3.interpolatePlasma)

// === AXES (minimal, no domain line) ===
svg.append('g')
  .attr('transform', `translate(0, ${innerHeight})`)
  .call(d3.axisBottom(x).tickSize(0))
  .call(g => g.select('.domain').remove());

svg.append('g')
  .call(d3.axisLeft(y).tickSize(0))
  .call(g => g.select('.domain').remove());

// === TOOLTIP ===
const tooltip = d3.select('#container')
  .append('div')
  .attr('class', 'tooltip')
  .style('opacity', 0)
  .style('position', 'absolute')
  .style('background-color', 'white')
  .style('border', '2px solid #333')
  .style('border-radius', '5px')
  .style('padding', '8px');

// === CELLS ===
svg.selectAll<SVGRectElement, HeatmapCell>('rect.cell')
  .data(data, d => `${d.group}:${d.variable}`)  // Key function
  .join('rect')
  .attr('class', 'cell')
  .attr('x', d => x(d.group)!)
  .attr('y', d => y(d.variable)!)
  .attr('rx', 4)
  .attr('ry', 4)
  .attr('width', x.bandwidth())
  .attr('height', y.bandwidth())
  .style('fill', d => colorScale(d.value))
  .style('stroke', 'none')
  .style('opacity', 0.8)
  .on('mouseenter', function(event, d) {
    tooltip.style('opacity', 1);
    d3.select(this).style('stroke', '#333').style('opacity', 1);
  })
  .on('mousemove', function(event, d) {
    tooltip
      .html(`<strong>${d.group} × ${d.variable}</strong><br/>Value: ${d.value}`)
      .style('left', `${event.pageX + 10}px`)
      .style('top', `${event.pageY - 10}px`);
  })
  .on('mouseleave', function() {
    tooltip.style('opacity', 0);
    d3.select(this).style('stroke', 'none').style('opacity', 0.8);
  });

Treemap

Required behavior:

  1. Hierarchical data with d3.hierarchy() + .sum() + .sort()
  2. Layout with d3.treemap() and tiling algorithm
  3. Color by top-level parent category
  4. Clip paths prevent text overflow
  5. Multi-line labels with value on last line
// === DATA FORMAT ===
interface TreeNode {
  name: string;
  value?: number;        // Leaf nodes have value
  children?: TreeNode[]; // Parent nodes have children
}

// === LAYOUT ===
const width = 800;
const height = 600;

const root = d3.treemap<TreeNode>()
  .tile(d3.treemapSquarify)  // Or: treemapBinary, treemapSlice, treemapDice
  .size([width, height])
  .padding(1)
  .round(true)
(
  d3.hierarchy(data)
    .sum(d => d.value || 0)
    .sort((a, b) => (b.value || 0) - (a.value || 0))
);

// === COLOR SCALE (by top-level parent) ===
const topLevelNames = data.children?.map(d => d.name) || [];
const color = d3.scaleOrdinal<string>()
  .domain(topLevelNames)
  .range(CATEGORICAL_COLORS);

// Helper: get top-level parent
function getTopParent(d: d3.HierarchyRectangularNode<TreeNode>) {
  let node = d;
  while (node.depth > 1 && node.parent) node = node.parent;
  return node;
}

// === LEAVES ===
const leaf = svg.selectAll<SVGGElement, d3.HierarchyRectangularNode<TreeNode>>('g.leaf')
  .data(root.leaves())
  .join('g')
  .attr('class', 'leaf')
  .attr('transform', d => `translate(${d.x0}, ${d.y0})`);

// === TOOLTIP (via <title>) ===
const format = d3.format(',d');
leaf.append('title')
  .text(d => `${d.ancestors().reverse().map(n => n.data.name).join(' → ')}\n${format(d.value || 0)}`);

// === RECTANGLES ===
leaf.append('rect')
  .attr('id', (d, i) => `leaf-${i}`)
  .attr('fill', d => color(getTopParent(d).data.name))
  .attr('fill-opacity', 0.6)
  .attr('width', d => d.x1 - d.x0)
  .attr('height', d => d.y1 - d.y0)
  .attr('rx', 2);

// === CLIP PATHS ===
leaf.append('clipPath')
  .attr('id', (d, i) => `clip-${i}`)
  .append('rect')
  .attr('width', d => d.x1 - d.x0)
  .attr('height', d => d.y1 - d.y0);

// === MULTI-LINE LABELS ===
leaf.append('text')
  .attr('clip-path', (d, i) => `url(#clip-${i})`)
  .selectAll('tspan')
  .data(d => {
    // Split name on camelCase or spaces, add formatted value
    const nameParts = d.data.name.split(/(?=[A-Z][a-z])|\s+/g);
    return [...nameParts, format(d.value || 0)];
  })
  .join('tspan')
  .attr('x', 3)
  .attr('y', (_, i, nodes) => `${(i === nodes.length - 1 ? 0.3 : 0) + 1.1 + i * 0.9}em`)
  .attr('fill-opacity', (_, i, nodes) => i === nodes.length - 1 ? 0.7 : 1)
  .attr('font-size', '10px')
  .text(d => d);

Color Schemes

Custom Color Palette (Streamlit-Inspired)

// Categorical colors (for discrete data: pie, bar, legend)
const CATEGORICAL_COLORS = [
  '#204F80',  // Deep blue
  '#804F1F',  // Warm brown
  '#0A2845',  // Navy
  '#426F99',  // Steel blue
  '#45280A',  // Dark brown
  '#996F42',  // Tan
  '#FF6B6B',  // Coral
  '#4ECDC4',  // Teal
  '#45B7D1',  // Sky blue
  '#96CEB4',  // Sage green
];

// Sequential colors (for continuous data: heatmaps, gradients)
const SEQUENTIAL_COLORS = [
  '#FDF2C5',  // Light cream
  '#FCE584',  // Pale yellow
  '#FBD453',  // Golden yellow
  '#FBC030',  // Amber
  '#F49F1E',  // Orange
  '#DC7702',  // Deep orange
  '#B85300',  // Burnt orange
  '#8F4014',  // Brown
  '#793207',  // Dark brown
  '#441B06',  // Near black
];

// Marker/highlight colors
const MARKER_COLORS = {
  primary:   '#804F1F',  // Warm brown (Max Sharpe style)
  secondary: '#204F80',  // Deep blue (Min Volatility style)
  tertiary:  '#45280A',  // Dark brown (Max Utility style)
};

Sequential (Continuous Data)

For heatmaps, scatter color encoding, gradients:

import * as d3Chromatic from 'd3-scale-chromatic';

const sequentialSchemes: Record<string, (t: number) => string> = {
  // Custom warm gradient (cream → dark brown)
  custom:  d3.interpolateRgb('#FDF2C5', '#441B06'),
  // Multi-stop custom gradient
  customMulti: d3.interpolateRgbBasis(SEQUENTIAL_COLORS),
  // Built-in schemes
  inferno: d3Chromatic.interpolateInferno,
  plasma:  d3Chromatic.interpolatePlasma,
  viridis: d3Chromatic.interpolateViridis,
  warm:    d3Chromatic.interpolateWarm,
  cool:    d3Chromatic.interpolateCool,
  magma:   d3Chromatic.interpolateMagma,
};

const colorScale = d3.scaleSequential()
  .domain(d3.extent(data, d => d.value) as [number, number])
  .interpolator(sequentialSchemes.custom);

Categorical (Discrete Data)

For pie charts, grouped bars, legends:

const categoricalSchemes: Record<string, readonly string[]> = {
  custom:     CATEGORICAL_COLORS,
  category10: d3Chromatic.schemeCategory10,
  set1:       d3Chromatic.schemeSet1,
  set2:       d3Chromatic.schemeSet2,
  set3:       d3Chromatic.schemeSet3,
  pastel1:    d3Chromatic.schemePastel1,
  pastel2:    d3Chromatic.schemePastel2,
  dark2:      d3Chromatic.schemeDark2,
};

const colorScale = d3.scaleOrdinal<string>()
  .domain(data.map(d => d.category))
  .range(categoricalSchemes.custom);

Theme-Aware Colors

Light and dark mode support with Streamlit-inspired palette:

function getThemeColors() {
  const isDark = document.documentElement.classList.contains('dark');
  return {
    // Base colors
    base:        isDark ? '#0E1117' : '#FFFFFF',
    text:        isDark ? '#FAFAFA' : '#1F1916',
    subText:     isDark ? '#E5E7EB' : '#56524D',
    primary:     isDark ? '#FF4B4B' : '#56524D',
    // Chart elements
    axis:        isDark ? '#4B5563' : '#2B2523',
    grid:        isDark ? '#374151' : '#E4E4E4',
    border:      isDark ? '#4B5563' : '#2B2523',
    // Interactive elements
    tooltipBg:   isDark ? '#262730' : '#FFFFFF',
    tooltipBorder: isDark ? '#4B5563' : '#2B2523',
    // Sidebar (if applicable)
    sidebarBg:   isDark ? '#1A1D26' : '#D4D4D4',
    sidebarText: isDark ? '#E5E7EB' : '#1F1916',
  };
}

// Update chart on theme change
window.addEventListener('themechange', () => {
  const colors = getThemeColors();
  svg.selectAll('.x-axis .domain').attr('stroke', colors.axis);
  svg.selectAll('.y-axis .domain').attr('stroke', colors.axis);
  svg.selectAll('.tick line').attr('stroke', colors.axis);
  svg.selectAll('.tick text').attr('fill', colors.subText);
});

// Dispatch from toggle button
window.dispatchEvent(new Event('themechange'));

Interactive Elements

Tooltip Pattern

const tooltip = d3.select('#tooltip');

selection
  .on('mouseenter', (event, d) => {
    d3.select(event.currentTarget).attr('opacity', 1);
    tooltip
      .style('opacity', '1')
      .style('left', `${event.pageX + 10}px`)
      .style('top', `${event.pageY - 10}px`)
      .html(`<strong>${d.label}</strong><br/>Value: ${d.value}`);
  })
  .on('mouseleave', (event) => {
    d3.select(event.currentTarget).attr('opacity', 0.6);
    tooltip.style('opacity', '0');
  });

Legend with Toggle

const legendItems = legend.selectAll('.legend-item')
  .data(categories)
  .join('g')
  .attr('class', 'legend-item')
  .style('cursor', 'pointer')
  .on('click', (_, d) => {
    const points = svg.selectAll(`.point-${d.key}`);
    const visible = points.attr('opacity') > 0.3;
    points.transition().duration(300).attr('opacity', visible ? 0.1 : 0.6);
  });

Symbol Markers

const symbols: Record<string, d3.SymbolType> = {
  circle:   d3.symbolCircle,
  star:     d3.symbolStar,
  diamond:  d3.symbolDiamond,
  square:   d3.symbolSquare,
  triangle: d3.symbolTriangle,
  cross:    d3.symbolCross,
};

g.append('path')
  .attr('d', d3.symbol().type(symbols.star).size(200)())
  .attr('fill', '#804F1F')
  .attr('stroke', '#fff')
  .attr('stroke-width', 2);

Structural Patterns

Margin Convention

const margin = { top: 40, right: 30, bottom: 60, left: 65 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

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

Clip Path for Overflow

svg.append('defs')
  .append('clipPath')
  .attr('id', 'chart-clip')
  .append('rect')
  .attr('width', innerWidth)
  .attr('height', innerHeight);

const chartArea = g.append('g').attr('clip-path', 'url(#chart-clip)');

Axis Formatting

// Percentage
.tickFormat(d => `${(+d * 100).toFixed(0)}%`)

// Currency
.tickFormat(d => `$${d3.format(',.0f')(d)}`)

// SI prefix (K, M, B)
.tickFormat(d3.format('.2s'))

// Date
.tickFormat(d3.timeFormat('%b %Y'))

Complete Examples

See references/scatter-template.md for full scatter plot implementation. See references/pie-template.md for full pie/donut chart implementation. See references/bar-race-template.md for full bar chart race implementation. See references/line-race-template.md for full line chart race implementation. See references/heatmap-template.md for full heatmap implementation. See references/treemap-template.md for full treemap implementation. See references/color-schemes.md for comprehensive color palette reference.