D3.js Skill

by chrisvoncsefalvay (community) · Claude, d3.js, JavaScript, React, Vue, Svelte

Skill AI Assistants Open Source активный

Claude skill для работы с d3.js — визуализация данных.


Инструкция по установке не найдена в README проекта — возможно, она описана только во внешней документации. Ссылки на репозиторий смотрите на вкладке «About».

переведено ИИ

name: d3-viz description: Создание интерактивных визуализаций данных с использованием d3.js. Используйте этот skill при создании кастомных графиков, диаграмм, сетевых схем, географических визуализаций или любой сложной SVG-визуализации данных, требующей тонкого контроля над визуальными элементами, переходами или взаимодействиями. Используйте для нестандартных визуализаций сверх стандартных библиотек чартинга — на React, Vue, Svelte, чистом JavaScript или в любой другой среде.


Визуализация D3.js

Обзор

Этот skill предоставляет руководство по созданию сложных интерактивных визуализаций данных с использованием d3.js. D3.js (Data-Driven Documents) отлично справляется с привязкой данных к DOM-элементам и применением управляемых данными трансформаций для создания кастомных визуализаций публикационного качества с точным контролем над каждым визуальным элементом. Техники работают в любой JavaScript-среде, включая чистый JavaScript, React, Vue, Svelte и другие фреймворки.

Когда использовать d3.js

Используйте d3.js для: - Кастомных визуализаций, требующих уникальных визуальных кодировок или layout'ов - Интерактивных исследований со сложным поведением pan, zoom или brush - Сетевых/графовых визуализаций (force-directed layout'ы, древовидные диаграммы, иерархии, chord-диаграммы) - Географических визуализаций с кастомными проекциями - Визуализаций, требующих плавных, хореографированных переходов - Графики публикационного качества с тонким контролем стилизации - Новых типов чартов, недоступных в стандартных библиотеках

Рассмотрите альтернативы для: - 3D-визуализаций — используйте вместо этого Three.js

Основной воркфлоу

1. Настройка d3.js

Импортируйте d3 в начале скрипта:

import * as d3 from 'd3';

Или используйте версию через CDN (7.x):

<script src="https://d3js.org/d3.v7.min.js"></script>

Все модули (шкалы, оси, формы, переходы и т.д.) доступны через namespace d3.

2. Выберите паттерн интеграции

Паттерн A: Прямая манипуляция DOM (рекомендуется в большинстве случаев) Используйте d3, чтобы выбирать DOM-элементы и манипулировать ими императивно. Работает в любой JavaScript-среде:

function drawChart(data) {
  if (!data || data.length === 0) return;

  const svg = d3.select('#chart'); // Выбор по ID, классу или DOM-элементу

  // Очистить предыдущее содержимое
  svg.selectAll("*").remove();

  // Задать размеры
  const width = 800;
  const height = 400;
  const margin = { top: 20, right: 30, bottom: 40, left: 50 };

  // Создать шкалы, оси и отрисовать визуализацию
  // ... код d3 здесь ...
}

// Вызывать при изменении данных
drawChart(myData);

Паттерн B: Декларативный рендеринг (для фреймворков с шаблонизацией) Используйте d3 для вычислений данных (шкалы, layout'ы), но рендерите элементы через ваш фреймворк:

function getChartElements(data) {
  const xScale = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.value)])
    .range([0, 400]);

  return data.map((d, i) => ({
    x: 50,
    y: i * 30,
    width: xScale(d.value),
    height: 25
  }));
}

// В React: {getChartElements(data).map((d, i) => <rect key={i} {...d} fill="steelblue" />)}
// В Vue: директива v-for над возвращённым массивом
// В чистом JS: создавайте элементы вручную из возвращённых данных

Используйте Паттерн A для сложных визуализаций с переходами, взаимодействиями или когда используете d3 по полной. Используйте Паттерн B для более простых визуализаций или когда ваш фреймворк предпочитает декларативный рендеринг.

