Небольшой эксперимент: что если графики Apache ECharts рисовать в стиле hand-drawn — как будто наброшены от руки?

Идея
Мне нравится эстетика «нарисованных от руки» графиков — шероховатые контуры, штриховая заливка, кривоватые линии. Библиотека Rough.js умеет именно это: принимает SVG-путь или канвас-команды и рисует их с лёгкой «небрежностью».
Apache ECharts строит графики через zrender — собственный движок сцены. У zrender есть механизм подключаемых painter-ов: можно зарегистрировать свой класс рендеринга и передать его при инициализации через { renderer: 'rough' }. Это идеальная точка входа.
Как это устроено
1 | ECharts (логика графиков) |
RPainter — реализация интерфейса PainterBase. По структуре близко к стандартному CanvasPainter из zrender: управляет canvas-слоями, вызывает цикл перерисовки.
brush() — диспетчер для каждого элемента из отсортированного display list. Для Path-элементов вызывает brushPath, для текста — стандартный canvas, для изображений — drawImage.
brushPath() — ключевая функция. Она не рисует напрямую, а:
- Записывает команды zrender-пути в
SVGPathRecorder(duck-typed canvas context) - Получает строку SVG-пути (
M,L,C,Q,A,Z) - Передаёт её в
rough.canvas(canvas).path(svgPath, opts)
SVGPathRecorder — псевдо-canvas-контекст, который перехватывает вызовы PathProxy.rebuildPath() и транслирует их в SVG path data. Главная тонкость — дуга: SVG A не умеет рисовать полную окружность (360°), поэтому она разбивается на две полудуги.
Эвристика roughness
Не все элементы графика стоит делать одинаково «кривыми». Оси, сетка и тики при сильном roughness становятся нечитаемыми.
1 | roughness: hasFill ? 1.5 : 0.4, |
Заполненные фигуры (столбцы, области) получают полный sketchy-эффект; stroke-only пути (оси, риски) — минимальный.
Что работает
На скриншоте — пять типов графиков, все через один рендерер:
| График | Описание |
|---|---|
| Столбчатый | Продажи по дням недели |
| Группированный | Выручка по регионам, три серии |
| Линейный + area | Температура по месяцам, сглаженная кривая |
| Горизонтальный bar | Штат отделов |
| Gauge | Спидометр оборотов двигателя |
Шрифт Caveat из Google Fonts подключён через CSS, но ECharts рисует текст через canvas — там CSS не работает. Шрифт нужно указывать явно в опциях через textStyle: { fontFamily: 'Caveat, cursive' }.
Интерактивность (hover, тултипы) работает из коробки — zrender вешает свой HandlerProxy поверх canvas, рендерер тут не причём.
Ограничения и нерешённые проблемы
Градиенты и паттерны не поддерживаются — Rough.js принимает только plain-строки цвета. Такие заливки откатываются к 'none'.
Clip-path анимации не работают. ECharts активно использует clip paths для анимации появления: линия «рисуется» через расширяющийся clip rect, gauge-дуга «заполняется» через clip-сектор. Наш painter — статический снапшот-рендерер без анимационного цикла, поэтому clip paths мы намеренно игнорируем. Из-за этого анимации появления просто не проигрываются — элементы рисуются сразу в финальном состоянии.
Gauge-зоны (цветные сегменты шкалы) не отображаются при первой отрисовке и появляются только после hover. Это оказался нетривиальный баг на пересечении нескольких механизмов zrender: lazy path building, viewport culling в shouldBePainted, и порядка вызовов. Несколько подходов к исправлению ничего не дали — эксперимент есть эксперимент.
Итого
Подключить кастомный painter к ECharts на удивление несложно — интерфейс PainterBase небольшой, а zrender хорошо изолирует renderer от логики графиков. Основная сложность — не в самом рендеринге, а в деталях: как zrender управляет состоянием элементов, dirty flags, clip paths и анимационным циклом.
Результат выглядит мило, хотя и работает не идеально. Код открыт — смотреть на GitHub.