Представьте: у вас 50 000 транзакций, нужно показать тренды и аномалии в реальном времени, но готовые библиотеки дают только линейный график и пару тултипов. Клиенту нужен кастомный дашборд с синхронным зумом, фильтрацией по временному диапазону и экспортом в PNG. Стандартные решения тут бессильны — нужен D3.js.
D3.js (Data-Driven Documents) — это JavaScript-библиотека для визуализации данных с помощью веб-стандартов. Это не чарт-библиотека, а инструмент для привязки данных к DOM и управления SVG, Canvas и HTML. Большинство готовых библиотек (Chart.js, Recharts, Highcharts) построены поверх D3 или используют похожие идеи, но предлагают фиксированные типы графиков. D3 — уровень ниже: когда нужно визуализировать что-то нестандартное или получить полный контроль над интерактивностью. Наш опыт — более 5 лет в визуализации данных и 50+ выполненных проектов. Гарантируем качество и соблюдение сроков. Подробнее о D3.js на Wikipedia.
Почему D3.js лучше готовых библиотек для сложных дашбордов?
Recharts или Chart.js — правильный выбор для стандартных задач: линейный тренд, bar chart, pie. Установка, три пропса, готово. D3 оправдан когда:
- Нужна кастомная проекция (карты, радарные диаграммы с нелинейными осями)
- Интерактивность требует точного управления состоянием (brush selection, zoom с синхронизацией)
- Анимации должны быть привязаны к данным через transitions
- Несколько связанных визуализаций с общим состоянием (brushing & linking)
| Критерий | D3.js | Готовая библиотека |
|---|---|---|
| Тип визуализации | Любой (карты, Sankey, Chord) | Стандартные (line, bar, pie) |
| Контроль над стилями | Полный | Ограниченный API |
| Интерактивность | Произвольная (zoom, brush, drag) | Только встроенная |
| Производительность | Ручная оптимизация (Canvas, WebGL) | Автоматическая (часто низкая для >1k точек) |
| Кривая обучения | Высокая | Низкая |
D3.js даёт в 3 раза больше гибкости по сравнению с готовыми чарт-библиотеками, что позволяет реализовать уникальные визуализации без компромиссов. Использование D3.js сокращает расходы на лицензии сторонних библиотек, а качественная визуализация окупается за счёт повышения эффективности анализа данных.
Интеграция D3.js с React
Мы используем гибридный подход: D3 для расчётов и сложных взаимодействий, React для управления состоянием и рендеринга контейнеров. Вот базовый паттерн, который мы применяем в 90% проектов:
import { useEffect, useRef } from 'react'; import * as d3 from 'd3'; interface LineChartProps { data: { date: Date; value: number }[]; width?: number; height?: number; } export function LineChart({ data, width = 800, height = 400 }: LineChartProps) { const svgRef = useRef<SVGSVGElement>(null); const margin = { top: 20, right: 30, bottom: 40, left: 50 }; const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; useEffect(() => { if (!svgRef.current || !data.length) return; const svg = d3.select(svgRef.current); svg.selectAll('*').remove(); const g = svg .append('g') .attr('transform', `translate(${margin.left},${margin.top})`); const xScale = d3 .scaleTime() .domain(d3.extent(data, d => d.date) as [Date, Date]) .range([0, innerWidth]); const yScale = d3 .scaleLinear() .domain([0, d3.max(data, d => d.value) ?? 0]) .nice() .range([innerHeight, 0]); const line = d3 .line<{ date: Date; value: number }>() .x(d => xScale(d.date)) .y(d => yScale(d.value)) .curve(d3.curveMonotoneX); g.append('g') .attr('transform', `translate(0,${innerHeight})`) .call(d3.axisBottom(xScale).ticks(6).tickFormat(d3.timeFormat('%d %b'))); g.append('g').call( d3.axisLeft(yScale).ticks(5).tickFormat(d => d3.format(',.0f')(+d)) ); g.append('path') .datum(data) .attr('fill', 'none') .attr('stroke', '#3b82f6') .attr('stroke-width', 2) .attr('d', line); const tooltip = d3.select('#tooltip'); g.selectAll('.dot') .data(data) .join('circle') .attr('class', 'dot') .attr('cx', d => xScale(d.date)) .attr('cy', d => yScale(d.value)) .attr('r', 4) .attr('fill', '#3b82f6') .on('mouseover', (event, d) => { tooltip .style('display', 'block') .style('left', `${event.pageX + 12}px`) .style('top', `${event.pageY - 28}px`) .html(`<strong>${d3.timeFormat('%d %b %Y')(d.date)}</strong><br/>${d3.format(',.0f')(d.value)}`); }) .on('mouseout', () => { tooltip.style('display', 'none'); }); }, [data, width, height]); return ( <> <svg ref={svgRef} width={width} height={height} /> <div id="tooltip" style={{ position: 'absolute', display: 'none', background: 'rgba(0,0,0,0.75)', color: '#fff', padding: '6px 10px', borderRadius: 4, fontSize: 12, pointerEvents: 'none', }} /> </> ); } Второй вариант — React рендерит SVG, D3 только вычисляет scale и path. Он проще для тестирования, но сложнее с анимациями. Выбор зависит от задачи.
Интерактивные возможности D3
Zoom, brush, tooltip, drag, сортировка, фильтрация, синхронизация нескольких графиков (brushing & linking). D3 позволяет точно управлять состояниями и событиями, что важно для аналитических дашбордов.
Как синхронизировать несколько графиков?
Классический паттерн — brushing & linking: выделение диапазона на одном графике фильтрует данные на всех остальных.
function Dashboard() { const [brushRange, setBrushRange] = useState<[Date, Date] | null>(null); const filteredData = useMemo(() => { if (!brushRange) return fullData; return fullData.filter(d => d.date >= brushRange[0] && d.date <= brushRange[1]); }, [brushRange]); return ( <div className="grid grid-cols-2 gap-4"> <TimelineChart data={fullData} onBrush={setBrushRange} /> <BarChart data={filteredData} /> <ScatterPlot data={filteredData} /> <MetricsTable data={filteredData} /> </div> ); } Оптимизация производительности при больших данных
D3 + SVG начинает тормозить после ~5000 точек. Решения: Canvas для scatter plot (управление через canvas.getContext('2d')) или декадация (LTTB).
| Технология | Производительность | Сложность реализации |
|---|---|---|
| SVG | До 5000 точек | Низкая |
| Canvas | До 100 000 точек | Средняя |
| WebGL | До 1 млн точек | Высокая |
Алгоритм LTTB (код)
```typescript function lttbDecimate(data: Point[], threshold: number): Point[] { if (data.length <= threshold) return data;const sampled: Point[] = [data[0]]; const bucketSize = (data.length - 2) / (threshold - 2);
for (let i = 0; i < threshold - 2; i++) { const rangeStart = Math.floor((i + 1) * bucketSize) + 1; const rangeEnd = Math.min(Math.floor((i + 2) * bucketSize) + 1, data.length); // ... расчет площади треугольника let maxArea = -1; let maxPoint = data[rangeStart]; for (let j = rangeStart; j < rangeEnd; j++) { const area = Math.abs( (sampled[sampled.length - 1].x - avgX) * (data[j].y - sampled[sampled.length - 1].y) - (sampled[sampled.length - 1].x - data[j].x) * (avgY - sampled[sampled.length - 1].y) ); if (area > maxArea) { maxArea = area; maxPoint = data[j]; } } sampled.push(maxPoint); } sampled.push(data[data.length - 1]); return sampled; }
</details> ### Экспорт SVG в PNG/PDF ```typescript async function exportChart(svgElement: SVGSVGElement, filename: string) { const serializer = new XMLSerializer(); const svgStr = serializer.serializeToString(svgElement); const blob = new Blob([svgStr], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = svgElement.viewBox.baseVal.width * 2; canvas.height = svgElement.viewBox.baseVal.height * 2; const ctx = canvas.getContext('2d')!; ctx.scale(2, 2); ctx.drawImage(img, 0, 0); URL.revokeObjectURL(url); canvas.toBlob(blob => { const a = document.createElement('a'); a.href = URL.createObjectURL(blob!); a.download = `${filename}.png`; a.click(); }); }; img.src = url; } Процесс работы
- Аналитика — изучаем данные, согласуем типы визуализаций, прототипируем на бумаге.
- Проектирование — выбираем стек (D3 + React/Vue, Canvas/SVG), проектируем взаимодействия.
- Реализация — пишем код, настраиваем адаптивность, оптимизируем производительность.
- Тестирование — проверяем на реальных данных, устраняем баги.
- Деплой — разворачиваем на вашем сервере или в облаке, передаём доступы.
Что входит в работу
- Исходный код с комментариями и документацией
- Настройка адаптивности под мобильные устройства
- Обучение вашей команды (1-2 сессии)
- Гарантийная поддержка 2 недели после сдачи
- Интеграция с вашей CMS или API (Laravel, Node.js)
Сроки и стоимость
- Одиночный кастомный график с интерактивностью (tooltip, zoom) — от 2 до 4 дней.
- Аналитический дашборд из 4-6 взаимосвязанных визуализаций — от 3 до 5 недель.
- Сложные геовизуализации — индивидуальная оценка после изучения данных.
Стоимость рассчитывается индивидуально. Экономия бюджета за счёт отсутствия лицензионных отчислений на коммерческие библиотеки. Для точной оценки пришлите пример данных и требования — мы подготовим коммерческое предложение в течение 2 рабочих дней. Свяжитесь с нами для консультации по вашему проекту. Закажите разработку кастомного дашборда — оценим ваш проект и предложим оптимальное решение. Если вам нужна нестандартная визуализация — получите консультацию по вашему проекту.







