proglib
Сообщение

Хотите написать игру на Python? Разберитесь в языке с нуля!

Хотите написать игру на Python? Разберитесь в языке с нуля!

furry.cat 12 февраля 2020

Секреты правильной растяжки: адаптивные графики в вебе без клиентского JavaScript

О том, как делать графики, которые сами приспособятся к размерам области вывода данных. На случай, если в браузере выключен JavaScript или что-то пошло не так.
0
1788

Существует кучах библиотек для создания графиков и диаграмм в вебе. Можно даже имитировать отрисовку диаграмм «от руки». Обычно данные для графиков запрашиваются из сети или сама картинка рендерится на 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:

            <polyline points="
  0,0
  30,99
  60,96
  90,91
  120,84
  150,75
  180,64
  210,51
  240,36
  270,19
  300,0
"></polyline>
        

Конечно, в реальности вы скорее будете использовать специальную функцию вместо того, чтобы считать координаты вручную:

            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 width="300" height="100">
  <polyline points="${points}"></polyline>
</svg>
`;
        

Добавив в svg пару осей и немного стилей, получаем график:

Живой пример

Всю логику можно разместить в NodeJS-скрипте, так что код будет целиком генерироваться на сервере. Таким образом, нам не требуется ни строчки клиентского JavaScript.

Однако этот чарт не адаптивен, размеры всегда составляют 300x100px. А это не то, чего мы ожидаем.

Решение. Часть 1

У SVG-элемента есть атрибут viewBox, задающий систему координат изображения, независящую от реальных размеров. По умолчанию соотношение сторон, заданное в viewBox сохраняется. Подавляется такое поведение с помощью preserveAspectRatio="none".

Добавим простую систему координат:

            <svg viewBox="0 0 100 100" preserveAspectRatio="none">
        

Что ж, теперь наш график действительно стал гибким...

Живой пример

Очевидно, это не то, что нам нужно. Символы ужасно масштабируются, и в некоторых случаях числа невозможно считать. Кроме того, варьируется толщина линии.

Вторую проблему легко решить, используя малоизвестное CSS-свойство vector-effect: non-scaling-stroke для каждого элемента.

Живой пример

Но с масштабированием текста придётся разбираться другими методами.

Решение. Часть 2

Оси графика необязательно рисовать прямо в SVG. Мы можем использовать обычные HTML-элементы и расположить их с помощью CSS. Чтобы синхронизировать масштабирование HTML- и SVG-слоев, применяем процентную координатную сетку.

            <!-- x axis -->
<div class="x axis" style="top: 100%; width: 100%; border-top: 1px solid black;">
  <span style="left: 0">0</span>
  <span style="left: 20%">2</span>
  <span style="left: 40%">4</span>
  <span style="left: 60%">6</span>
  <span style="left: 80%">8</span>
  <span style="left: 100%">10</span>
</div>

<!-- y axis -->
<div class="y axis" style="height: 100%; border-left: 1px solid black;">
  <span style="top: 100%">0</span>
  <span style="top: 50%">50</span>
  <span style="top: 0%">100</span>
</div>

<style>
  .axis {
    position: absolute;
  }

  .axis span {
    position: absolute;
    line-height: 1;
  }

  .x.axis span {
    top: 0.5em;
    transform: translate(-50%,0);
  }

  .y.axis span {
    left: -0.5em;
    transform: translate(-100%,-50%);
  }
</style>
        

График больше не ломается!

Живой пример

Ещё одно преимущество HTML-элементов – они автоматически привязываются к ближайшему пикселю. Так что не появляется неприятный эффект нечёткости, который встречается в SVG.

Готовое решение

Потребовалось много ручной работы. Чтобы этого избежать, воспользуемся готовым решением – компонентом Pancake для SvelteJS.

            <script>
  import * as Pancake from '@sveltejs/pancake';

  const points = [
    { 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 }
  ];
</script>

<div class="chart">
  <Pancake.Chart x1={0} x2={10} y1={0} y2={100}>
    <Pancake.Box x2={10} y2={100}>
      <div class="axes"></div>
    </Pancake.Box>

    <Pancake.Grid vertical count={5} let:value>
      <span class="x label">{value}</span>
    </Pancake.Grid>

    <Pancake.Grid horizontal count={3} let:value>
      <span class="y label">{value}</span>
    </Pancake.Grid>

    <Pancake.Svg>
      <Pancake.SvgLine data={points} let:d>
        <path class="data" {d}/>
      </Pancake.SvgLine>
    </Pancake.Svg>
  </Pancake.Chart>
</div>

<style>
  .chart {
    height: 100%;
    padding: 3em 2em 2em 3em;
    box-sizing: border-box;
  }

  .axes {
    width: 100%;
    height: 100%;
    border-left: 1px solid black;
    border-bottom: 1px solid black;
  }

  .y.label {
    position: absolute;
    left: -2.5em;
    width: 2em;
    text-align: right;
    bottom: -0.5em;
  }

  .x.label {
    position: absolute;
    width: 4em;
    left: -2em;
    bottom: -22px;
    font-family: sans-serif;
    text-align: center;
  }

  path.data {
    stroke: red;
    stroke-linejoin: round;
    stroke-linecap: round;
    stroke-width: 2px;
    fill: none;
  }
</style>
        

Живой пример

Благодаря Svelte этот чарт отрендерить на сервере с помощью NodeJS или вставить в DOM на клиенте. Для больших и сложных интерактивных графиков может потребоваться одновременно и то, и другое. Основную разметку можно генерировать на сервере, а затем прогрессивно улучшать на клиенте, добавляя интерактивность. Без компонентных фреймворков типа Svelte это было бы непросто.

Обратите внимание, Pancake не создаёт элементы span и path внутри SVG-изображения. Компоненты прежде всего логические – вы детально контролируете представление.

Усложняем задачу

Мы можем сделать гораздо больше, чем простейшие линейные графики:

Например, очень интересны диаграммы рассеивания (scatterplots).

Живой пример

Мы не можем использовать для них элементы circle, так как они могут некорректно масштабироваться. Компонент Pancake.Scatterplot генерирует path, состоящий из несвязанных дуг с нулевым радиусом. Размер окружностей определяется шириной контура (stroke width).

Благодаря тому, что библиотека оформлена в виде компонента Svelte, мы легко можем добавить графикам немного интерактива.

Живой пример

Взаимодействия также можно декларативно обрабатывать прямо внутри графика Pancake. Например, создадим дерево квадрантов (quadtree, во многом заимствовано из библиотеки D3), которое позволит находить ближайшую к курсору точку.

            <Pancake.SvgScatterplot data={points} let:d>
  <path class="data" {d}/>
</Pancake.SvgScatterplot>

<Pancake.Quadtree data={points} let:closest>
  {#if closest}
    <Pancake.SvgPoint x={closest.x} y={closest.y} let:d>
      <path class="highlight" {d}/>
    </Pancake.SvgPoint>
  {/if}
</Pancake.Quadtree>
        

В будущем в библиотеке появится поддержка canvas (2D и WebGL). Конечно, графики на canvas не смогут работать без JavaScript, но при работе с большими наборами данных SVG может не справляться.

Предостережения

Помните, Pancake является экспериментальной библиотекой, у него ещё не очень много реальных кейсов.

Основное внимание уделено управлению системой координат для двумерных чартов. Для графиков, гистограмм и диаграмм рассеивания этого достаточно, но круговые диаграммы, к сожалению, не поддерживаются.

На данный момент у библиотеки нет полноценной документации. Вы можете использовать примеры, представленные на домашней странице. Вполне возможно, что в будущем API будет меняться.

***

Название Pancake (Блин) связано с тем, что фактически чарты строятся путём наложения слоёв друг на друга. Проект во многом вдохновлялся фреймворком Layer Cake и библиотекой D3.

А какие инструменты вы применяете для построения графиков?

Источники

РУБРИКИ В СТАТЬЕ

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

Tools Programmer
Дублин, по итогам собеседования
Lead Gameplay Programmer
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG