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.
.cursor/rules/d3.mdcWhat 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
fillandfont-sizeout ofattr()into CSS classes - use
d3-zoominstead 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
fillandfont-size, add CSS classes for the axes
How to install d3
Manually: put the files in place
.cursor/rules/d3.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/d3.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/d3.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/d3.mdManually: put the files in place
.clinerules/d3.mdStep 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