3. Структурируйте код визуализации

Следуйте этой стандартной структуре в функции отрисовки:

function drawVisualization(data) {
  if (!data || data.length === 0) return;

  const svg = d3.select('#chart'); // Или передайте селектор/элемент
  svg.selectAll("*").remove(); // Очистить предыдущий рендер

  // 1. Определить размеры
  const width = 800;
  const height = 400;
  const margin = { top: 20, right: 30, bottom: 40, left: 50 };
  const innerWidth = width - margin.left - margin.right;
  const innerHeight = height - margin.top - margin.bottom;

  // 2. Создать основную группу с отступами
  const g = svg.append("g")
    .attr("transform", `translate(${margin.left},${margin.top})`);

  // 3. Создать шкалы
  const xScale = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.x)])
    .range([0, innerWidth]);

  const yScale = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.y)])
    .range([innerHeight, 0]); // Примечание: инвертировано для SVG-координат

  // 4. Создать и добавить оси
  const xAxis = d3.axisBottom(xScale);
  const yAxis = d3.axisLeft(yScale);

  g.append("g")
    .attr("transform", `translate(0,${innerHeight})`)
    .call(xAxis);

  g.append("g")
    .call(yAxis);

  // 5. Привязать данные и создать визуальные элементы
  g.selectAll("circle")
    .data(data)
    .join("circle")
    .attr("cx", d => xScale(d.x))
    .attr("cy", d => yScale(d.y))
    .attr("r", 5)
    .attr("fill", "steelblue");
}

// Вызывать при изменении данных
drawVisualization(myData);

4. Реализуйте адаптивный размер

Сделайте визуализации адаптивными к размеру контейнера:

function setupResponsiveChart(containerId, data) {
  const container = document.getElementById(containerId);
  const svg = d3.select(`#${containerId}`).append('svg');

  function updateChart() {
    const { width, height } = container.getBoundingClientRect();
    svg.attr('width', width).attr('height', height);

    // Перерисовать визуализацию с новыми размерами
    drawChart(data, svg, width, height);
  }

  // Обновить при начальной загрузке
  updateChart();

  // Обновить при изменении размера окна
  window.addEventListener('resize', updateChart);

  // Вернуть функцию очистки
  return () => window.removeEventListener('resize', updateChart);
}

// Использование:
// const cleanup = setupResponsiveChart('chart-container', myData);
// cleanup(); // Вызвать при размонтировании компонента или удалении элемента

Или используйте ResizeObserver для более прямого мониторинга контейнера:

function setupResponsiveChartWithObserver(svgElement, data) {
  const observer = new ResizeObserver(() => {
    const { width, height } = svgElement.getBoundingClientRect();
    d3.select(svgElement)
      .attr('width', width)
      .attr('height', height);

    // Перерисовать визуализацию
    drawChart(data, d3.select(svgElement), width, height);
  });

  observer.observe(svgElement.parentElement);
  return () => observer.disconnect();
}

Типичные паттерны визуализации

Столбчатая диаграмма

function drawBarChart(data, svgElement) {
  if (!data || data.length === 0) return;

  const svg = d3.select(svgElement);
  svg.selectAll("*").remove();

  const width = 800;
  const height = 400;
  const margin = { top: 20, right: 30, bottom: 40, left: 50 };
  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})`);

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

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

  g.append("g")
    .attr("transform", `translate(0,${innerHeight})`)
    .call(d3.axisBottom(xScale));

  g.append("g")
    .call(d3.axisLeft(yScale));

  g.selectAll("rect")
    .data(data)
    .join("rect")
    .attr("x", d => xScale(d.category))
    .attr("y", d => yScale(d.value))
    .attr("width", xScale.bandwidth())
    .attr("height", d => innerHeight - yScale(d.value))
    .attr("fill", "steelblue");
}

// Использование:
// drawBarChart(myData, document.getElementById('chart'));

Линейный график

const line = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.value))
  .curve(d3.curveMonotoneX); // Плавная кривая

g.append("path")
  .datum(data)
  .attr("fill", "none")
  .attr("stroke", "steelblue")
  .attr("stroke-width", 2)
  .attr("d", line);

Диаграмма рассеяния

g.selectAll("circle")
  .data(data)
  .join("circle")
  .attr("cx", d => xScale(d.x))
  .attr("cy", d => yScale(d.y))
  .attr("r", d => sizeScale(d.size)) // Опционально: кодирование размера
  .attr("fill", d => colourScale(d.category)) // Опционально: кодирование цвета
  .attr("opacity", 0.7);

Chord-диаграмма

Chord-диаграмма показывает связи между сущностями в круговом layout'е, с лентами, представляющими потоки между ними:

function drawChordDiagram(data) {
  // формат данных: массив объектов с source, target и value
  // Пример: [{ source: 'A', target: 'B', value: 10 }, ...]

  if (!data || data.length === 0) return;

  const svg = d3.select('#chart');
  svg.selectAll("*").remove();

  const width = 600;
  const height = 600;
  const innerRadius = Math.min(width, height) * 0.3;
  const outerRadius = innerRadius + 30;

  // Создать матрицу из данных
  const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target])));
  const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0));

  data.forEach(d => {
    const i = nodes.indexOf(d.source);
    const j = nodes.indexOf(d.target);
    matrix[i][j] += d.value;
    matrix[j][i] += d.value;
  });

  // Создать chord layout
  const chord = d3.chord()
    .padAngle(0.05)
    .sortSubgroups(d3.descending);

  const arc = d3.arc()
    .innerRadius(innerRadius)
    .outerRadius(outerRadius);

  const ribbon = d3.ribbon()
    .source(d => d.source)
    .target(d => d.target);

  const colourScale = d3.scaleOrdinal(d3.schemeCategory10)
    .domain(nodes);

  const g = svg.append("g")
    .attr("transform", `translate(${width / 2},${height / 2})`);

  const chords = chord(matrix);

  // Отрисовать ленты
  g.append("g")
    .attr("fill-opacity", 0.67)
    .selectAll("path")
    .data(chords)
    .join("path")
    .attr("d", ribbon)
    .attr("fill", d => colourScale(nodes[d.source.index]))
    .attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker());

  // Отрисовать группы (дуги)
  const group = g.append("g")
    .selectAll("g")
    .data(chords.groups)
    .join("g");

  group.append("path")
    .attr("d", arc)
    .attr("fill", d => colourScale(nodes[d.index]))
    .attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker());

  // Добавить подписи
  group.append("text")
    .each(d => { d.angle = (d.startAngle + d.endAngle) / 2; })
    .attr("dy", "0.31em")
    .attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`)
    .attr("text-anchor", d => d.angle > Math.PI ? "end" : null)
    .text((d, i) => nodes[i])
    .style("font-size", "12px");
}

Тепловая карта

Тепловая карта использует цвет для кодирования значений в двумерной сетке, полезна для показа паттернов по категориям:

function drawHeatmap(data) {
  // формат данных: массив объектов с row, column и value
  // Пример: [{ row: 'A', column: 'X', value: 10 }, ...]

  if (!data || data.length === 0) return;

  const svg = d3.select('#chart');
  svg.selectAll("*").remove();

  const width = 800;
  const height = 600;
  const margin = { top: 100, right: 30, bottom: 30, left: 100 };
  const innerWidth = width - margin.left - margin.right;
  const innerHeight = height - margin.top - margin.bottom;

  // Получить уникальные строки и столбцы
  const rows = Array.from(new Set(data.map(d => d.row)));
  const columns = Array.from(new Set(data.map(d => d.column)));

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

  // Создать шкалы
  const xScale = d3.scaleBand()
    .domain(columns)
    .range([0, innerWidth])
    .padding(0.01);

  const yScale = d3.scaleBand()
    .domain(rows)
    .range([0, innerHeight])
    .padding(0.01);

  // Цветовая шкала для значений
  const colourScale = d3.scaleSequential(d3.interpolateYlOrRd)
    .domain([0, d3.max(data, d => d.value)]);

  // Отрисовать прямоугольники
  g.selectAll("rect")
    .data(data)
    .join("rect")
    .attr("x", d => xScale(d.column))
    .attr("y", d => yScale(d.row))
    .attr("width", xScale.bandwidth())
    .attr("height", yScale.bandwidth())
    .attr("fill", d => colourScale(d.value));

  // Добавить подписи оси X
  svg.append("g")
    .attr("transform", `translate(${margin.left},${margin.top})`)
    .selectAll("text")
    .data(columns)
    .join("text")
    .attr("x", d => xScale(d) + xScale.bandwidth() / 2)
    .attr("y", -10)
    .attr("text-anchor", "middle")
    .text(d => d)
    .style("font-size", "12px");

  // Добавить подписи оси Y
  svg.append("g")
    .attr("transform", `translate(${margin.left},${margin.top})`)
    .selectAll("text")
    .data(rows)
    .join("text")
    .attr("x", -10)
    .attr("y", d => yScale(d) + yScale.bandwidth() / 2)
    .attr("dy", "0.35em")
    .attr("text-anchor", "end")
    .text(d => d)
    .style("font-size", "12px");

  // Добавить цветовую легенду
  const legendWidth = 20;
  const legendHeight = 200;
  const legend = svg.append("g")
    .attr("transform", `translate(${width - 60},${margin.top})`);

  const legendScale = d3.scaleLinear()
    .domain(colourScale.domain())
    .range([legendHeight, 0]);

  const legendAxis = d3.axisRight(legendScale)
    .ticks(5);

  // Отрисовать цветовой градиент в легенде
  for (let i = 0; i < legendHeight; i++) {
    legend.append("rect")
      .attr("y", i)
      .attr("width", legendWidth)
      .attr("height", 1)
      .attr("fill", colourScale(legendScale.invert(i)));
  }

  legend.append("g")
    .attr("transform", `translate(${legendWidth},0)`)
    .call(legendAxis);
}

Круговая диаграмма

const pie = d3.pie()
  .value(d => d.value)
  .sort(null);

const arc = d3.arc()
  .innerRadius(0)
  .outerRadius(Math.min(width, height) / 2 - 20);

const colourScale = d3.scaleOrdinal(d3.schemeCategory10);

const g = svg.append("g")
  .attr("transform", `translate(${width / 2},${height / 2})`);

g.selectAll("path")
  .data(pie(data))
  .join("path")
  .attr("d", arc)
  .attr("fill", (d, i) => colourScale(i))
  .attr("stroke", "white")
  .attr("stroke-width", 2);

Force-directed сеть

const simulation = d3.forceSimulation(nodes)
  .force("link", d3.forceLink(links).id(d => d.id).distance(100))
  .force("charge", d3.forceManyBody().strength(-300))
  .force("center", d3.forceCenter(width / 2, height / 2));

const link = g.selectAll("line")
  .data(links)
  .join("line")
  .attr("stroke", "#999")
  .attr("stroke-width", 1);

const node = g.selectAll("circle")
  .data(nodes)
  .join("circle")
  .attr("r", 8)
  .attr("fill", "steelblue")
  .call(d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));

simulation.on("tick", () => {
  link
    .attr("x1", d => d.source.x)
    .attr("y1", d => d.source.y)
    .attr("x2", d => d.target.x)
    .attr("y2", d => d.target.y);

  node
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);
});

function dragstarted(event) {
  if (!event.active) simulation.alphaTarget(0.3).restart();
  event.subject.fx = event.subject.x;
  event.subject.fy = event.subject.y;
}

