Секреты правильной растяжки: адаптивные графики в вебе без клиентского JavaScript
О том, как делать графики, которые сами приспособятся к размерам области вывода данных. На случай, если в браузере выключен JavaScript или что-то пошло не так.
Существует кучах библиотек для создания графиков и диаграмм в вебе. Можно даже имитировать отрисовку диаграмм «от руки». Обычно данные для графиков запрашиваются из сети или сама картинка рендерится на canvas
– кажется, что без JavaScript не обойтись.
К сожалению, мы не можем полагаться на то, что у всех пользователей включен JavaScript или что он корректно работает. Пока страница грузится, на ней будет прямоугольная дыра в области построения чарта.
Ещё одна проблема – адаптивность. Графики необходимо перерисовывать при ресайзе, иначе они «поломаются». Это создаёт лишнюю работу и для браузера, и для программиста. Особенно если используются низкоуровневые библиотеки вроде D3.
Возьмём для примера SVG-графики из статьи New York Times и подумаем, можем ли построить их без JS.
Все методы, используемые в статье, собраны в экспериментальный компонент Pancake для фреймворка Svelte.
Проблема
Создать линейный график на SVG не так уж и сложно. Допустим, есть вот такие данные:
const data = [ { x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 4 }, { x: 3, y: 9 }, { x: 4, y: 16 }, { x: 5, y: 25 }, { x: 6, y: 36 }, { x: 7, y: 49 }, { x: 8, y: 64 }, { x: 9, y: 81 }, { x: 10, y: 100 } ];
Требуется вывести результат в область размером 300x100 пикселей.
Чтобы получить координаты опорных точек, нужно умножить x
на 30, а y
вычесть из 100:
Конечно, в реальности вы скорее будете использовать специальную функцию вместо того, чтобы считать координаты вручную:
function scale(domain, range) { const m = (range[1] - range[0]) / (domain[1] - domain[0]); return num => range[0] + m * (num - domain[0]); } const x = scale([0, Math.max(...data.map(d => d.x))], [0, 300]); const y = scale([0, Math.max(...data.map(d => d.y))], [100, 0]); const points = data.map(d => `${x(d.x)},${y(d.y)}`).join(' '); const chart = ` `;
Добавив в svg пару осей и немного стилей, получаем график:
Всю логику можно разместить в NodeJS-скрипте, так что код будет целиком генерироваться на сервере. Таким образом, нам не требуется ни строчки клиентского JavaScript.
Однако этот чарт не адаптивен, размеры всегда составляют 300x100px. А это не то, чего мы ожидаем.
Решение. Часть 1
У SVG-элемента есть атрибут viewBox
, задающий систему координат изображения, независящую от реальных размеров. По умолчанию соотношение сторон, заданное в viewBox
сохраняется. Подавляется такое поведение с помощью preserveAspectRatio="none"
.
Добавим простую систему координат:
Что ж, теперь наш график действительно стал гибким...
Очевидно, это не то, что нам нужно. Символы ужасно масштабируются, и в некоторых случаях числа невозможно считать. Кроме того, варьируется толщина линии.
Вторую проблему легко решить, используя малоизвестное CSS-свойство vector-effect: non-scaling-stroke
для каждого элемента.
Но с масштабированием текста придётся разбираться другими методами.
Решение. Часть 2
Оси графика необязательно рисовать прямо в SVG. Мы можем использовать обычные HTML-элементы и расположить их с помощью CSS. Чтобы синхронизировать масштабирование HTML- и SVG-слоев, применяем процентную координатную сетку.
0246810 0 50 100
График больше не ломается!
Ещё одно преимущество HTML-элементов – они автоматически привязываются к ближайшему пикселю. Так что не появляется неприятный эффект нечёткости, который встречается в SVG.
Готовое решение
Потребовалось много ручной работы. Чтобы этого избежать, воспользуемся готовым решением – компонентом Pancake для SvelteJS.
{value} {value}
Благодаря Svelte этот чарт отрендерить на сервере с помощью NodeJS или вставить в DOM на клиенте. Для больших и сложных интерактивных графиков может потребоваться одновременно и то, и другое. Основную разметку можно генерировать на сервере, а затем прогрессивно улучшать на клиенте, добавляя интерактивность. Без компонентных фреймворков типа Svelte это было бы непросто.
Обратите внимание, Pancake не создаёт элементы span
и path
внутри SVG-изображения. Компоненты прежде всего логические – вы детально контролируете представление.
Усложняем задачу
Мы можем сделать гораздо больше, чем простейшие линейные графики:
Например, очень интересны диаграммы рассеивания (scatterplots).
Мы не можем использовать для них элементы circle
, так как они могут некорректно масштабироваться. Компонент Pancake.Scatterplot
генерирует path
, состоящий из несвязанных дуг с нулевым радиусом. Размер окружностей определяется шириной контура (stroke width).
Благодаря тому, что библиотека оформлена в виде компонента Svelte, мы легко можем добавить графикам немного интерактива.
Взаимодействия также можно декларативно обрабатывать прямо внутри графика Pancake. Например, создадим дерево квадрантов (quadtree, во многом заимствовано из библиотеки D3), которое позволит находить ближайшую к курсору точку.
{#if closest} {/if}
В будущем в библиотеке появится поддержка canvas
(2D и WebGL). Конечно, графики на canvas
не смогут работать без JavaScript, но при работе с большими наборами данных SVG может не справляться.
Предостережения
Помните, Pancake является экспериментальной библиотекой, у него ещё не очень много реальных кейсов.
Основное внимание уделено управлению системой координат для двумерных чартов. Для графиков, гистограмм и диаграмм рассеивания этого достаточно, но круговые диаграммы, к сожалению, не поддерживаются.
На данный момент у библиотеки нет полноценной документации. Вы можете использовать примеры, представленные на домашней странице. Вполне возможно, что в будущем API будет меняться.
Название Pancake связано с тем, что фактически чарты строятся путём наложения слоёв друг на друга, как в стопке панкейков (толстый пышный блинчик, по вкусу напоминающий бисквитный корж – такие можно видеть в американских фильмах). Проект во многом вдохновлялся фреймворком Layer Cake и библиотекой D3.