Представьте: у вас 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 рабочих дней. Свяжитесь с нами для консультации по вашему проекту. Закажите разработку кастомного дашборда — оценим ваш проект и предложим оптимальное решение. Если вам нужна нестандартная визуализация — получите консультацию по вашему проекту.