function dragged(event) {
  event.subject.fx = event.x;
  event.subject.fy = event.y;
}

function dragended(event) {
  if (!event.active) simulation.alphaTarget(0);
  event.subject.fx = null;
  event.subject.fy = null;
}

Добавление интерактивности

Всплывающие подсказки

// Создать div для подсказки (вне SVG)
const tooltip = d3.select("body").append("div")
  .attr("class", "tooltip")
  .style("position", "absolute")
  .style("visibility", "hidden")
  .style("background-color", "white")
  .style("border", "1px solid #ddd")
  .style("padding", "10px")
  .style("border-radius", "4px")
  .style("pointer-events", "none");

// Добавить к элементам
circles
  .on("mouseover", function(event, d) {
    d3.select(this).attr("opacity", 1);
    tooltip
      .style("visibility", "visible")
.html(`<strong>${d.label}</strong><br/>Value: ${d.value}`);
  })
  .on("mousemove", function(event) {
    tooltip
      .style("top", (event.pageY - 10) + "px")
      .style("left", (event.pageX + 10) + "px");
  })
  .on("mouseout", function() {
    d3.select(this).attr("opacity", 0.7);
    tooltip.style("visibility", "hidden");
  });

Zoom и pan

const zoom = d3.zoom()
  .scaleExtent([0.5, 10])
  .on("zoom", (event) => {
    g.attr("transform", event.transform);
  });

svg.call(zoom);

Взаимодействия по клику

circles
  .on("click", function(event, d) {
    // Обработать клик (диспатч события, обновление состояния приложения и т.д.)
    console.log("Clicked:", d);

    // Визуальный отклик
    d3.selectAll("circle").attr("fill", "steelblue");
    d3.select(this).attr("fill", "orange");

    // Опционально: диспатч кастомного события для вашего фреймворка/приложения
    // window.dispatchEvent(new CustomEvent('chartClick', { detail: d }));
  });

Переходы и анимации

Добавляйте плавные переходы к визуальным изменениям:

// Базовый переход
circles
  .transition()
  .duration(750)
  .attr("r", 10);

// Цепочка переходов
circles
  .transition()
  .duration(500)
  .attr("fill", "orange")
  .transition()
  .duration(500)
  .attr("r", 15);

// Ступенчатые переходы
circles
  .transition()
  .delay((d, i) => i * 50)
  .duration(500)
  .attr("cy", d => yScale(d.value));

// Кастомный easing
circles
  .transition()
  .duration(1000)
  .ease(d3.easeBounceOut)
  .attr("r", 10);

Справочник по шкалам

Количественные шкалы

// Линейная шкала
const xScale = d3.scaleLinear()
  .domain([0, 100])
  .range([0, 500]);

// Логарифмическая шкала (для экспоненциальных данных)
const logScale = d3.scaleLog()
  .domain([1, 1000])
  .range([0, 500]);

// Степенная шкала
const powScale = d3.scalePow()
  .exponent(2)
  .domain([0, 100])
  .range([0, 500]);

// Временная шкала
const timeScale = d3.scaleTime()
  .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])
  .range([0, 500]);

Порядковые шкалы

// Полосовая шкала (для столбчатых диаграмм)
const bandScale = d3.scaleBand()
  .domain(['A', 'B', 'C', 'D'])
  .range([0, 400])
  .padding(0.1);

// Точечная шкала (для категорий линий/точек)
const pointScale = d3.scalePoint()
  .domain(['A', 'B', 'C', 'D'])
  .range([0, 400]);

// Порядковая шкала (для цветов)
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);

Последовательные шкалы

// Последовательная цветовая шкала
const colourScale = d3.scaleSequential(d3.interpolateBlues)
  .domain([0, 100]);

// Дивергентная цветовая шкала
const divScale = d3.scaleDiverging(d3.interpolateRdBu)
  .domain([-10, 0, 10]);

Best practices

Подготовка данных

Всегда валидируйте и готовьте данные перед визуализацией:

// Отфильтровать невалидные значения
const cleanData = data.filter(d => d.value != null && !isNaN(d.value));

// Отсортировать данные, если порядок важен
const sortedData = [...data].sort((a, b) => b.value - a.value);

// Распарсить даты
const parsedData = data.map(d => ({
  ...d,
  date: d3.timeParse("%Y-%m-%d")(d.date)
}));

Оптимизация производительности

Для больших датасетов (>1000 элементов):

// Используйте canvas вместо SVG для множества элементов
// Используйте quadtree для обнаружения коллизий
// Упрощайте пути через d3.line().curve(d3.curveStep)
// Реализуйте виртуальный скроллинг для больших списков
// Используйте requestAnimationFrame для кастомных анимаций

Доступность

Делайте визуализации доступными:

// Добавьте ARIA-подписи
svg.attr("role", "img")
   .attr("aria-label", "Bar chart showing quarterly revenue");

// Добавьте заголовок и описание
svg.append("title").text("Quarterly Revenue 2024");
svg.append("desc").text("Bar chart showing revenue growth across four quarters");

// Обеспечьте достаточный цветовой контраст
// Предоставьте навигацию с клавиатуры для интерактивных элементов
// Включите альтернативу в виде таблицы данных

Стилизация

Используйте последовательную, профессиональную стилизацию:

// Определите цветовые палитры заранее
const colours = {
  primary: '#4A90E2',
  secondary: '#7B68EE',
  background: '#F5F7FA',
  text: '#333333',
  gridLines: '#E0E0E0'
};

// Примените единообразную типографику
svg.selectAll("text")
  .style("font-family", "Inter, sans-serif")
  .style("font-size", "12px");

// Используйте ненавязчивые линии сетки
g.selectAll(".tick line")
  .attr("stroke", colours.gridLines)
  .attr("stroke-dasharray", "2,2");

Типичные проблемы и решения

Проблема: Оси не появляются - Убедитесь, что у шкал валидные домены (проверьте на значения NaN) - Проверьте, что ось добавлена в правильную группу - Проверьте, что transform-трансляции корректны

Проблема: Переходы не работают - Вызывайте .transition() перед изменением атрибутов - Убедитесь, что элементы имеют уникальные ключи для правильной привязки данных - Проверьте, что зависимости useEffect включают все изменяющиеся данные

Проблема: Адаптивный размер не работает - Используйте ResizeObserver или слушатель resize окна - Обновляйте размеры в состоянии, чтобы вызвать перерендер - Убедитесь, что у SVG есть атрибуты width/height или viewBox

Проблема: Проблемы производительности - Ограничьте число DOM-элементов (рассмотрите canvas для >1000 элементов) - Используйте debounce для обработчиков resize - Используйте .join() вместо отдельных selection'ов enter/update/exit - Избегайте лишних перерендеров, проверяя зависимости

Ресурсы

references/

Содержит детальные референсные материалы: - d3-patterns.md — исчерпывающая коллекция паттернов визуализации и примеров кода - scale-reference.md — полный гайд по шкалам d3 с примерами - colour-schemes.md — цветовые схемы d3 и рекомендации по палитрам

assets/

Содержит шаблоны-заготовки:

  • chart-template.js — стартовый шаблон для базового чарта
  • interactive-template.js — шаблон с подсказками, zoom и взаимодействиями
  • sample-data.json — примеры датасетов для тестирования

Эти шаблоны работают с чистым JavaScript, React, Vue, Svelte или любой другой JavaScript-средой. Адаптируйте их под ваш конкретный фреймворк.

Чтобы использовать эти ресурсы, читайте соответствующие файлы, когда нужно детальное руководство по конкретным типам визуализаций или паттернам.

Войдите, чтобы оставить комментарий