Skip to main content
rule

d3 — Cursor rule: D3.js v7+ best practices

A Cursor rule that tells the agent how to write D3.js v7+ charts: modular imports, selection.join(), external CSS for styling.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/d3.mdc
Open on GitHub

What d3 does

The rule applies to **/*.{js,jsx} files and acts as a style guide for the agent. Visualization logic stays split into focused functions — data loading, scales, axes, element rendering — instead of one long file.

Imports are the first thing it tightens. Pull in d3-selection, d3-scale, d3-axis, d3-array individually rather than the whole library, so tree-shaking actually works. For data binding it pushes selection.join() with a key function, and for zoom, drag and brush it points at d3-zoom, d3-drag and d3-brush. Hand-rolled mousedown and mousemove handlers for zoom are called out as a dead end.

Visual properties like fill, stroke and font-size belong in CSS classes, not in attr() calls. If your codebase already styles D3 elements inline, expect the agent to rewrite them — worth reading the diff before you commit.

What it helps with

  • split a chart into separate functions instead of one long module
  • replace manual enter/update/exit with selection.join()
  • cut bundle size by importing individual D3 modules
  • move fill and font-size out of attr() into CSS classes
  • use d3-zoom instead of custom mouse event handlers

Example prompts for the agent

  • Rewrite this chart for D3 v7 with individual module imports and selection.join()
  • Split the visualization into functions: data, scales, axes, rendering
  • Drop the inline fill and font-size, add CSS classes for the axes

How to install d3

Manually: put the files in place

.cursor/rules/d3.mdc

Frontmatter: description, globs, alwaysApply

Step by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules

Questions about d3

How do I install the d3 rule in Cursor?

Create .cursor/rules/d3.mdc and paste the rule contents into it. Cursor picks it up for files matching the **/*.{js,jsx} glob.

What does the d3 rule do?

It gives the agent a set of D3.js v7+ practices: modular imports, selection.join() for data binding, dedicated behavior modules for zoom and drag, and styling through external CSS.

Which files does the rule cover?

JavaScript and JSX files matching **/*.{js,jsx}. In a TypeScript project it won't apply until you widen the glob.

Source text

d3.mdc · CC0-1.0
---
description: This guide provides definitive, actionable best practices for writing modern D3.js v7+ code, focusing on modularity, performance, and maintainability.
globs: **/*.{js,jsx}
---
# d3 Best Practices

D3.js is a powerful, low-level library for data visualization. To leverage its full potential and maintain a clean, performant codebase, adhere to these definitive guidelines.

## 1. Code Organization and Structure

Always structure your D3 visualizations with clear separation of concerns. This enhances readability, testability, and reusability.

### 1.1. Modular Imports

Import only the specific D3 modules you need. This is critical for bundle size optimization and tree-shaking.

❌ **BAD: Over-importing**
```javascript
// Imports the entire D3 library, increasing bundle size unnecessarily.
import * as d3 from 'd3'; 

// Or using a CDN for the full library
// <script src="https://d3js.org/d3.v7.min.js"></script>
```

✅ **GOOD: Targeted Imports**
```javascript
// Import only necessary modules for a simple bar chart
import { select } from 'd3-selection';
import { scaleLinear, scaleBand } from 'd3-scale';
import { axisBottom, axisLeft } from 'd3-axis';
import { max } from 'd3-array';
```

### 1.2. Functional Encapsulation

Organize your visualization logic into distinct, focused functions. A single function should handle data loading, another for scale creation, another for axis rendering, and so on.

```javascript
// chart.js
import { select } from 'd3-selection';
import { scaleLinear, scaleBand } from 'd3-scale';
import { axisBottom, axisLeft } from 'd3-axis';
import { max } from 'd3-array';

export function createBarChart(containerSelector, data, options = {}) {
  const { width = 800, height = 500, margin = { top: 20, right: 20, bottom: 30, left: 40 } } = options;

  const innerWidth = width - margin.left - margin.right;
  const innerHeight = height - margin.top - margin.bottom;

  const svg = select(containerSelector)
    .append('svg')
    .attr('viewBox', `0 0 ${width} ${height}`) // Responsive design
    .attr('role', 'img')
    .attr('aria-label', 'Bar chart showing data distribution');

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

  // 1. Create Scales
  const xScale = scaleBand()
    .domain(data.map(d => d.category))
    .range([0, innerWidth])
    .padding(0.1);

  const yScale = scaleLinear()
    .domain([0, max(data, d => d.value)])
    .range([innerHeight, 0]);

  // 2. Render Axes
  g.append('g')
    .attr('class', 'x-axis') // Style with CSS
    .attr('transform', `translate(0,${innerHeight})`)
    .call(axisBottom(xScale));

  g.append('g')
    .attr('class', 'y-axis') // Style with CSS
    .call(axisLeft(yScale));

  // 3. Draw Elements (Bars)
  g.selectAll('.bar')
    .data(data)
    .join('rect')
      .attr('class', 'bar')
      .attr('x', d => xScale(d.category))
      .attr('y', d => yScale(d.value))
      .attr('width', xScale.bandwidth())
      .attr('height', d => innerHeight - yScale(d.value));

  // Add labels, title, etc. as separate concerns
}

// main.js
import { createBarChart } from './chart.js';

async function init() {
  const chartData = await fetch('/api/data').then(res => res.json());
  createBarChart('#chart-container', chartData);
}

init();
```

## 2. Common Patterns and Anti-patterns

Embrace D3's idiomatic patterns for data binding and DOM manipulation.

### 2.1. The `selection.join()` Pattern

Always use `selection.join()` for data-driven element creation, update, and removal. It is the most concise and efficient way to handle the enter, update, and exit selections.

❌ **BAD: Manual Enter/Update/Exit (D3 v3/v4 style)**
```javascript
const bars = g.selectAll('.bar')
  .data(data);

// Exit
bars.exit().remove();

// Enter
bars.enter().append('rect')
  .attr('class', 'bar')
  .attr('x', d => xScale(d.category))
  .attr('width', xScale.bandwidth())
  .attr('y', innerHeight) // Initial position for animation
  .attr('height', 0) // Initial height for animation
  .transition().duration(500) // Animate enter
    .attr('y', d => yScale(d.value))
    .attr('height', d => innerHeight - yScale(d.value));

// Update (merged selection)
bars.transition().duration(500)
  .attr('x', d => xScale(d.category))
  .attr('y', d => yScale(d.value))
  .attr('width', xScale.bandwidth())
  .attr('height', d => innerHeight - yScale(d.value));
```

✅ **GOOD: `selection.join()` (D3 v5+ style)**
```javascript
g.selectAll('.bar')
  .data(data, d => d.category) // Use a key function for stable joins
  .join(
    enter => enter.append('rect')
      .attr('class', 'bar')
      .attr('x', d => xScale(d.category))
      .attr('y', innerHeight) // Start from bottom for animation
      .attr('width', xScale.bandwidth())
      .attr('height', 0) // Start with zero height for animation
      .call(enter => enter.transition().duration(500)
        .attr('y', d => yScale(d.value))
        .attr('height', d => innerHeight - yScale(d.value))),
    update => update.transition().duration(500)
      .attr('x', d => xScale(d.category))
      .attr('y', d => yScale(d.value))
      .attr('width', xScale.bandwidth())
      .attr('height', d => innerHeight - yScale(d.value)),
    exit => exit.transition().duration(500)
      .attr('y', innerHeight) // Animate to bottom
      .attr('height', 0) // Animate to zero height
      .remove()
  );
```

### 2.2. External CSS for Styling

Style D3 elements using external CSS classes. Avoid inline `style()` or `attr()` calls for visual properties like `fill`, `stroke`, `font-size`. This improves maintainability, themeability, and accessibility.

❌ **BAD: Inline Styling**
```javascript
g.append('g')
  .attr('transform', `translate(0,${innerHeight})`)
  .call(axisBottom(xScale))
  .selectAll('text')
    .attr('font-size', '12px')
    .attr('fill', '#333');
```

✅ **GOOD: CSS Classes**
```javascript
// In your JavaScript
g.append('g')
  .attr('class', 'x-axis') // Apply a class
  .attr('transform', `translate(0,${innerHeight})`)
  .call(axisBottom(xScale));

// In your CSS file (e.g., style.css)
.x-axis text {
  font-size: 12px;
  fill: #333;
}
.x-axis path, .x-axis line {
  stroke: currentColor; /* Inherits color from parent */
}
```

### 2.3. D3 Behaviors for Interaction

For common interactions like zooming, panning, and dragging, use D3's dedicated behavior modules (`d3-zoom`, `d3-drag`, `d3-brush`). They handle complex event logic and browser quirks robustly.

❌ **BAD: Manual Zoom/Pan Implementation**
```javascript
// Don't try to reimplement zoom/pan with raw mouse events.
svg.on('mousedown', handleMouseDown);
svg.on('mousemove', handleMouseMove);
// ... this path leads to bugs and frustration.
```

✅ **GOOD: Using `d3-zoom`**
```javascript
import { zoom, zoomIdentity } from 'd3-zoom';
import { select } from 'd3-selection';

function zoomed({ transform }) {
  // Apply the transform to your chart elements
  g.attr('transform', transform);
  // Update axes if necessary
  xAxisGroup.call(axisBottom(transform.rescaleX(xScale)));
  yAxisGroup.call(axisLeft(transform.rescaleY(yScale)));
}

const chartZoom = zoom()
  .scaleExtent([0.5, 10]) // Min/max zoom level
  .translateExtent([[0, 0], [width, height]]) // Pan boundaries
  .on('zoom', zoomed);

select('#chart-container svg')
  .call(chartZoom)
  .call(chartZoom.transform, zoomIdentity); // Initialize zoom
```

## 3. Performance Considerations

Optimize D3 charts for smooth interactions and efficient rendering, especially with large datasets.

### 3.1. Use `viewBox` for Responsiveness

Always use the `viewBox` attribute on your SVG element for responsive scaling. Avoid manually recalculating and setting `width`/`height` attributes on resize events.

❌ **BAD: Fixed Dimensions, Manual Resize Handling**
```javascript
const svg = d3.select('#chart').append('svg')
  .attr('width', 960)
  .attr('height', 500);

// Requires complex resize observer logic to update width/height and redraw
window.addEventListener('resize', () => { /* ... */ });
```

✅ **GOOD: `viewBox` and CSS**
```javascript
// In your JavaScript
const svg = d3.select('#chart').append('svg')
  .attr('viewBox', `0 0 ${width} ${height}`); // Logical dimensions

// In your CSS
#chart svg {
  display: block; /* Remove extra space below SVG */
  width: 100%;    /* Scale to container width */
  height: auto;   /* Maintain aspect ratio */
}
```

### 3.2. Minimize DOM Operations

Batch DOM updates and avoid unnecessary re-renders. `selection.join()` inherently helps with this by efficiently managing enter/update/exit.

### 3.3. Consider Canvas for Large Datasets

For extremely large datasets (thousands or millions of elements), SVG can become slow. D3 can draw to HTML Canvas elements, which offer superior performance for high-volume rendering.

```javascript
// Example: Drawing a scatter plot on Canvas
import { select } from 'd3-selection';
// ... other D3 modules

const canvas = select('#chart-container')
  .append('canvas')
  .attr('width', width)
  .attr('height', height)
  .node();

const context = canvas.getContext('2d');

function drawCanvasChart(data) {
  context.clearRect(0, 0, width, height);
  context.fillStyle = 'steelblue';
  data.forEach(d => {
    context.beginPath();
    context.arc(xScale(d.x), yScale(d.y), 3, 0, 2 * Math.PI);
    context.fill();
  });
}

// Call drawCanvasChart(data) whenever data or scales change.
```

## 4. Common Pitfalls and Gotchas

Avoid these common mistakes to prevent bugs and maintain a robust D3 application.

### 4.1. Forgetting Key Functions with `data()`

When updating data, always provide a key function to `selection.data(newData, d => d.id)`. This tells D3 how to uniquely identify data points, ensuring stable joins and correct element transitions.

❌ **BAD: No Key Function (unstable joins)**
```javascript
// If data order changes or items are added/removed, D3 might re-bind incorrectly.
g.selectAll('.bar').data(data)
  .join(/* ... */);
```

✅ **GOOD: With Key Function**
```javascript
// D3 uses d.category to match old and new data points.
g.selectAll('.bar').data(data, d => d.category)
  .join(/* ... */);
```

### 4.2. Incorrectly Updating Scales and Axes

When data or dimensions change, you must update the scales' domains/ranges *before* calling the axis generators again.

❌ **BAD: Updating Axes without updating Scales**
```javascript
// This will render axes based on old scale domains/ranges.
xAxisGroup.call(axisBottom(xScale)); // xScale's domain/range might be stale
```

✅ **GOOD: Update Scales, then Axes**
```javascript
xScale.domain(data.map(d => d.category));
yScale.domain([0, max(data, d => d.value)]);

xAxisGroup.transition().duration(500).call(axisBottom(xScale));
yAxisGroup.transition().duration(500).call(axisLeft(yScale));
```

### 4.3. Ignoring Accessibility (ARIA Attributes)

D3 charts are often visual. For screen readers and other assistive technologies, add semantic SVG elements and ARIA attributes.

```javascript
const svg = select(containerSelector)
  .append('svg')
  .attr('role', 'img') // Indicate it's an image
  .attr('aria-label', 'Bar chart showing monthly sales data'); // Descriptive label

// For individual elements
g.selectAll('.bar')
  .data(data, d => d.category)
  .join('rect')
    .attr('aria-label', d => `Sales for ${d.category}: ${d.value}`);
```

## 5. Testing Approaches

Ensure the reliability and correctness of your D3 visualizations.

### 5.1. Unit Testing Pure Functions

Test your data transformation, scale creation, and utility functions independently. These are typically pure functions, making them easy to test.

```javascript
// data-utils.js
export function processRawData(rawData) {
  return rawData.map(d => ({
    category: d.name,
    value: +d.amount // Ensure numeric type
  }));
}

// data-utils.test.js (using Jest)
import { processRawData } from './data-utils';

test('processRawData converts amount to number', () => {
  const raw = [{ name: 'A', amount: '100' }];
  const processed = processRawData(raw);
  expect(processed[0].value).toBe(100);
});
```

### 5.2. Visual Regression Testing

For visual output, implement visual regression tests using tools like Playwright or Cypress. These tools capture screenshots of your charts and compare them against baseline images, flagging any unintended visual changes.

```javascript
// cypress/e2e/chart.cy.js
describe('Bar Chart Visuals', () => {
  it('should render correctly', () => {
    cy.visit('/charts/bar-chart'); // Your chart page
    cy.get('#chart-container svg').compareSnapshot('bar-chart-initial');
  });

  it('should update correctly on data change', () => {
    cy.visit('/charts/bar-chart?data=updated'); // Page with updated data
    cy.get('#chart-container svg').compareSnapshot('bar-chart-updated');
  });
});
```

### 5.3. Observable Notebooks for Prototyping

Use Observable notebooks for rapid prototyping, sharing, and testing D3 code snippets. They provide an interactive environment to validate visual output and explore D3 patterns.

© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc

Similar IDE rules

All IDE rules for Frontend and UI →

From the same set69

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor