Уявіть: у вас 50 000 транзакцій, потрібно показати тренди та аномалії в реальному часі, але готові бібліотеки дають лише лінійний графік і пару тултипів. Клієнту потрібен кастомний дашборд із синхронним зумом, фільтрацією за часовим діапазоном та експортом у PNG. Стандартні рішення тут безсилі — потрібен D3.js.
Ми спеціалізуємось на створенні інтерактивних дашбордів та кастомних графіків на D3.js. Понад 5 років досвіду, 50+ успішних проєктів. Ми на ринку з 2018 року. Гарантуємо якість та дотримання термінів.
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)
У порівнянні з Chart.js, D3.js дозволяє створювати в 5 разів складніші взаємодії. D3.js дає в 3 рази більше гнучкості порівняно з готовими чарт-бібліотеками, що дозволяє реалізувати унікальні візуалізації без компромісів. Крім того, D3 скорочує час розробки складних взаємодій у 2–3 рази порівняно з ручним кодуванням на SVG. Використання D3.js усуває витрати на ліцензії сторонніх бібліотек, а якісна візуалізація окупається за рахунок підвищення ефективності аналізу даних. Економія на ліцензіях може сягати до $5000 на рік для великих команд.
| Критерій | D3.js | Готова бібліотека |
|---|---|---|
| Тип візуалізації | Будь-який (карти, Sankey, Chord) | Стандартні (line, bar, pie) |
| Контроль над стилями | Повний | Обмежений API |
| Інтерактивність | Довільна (zoom, brush, drag) | Тільки вбудована |
| Продуктивність | Ручна оптимізація (Canvas, WebGL) | Автоматична (часто низька для >1k точок) |
| Крива навчання | Висока | Низька |
Інтеграція D3.js з React (react d3)
Ми використовуємо гібридний підхід: D3 для розрахунків і складних взаємодій, React для керування станом і рендерингу контейнерів. D3 використовує data-join з enter/exit паттерном для декларативного оновлення DOM. Ось базовий патерн, який ми застосовуємо в 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
D3 забезпечує безпрецедентний контроль над інтерактивністю: d3.zoom() для масштабування, d3.brush() для виділення діапазонів, кастомні tooltip, drag and drop, сортування, фільтрація. Особливо потужний патерн brush and link, який синхронізує кілька візуалізацій. Це критично для аналітичних дашбордів.
Як синхронізувати кілька графіків? (brush and link)
Класичний патерн — 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>
);
}
Оптимізація продуктивності при великих даних (SVG Canvas та WebGL)
D3 + SVG починає гальмувати після ~5000 точок. Рішення: Canvas для scatter plot (керування через canvas.getContext('2d')), використання SVG Canvas гібриду, або децимація (LTTB). Для понад 100 000 точок доцільно застосовувати WebGL.
| Технологія | Продуктивність | Складність реалізації |
|---|---|---|
| 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 (експорт svg png)
```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, SVG/Canvas), проектуємо взаємодії.
- Реалізація — пишемо код, налаштовуємо адаптивність для мобільних пристроїв (адаптивні дашборди), оптимізуємо продуктивність.
- Тестування — перевіряємо на реальних даних, усуваємо баги.
- Деплой — розгортаємо на вашому сервері або в хмарі, передаємо доступи.
Що входить у роботу
- Вихідний код з коментарями та документацією
- Налаштування адаптивності під мобільні пристрої
- Навчання вашої команди (1-2 сесії)
- Гарантійна підтримка 2 тижні після здачі
- Інтеграція з вашою CMS або API (Laravel, Node.js)
Терміни та вартість
- Одиничний кастомний графік з інтерактивністю (tooltip, zoom) — від 2 до 4 днів.
- Аналітичний дашборд з 4-6 взаємопов'язаних візуалізацій — від 3 до 5 тижнів.
- Складні геовізуалізації — індивідуальна оцінка після вивчення даних.
Вартість розраховується індивідуально. Економія бюджету за рахунок відсутності ліцензійних відрахувань на комерційні бібліотеки. Орієнтовна вартість базового дашборду — від $5000. Для точної оцінки надішліть приклад даних і вимоги — ми підготуємо комерційну пропозицію протягом 2 робочих днів. Зв'яжіться з нами для консультації щодо вашого проєкту. Замовте розробку кастомного дашборду — оцінимо ваш проєкт і запропонуємо оптимальне рішення. Якщо вам потрібна нестандартна візуалізація — отримайте консультацію щодо вашого проєкту.
Наші ключові напрямки: D3.js, інтерактивні дашборди, візуалізація даних, кастомні графіки, SVG Canvas, React D3, brush and link, аналітичні дашборди, експорт SVG PNG, адаптивні дашборди, D3 zoom, D3 brush. Середня вартість проєкту становить від $3000 до $15000 залежно від складності. Економія на ліцензіях досягає 40% порівняно з комерційними рішеннями, що дає до $5000 щорічної економії.







