d3 — правило для Cursor: лучшие практики D3.js v7+
Правило для Cursor задаёт, как писать код визуализаций на D3.js v7+: модульные импорты, selection.join() вместо ручного enter/update/exit, стили через CSS.
.cursor/rules/d3.mdcЧто делает d3
Правило подключается к файлам **/*.{js,jsx} и работает как инструкция для агента: он держит структуру визуализации разделённой на функции — загрузка данных, шкалы, оси, отрисовка элементов — вместо одной длинной простыни.
Главное требование — импортировать только нужные модули (d3-selection, d3-scale, d3-axis, d3-array), а не всю библиотеку целиком. Для привязки данных правило настаивает на selection.join() с ключевой функцией, а для зума, перетаскивания и выделения — на готовых модулях d3-zoom, d3-drag, d3-brush. Своя обработка mousedown и mousemove для зума — прямой путь к багам, и правило это отдельно проговаривает.
Визуальные свойства вроде fill, stroke и font-size выносятся в CSS-классы, а не в attr(). Если в проекте уже есть инлайновые стили на D3-элементах, агент начнёт их переписывать — это повод проверить дифф перед коммитом.
Какие задачи решает
- разбить визуализацию на отдельные функции вместо одного длинного модуля
- перейти с ручного enter/update/exit на
selection.join() - уменьшить бандл, заменив импорт всей библиотеки на точечные модули
- вынести цвета и размеры шрифтов из
attr()в CSS-классы - подключить
d3-zoomвместо самописной обработки событий мыши
Примеры запросов агенту
- Перепиши этот график на D3 v7: импортируй только нужные модули и используй
selection.join() - Раздели код визуализации на функции: данные, шкалы, оси, отрисовка
- Убери инлайновые
fillиfont-size, заведи CSS-классы для осей
Как установить d3
Вручную: положить файлы
.cursor/rules/d3.mdcВо frontmatter: description, globs, alwaysApply
Вручную: положить файлы
.claude/rules/d3.mdПоле paths во frontmatter ограничивает файлы: globs из источника → paths
AGENTS.md
Codex читает AGENTS.md: вставьте текст правила туда
Вручную: положить файлы
.github/instructions/d3.instructions.mdПоле applyTo задаёт, к каким файлам применять: globs из источника → applyTo
Вручную: положить файлы
.devin/rules/d3.mdВручную: положить файлы
.clinerules/d3.mdПо шагам, с командами для каждой платформы: Cursor rules: файлы .mdc, четыре режима и замена .cursorrules
Вопросы о d3
Как установить правило d3 в Cursor?
Создайте файл .cursor/rules/d3.mdc и вставьте в него содержимое правила. Cursor подхватит его для файлов, подходящих под glob **/*.{js,jsx}.
Что делает правило d3?
Задаёт агенту набор практик для D3.js v7+: модульные импорты, selection.join() для привязки данных, готовые модули поведения для зума и перетаскивания, стили через внешний CSS.
На какие файлы распространяется правило?
На JavaScript и JSX-файлы по маске **/*.{js,jsx}. В проектах с TypeScript правило не сработает, пока вы не расширите glob.
Исходный текст
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