Когда нужна кастомная визуализация на Visx?
Типовой запрос от клиента: отобразить динамику курса акций за последний год с возможностью приближения и тултипами. Recharts не справляется с нестандартной анимацией, Chart.js тормозит на 50 000 точках. Нужна кастомная визуализация на Visx — библиотеке Airbnb, объединяющей гибкость D3 с парадигмой React. Visx позволяет строить любые SVG-графики с полным контролем и высокой производительностью. Мы используем его в проектах с датасетами до 100 000 записей, обеспечивая плавную анимацию и мгновенное обновление.
Какие проблемы решает Visx?
Ограниченная кастомизация готовых библиотек
Recharts предлагает множество конфигураций, но любое отклонение от шаблона превращается в хак с переопределением пропсов. Visx дает полный контроль над SVG: вы управляете каждым элементом, шкалами и переходами.
Производительность на больших объёмах данных
D3-рендеринг через React-жизненный цикл может вызывать лишние ререндеры. Visx оптимизирует это за счет чистых компонентов и мемоизации. На практике мы добиваемся 60 FPS на датасетах из 10 000 точек.
Интеграция с React-приложением
Visx-компоненты — это обычные React-компоненты, которые легко встраиваются в любую архитектуру: Next.js, CRA, Vite. Не нужно подключать отдельный рендерер или контекст.
Как Visx решает задачу кастомизации?
Visx предлагает низкоуровневые примитивы: scaleTime, scaleLinear, LinePath, AreaClosed, AxisLeft, AxisBottom. Из них собирается любой график без оглядки на шаблоны. Для интерактива — хук useTooltip и события мыши. Весь код — TypeScript с типизацией данных.
Возьмем типовую задачу: линейный график с тултипом и зоной заливки. Пример реализации на Visx (фрагмент):
import { scaleTime, scaleLinear } from '@visx/scale';
import { LinePath, AreaClosed } from '@visx/shape';
import { AxisLeft, AxisBottom } from '@visx/axis';
import { GridRows, GridColumns } from '@visx/grid';
import { useTooltip, TooltipWithBounds, defaultStyles } from '@visx/tooltip';
import { localPoint } from '@visx/event';
import { bisector } from 'd3-array';
import { curveMonotoneX } from 'd3-shape';
interface DataPoint { date: Date; value: number; }
const bisectDate = bisector<DataPoint, Date>(d => d.date).left;
function CustomLineChart({
data,
width,
height,
margin = { top: 20, right: 20, bottom: 40, left: 60 }
}: {
data: DataPoint[];
width: number;
height: number;
margin?: { top: number; right: number; bottom: number; left: number };
}) {
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const xScale = scaleTime({
range: [0, innerWidth],
domain: [
Math.min(...data.map(d => d.date.getTime())),
Math.max(...data.map(d => d.date.getTime()))
]
});
const yScale = scaleLinear({
range: [innerHeight, 0],
domain: [0, Math.max(...data.map(d => d.value)) * 1.1],
nice: true
});
const { tooltipData, tooltipLeft, tooltipTop, showTooltip, hideTooltip } = useTooltip<DataPoint>();
const handleTooltip = (event: React.MouseEvent<SVGRectElement>) => {
const { x } = localPoint(event) || { x: 0 };
const x0 = xScale.invert(x - margin.left);
const index = bisectDate(data, x0, 1);
const d0 = data[index - 1];
const d1 = data[index];
const d = !d1 || Math.abs(x0.getTime() - d0.date.getTime()) <
Math.abs(x0.getTime() - d1.date.getTime()) ? d0 : d1;
showTooltip({
tooltipData: d,
tooltipLeft: xScale(d.date) + margin.left,
tooltipTop: yScale(d.value) + margin.top
});
};
return (
<div style={{ position: 'relative' }}>
<svg width={width} height={height}>
<g transform={`translate(${margin.left}, ${margin.top})`}>
<GridRows scale={yScale} width={innerWidth} stroke="#f0f0f0" />
<GridColumns scale={xScale} height={innerHeight} stroke="#f0f0f0" />
<defs>
<linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.3} />
<stop offset="100%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
</defs>
<AreaClosed
data={data}
x={d => xScale(d.date)}
y={d => yScale(d.value)}
yScale={yScale}
fill="url(#areaGradient)"
curve={curveMonotoneX}
/>
<LinePath
data={data}
x={d => xScale(d.date)}
y={d => yScale(d.value)}
stroke="#3b82f6"
strokeWidth={2}
curve={curveMonotoneX}
/>
<AxisLeft
scale={yScale}
tickFormat={v => `${(v as number / 1000).toFixed(0)}k`}
/>
<AxisBottom
top={innerHeight}
scale={xScale}
tickFormat={d => format(d as Date, 'dd MMM')}
/>
<rect
width={innerWidth}
height={innerHeight}
fill="transparent"
onMouseMove={handleTooltip}
onMouseLeave={hideTooltip}
/>
{tooltipData && (
<g>
<line
x1={tooltipLeft! - margin.left}
x2={tooltipLeft! - margin.left}
y1={0}
y2={innerHeight}
stroke="#3b82f6"
strokeDasharray="4,4"
strokeWidth={1}
/>
<circle
cx={tooltipLeft! - margin.left}
cy={tooltipTop! - margin.top}
r={5}
fill="#3b82f6"
stroke="white"
strokeWidth={2}
/>
</g>
)}
</g>
</svg>
{tooltipData && (
<TooltipWithBounds
top={tooltipTop}
left={tooltipLeft}
style={{ ...defaultStyles, background: '#1e293b', color: 'white' }}
>
<div>
<strong>{format(tooltipData.date, 'dd.MM.yyyy')}</strong>
<br />
{tooltipData.value.toLocaleString('ru')} ₽
</div>
</TooltipWithBounds>
)}
</div>
);
}
ParentSize для адаптивности
import { ParentSize } from '@visx/responsive';
function ResponsiveChart({ data }) {
return (
<ParentSize>
{({ width, height }) => (
<CustomLineChart data={data} width={width} height={height || 300} />
)}
</ParentSize>
);
}
Почему Visx, а не Chart.js?
Visx лучше Chart.js в ситуациях, где требуется нестандартная форма графика или сложные интерактивные сценарии. Chart.js проще в настройке и быстрее для простых диаграмм, но его архитектура хуже масштабируется под кастомные задачи. Visx дает полный доступ к SVG, что позволяет реализовать любую визуализацию. Наши клиенты экономят до 40% времени на разработке сложных дашбордов за счет использования Visx.
Сравнение Visx и Recharts
| Критерий | Visx | Recharts |
|---|---|---|
| Уровень контроля | Полный (SVG-примитивы) | Средний (конфигурация пропсов) |
| Кастомизация | Безграничная | Ограничена шаблонами |
| Производительность | Высокая (ручное управление) | Средняя (автоматические ререндеры) |
| Время разработки | 3-5 дней на тип | 1-2 дня на тип |
| Порог входа | Средний (нужно знать d3) | Низкий (React-only) |
Типичные ошибки при работе с Visx
- Пропуск мемоизации шкал. Шкалы нужно создавать внутри useMemo или вне компонента, иначе каждый ререндер будет пересоздавать их, что ведёт к потере производительности.
- Игнорирование ParentSize. Без адаптивного контейнера график не будет реагировать на изменение размеров окна, что ломает UX на мобильных устройствах.
- Некорректная обработка краёв данных. Если данные содержат NaN или null, Visx может выбросить ошибку. Всегда фильтруйте данные перед передачей в компоненты.
- Забыли про accessibility. SVG-графика должна быть доступна для скринридеров: добавляйте role="img" и aria-label.
Как создать кастомный график с нуля?
- Определите шкалы —
scaleTimeдля времени,scaleLinearдля значений. - Опишите геометрию —
LinePathдля линии,AreaClosedдля заливки. - Добавьте оси —
AxisLeftиAxisBottom. - Реализуйте тултип — используйте хук
useTooltipи события мыши. - Оберните в адаптивный контейнер —
ParentSizeдля автоматического ресайза.
Процесс работы и сроки
| Этап | Длительность |
|---|---|
| Аналитика и сбор требований | 0.5–1 день |
| Прототипирование MVP | 1–2 дня |
| Разработка финальной версии | 3–5 дней на тип |
| QA и оптимизация производительности | 1–2 дня |
| Деплой и интеграция | 0.5–1 день |
Полный цикл разработки одного типа визуализации занимает от 3 до 5 дней. Комплект из 3–4 различных типов — от 1.5 до 2 недель. Стоимость работ рассчитывается индивидуально и зависит от сложности интерактива и объёма данных.
Что входит в работу?
Мы выполняем разработку визуализации данных на Visx под ключ:
- анализ требований и проектирование макета;
- написание кода с нуля на React + TypeScript;
- тестирование на реальных данных (до 100 000 записей);
- адаптация под мобильные экраны и retina-дисплеи;
- документация в README и инлайн-комментариях;
- передача доступов к репозиторию и деплою;
- обучение вашей команды работе с Visx (вебинар 1 час).
Свяжитесь с нами для обсуждения вашей задачи. Получите консультацию по кастомной визуализации.







