Создаём столбчатую диаграмму

В этом уроке мы познакомимся с основами Vega. Возьмём столбчатую диаграмму со всплывающими подсказками и разберём её на части. После урока вы сможете изучать и менять визуализации Vega самостоятельно.

Начнём с простой и полезной визуализации — столбчатой диаграммы:

Ниже приведена спецификация Vega, создающая диаграмму. Сначала просмотрите её целиком, а затем мы разберём каждую часть.

{
  "$schema": "https://vega.github.io/schema/vega/v6.json",
  "width": 400,
  "height": 200,
  "padding": 5,

  "data": [
    {
      "name": "table",
      "values": [
        {"category": "A", "amount": 28},
        {"category": "B", "amount": 55},
        {"category": "C", "amount": 43},
        {"category": "D", "amount": 91},
        {"category": "E", "amount": 81},
        {"category": "F", "amount": 53},
        {"category": "G", "amount": 19},
        {"category": "H", "amount": 87}
      ]
    }
  ],

  "signals": [
    {
      "name": "tooltip",
      "value": {},
      "on": [
        {"events": "rect:mouseover", "update": "datum"},
        {"events": "rect:mouseout",  "update": "{}"}
      ]
    }
  ],

  "scales": [
    {
      "name": "xscale",
      "type": "band",
      "domain": {"data": "table", "field": "category"},
      "range": "width",
      "padding": 0.05,
      "round": true
    },
    {
      "name": "yscale",
      "domain": {"data": "table", "field": "amount"},
      "nice": true,
      "range": "height"
    }
  ],

  "axes": [
    { "orient": "bottom", "scale": "xscale" },
    { "orient": "left", "scale": "yscale" }
  ],

  "marks": [
    {
      "type": "rect",
      "from": {"data":"table"},
      "encode": {
        "enter": {
          "x": {"scale": "xscale", "field": "category"},
          "width": {"scale": "xscale", "band": 1},
          "y": {"scale": "yscale", "field": "amount"},
          "y2": {"scale": "yscale", "value": 0}
        },
        "update": {
          "fill": {"value": "steelblue"}
        },
        "hover": {
          "fill": {"value": "red"}
        }
      }
    },
    {
      "type": "text",
      "encode": {
        "enter": {
          "align": {"value": "center"},
          "baseline": {"value": "bottom"},
          "fill": {"value": "#333"}
        },
        "update": {
          "x": {"scale": "xscale", "signal": "tooltip.category", "band": 0.5},
          "y": {"scale": "yscale", "signal": "tooltip.amount", "offset": -2},
          "text": {"signal": "tooltip.amount"},
          "fillOpacity": [
            {"test": "isNaN(tooltip.amount)", "value": 0},
            {"value": 1}
          ]
        }
      }
    }
  ]
}

Мы последовательно рассмотрим:

  1. Размер визуализации
  2. Данные
  3. Шкалы
  4. Оси
  5. Примитивы
  6. Сигналы
  7. Что дальше

Размер визуализации

Первые свойства верхнего уровня задают размер визуализации:

  "width": 400,
  "height": 200,
  "padding": 5,
  "autosize": "pad",

Значения width и height определяют размер области данных — части диаграммы, где отображаются значения. Оси и легенды могут занимать дополнительное место.

Свойство padding задаёт отступ между содержимым диаграммы и границей представления.

Свойство autosize определяет способ расчёта итогового размера:

  • "pad" (по умолчанию) добавляет место для всех элементов, включая оси и легенды. Размер области данных не меняется. Если примитивы расположены далеко за её пределами, представление может стать очень большим.
  • "fit" старается уместить диаграмму целиком — область данных, оси и легенды, но без отступов — в заданные width и height. Vega уменьшает область данных, чтобы освободить место; крупные элементы, например высокая легенда, могут обрезаться.
  • "none" отключает автоматический расчёт. Общий размер определяется только width, height и padding; дополнительное место для осей и легенд не выделяется.

Подробнее см. спецификацию верхнего уровня.

Данные

Свойство data содержит массив определений данных. Каждый элемент — объект с уникальным именем набора name. Здесь данные заданы прямо в спецификации через values: массив объектов с полями category (строковая категория) и amount (число).

  "data": [
    {
      "name": "table",
      "values": [
        {"category": "A", "amount": 28},
        {"category": "B", "amount": 55},
        {"category": "C", "amount": 43},
        {"category": "D", "amount": 91},
        {"category": "E", "amount": 81},
        {"category": "F", "amount": 53},
        {"category": "G", "amount": 19},
        {"category": "H", "amount": 87}
      ]
    }
  ],

В спецификации Vega данные можно:

  • загрузить из сети через url, в том числе из файлов JSON и CSV;
  • получить из ранее определённого набора через source;
  • задать динамически при создании визуализации.

Можно указать только одно из свойств values, url и source.

Наборы данных можно изменять преобразованиями: фильтровать, агрегировать или размещать элементы. Массив их определений передаётся в свойстве transform.

Подробнее см. разделы «Данные» и «Преобразования».

Шкалы

Шкалы сопоставляют значения данных визуальным свойствам, например координатам в пикселях или цветам:

  "scales": [
    {
      "name": "xscale",
      "type": "band",
      "domain": {"data": "table", "field": "category"},
      "range": "width",
      "padding": 0.05,
      "round": true
    },
    {
      "name": "yscale",
      "domain": {"data": "table", "field": "amount"},
      "nice": true,
      "range": "height"
    }
  ],

Здесь определены две шкалы — по одной для осей X и Y. По X используется порядковая шкала band, которая сопоставляет упорядоченную область domain (в данном случае буквы) визуальному диапазону range. По Y используется количественная шкала linear. Она применяется по умолчанию, поэтому тип в определении yscale не указан.

У каждой шкалы должно быть уникальное имя. Исключение составляют шкалы внутри примитива group: там повторное имя может переопределить внешнюю шкалу, но это более сложный приём.

Значения range "width" и "height" — удобные сокращения Vega. Здесь они соответствуют массивам [0, 400] и [200, 0], заданным размером визуализации. Диапазон можно определить и массивом напрямую: два числа подходят для координат, а более длинный массив, например из шестнадцатеричных цветов RGB вроде "#ffa804", — для собственной палитры шкалы ordinal.

Свойство domain определяет входную область шкалы. Её можно задать массивом значений (числовым интервалом или списком категорий) либо вычислить по данным. В примере область шкалы Y определяется минимальным и максимальным значениями поля amount набора table. По умолчанию количественные шкалы также включают ноль. Чтобы это отключить, добавьте "zero": false.

В xscale свойство padding создаёт промежутки между столбцами, а round выравнивает их по границам пикселей. В yscale указано "nice": true: Vega округляет границы области до удобных для чтения значений. Например, исходный диапазон [0, 94.345] может стать [0, 100].

Подробнее см. раздел «Шкалы».

Оси

Оси показывают шкалы с помощью делений и подписей, помогая читать диаграмму.

  "axes": [
    { "orient": "bottom", "scale": "xscale" },
    { "orient": "left", "scale": "yscale" }
  ],

В определении оси как минимум нужны её ориентация и шкала. Здесь ось X расположена bottom (снизу), а ось Y — left (слева).

Посмотрим, как изменить оси:

  "axes": [
    { "orient": "bottom", "scale": "xscale" },
    { "orient": "right", "scale": "yscale", "tickCount": 5,"offset": 6 }
  ],

Мы изменили ось Y сразу несколькими способами. "tickCount": 5 запрашивает около пяти делений вместо прежних десяти. "orient": "right" переносит ось с левой стороны диаграммы на правую. "offset": 6 сдвигает её ещё на шесть пикселей вправо. Результат показан ниже:

Подробнее см. раздел «Оси».

Примитивы

Примитивы — основные элементы визуализации. Их положение, размер, форма и цвет кодируют данные. Vega предоставляет набор графических примитивов, из которых можно собирать сложные изображения. Здесь для столбчатой диаграммы нужны только прямоугольники rect.

Каждый примитив должен иметь свойство type, задающее тип: rect, area, line, symbol и т. д. Свойство from указывает данные для отображения. Обычно достаточно сослаться на именованный набор из свойства data верхнего уровня. Если from опущено, создаётся один экземпляр примитива.

  "marks": [
    {
      "type": "rect",
      "from": {"data":"table"},
      "encode": {
        "enter": {
          "x": {"scale": "xscale", "field": "category"},
          "width": {"scale": "xscale", "band": 1},
          "y": {"scale": "yscale", "field": "amount"},
          "y2": {"scale": "yscale", "value": 0}
        },
        "update": {
          "fill": {"value": "steelblue"}
        },
        "hover": {
          "fill": {"value": "red"}
        }
      }
    },

Визуальные свойства примитива, например положение и цвет, задаются именованными наборами кодирования внутри encode. Стандартные наборы: enter (при создании примитива), exit (перед удалением), update (при изменении) и hover (при наведении указателя). В примере сначала выполняется enter, затем update, и возникает диаграмма. При наведении hover окрашивает столбец красным. Когда указатель уходит, повторный update возвращает исходный цвет. Если опустить update, столбец после наведения останется красным!

Рассмотрим свойства примитива в наборе enter:

          "x": {"scale": "xscale", "field": "category"},
          "width": {"scale": "xscale", "band": 1},
          "y": {"scale": "yscale", "field": "amount"},
          "y2": {"scale": "yscale", "value": 0}

Первые два свойства, x и width, задают горизонтальное положение и ширину столбца. Значение x — координата его левого края — получается применением шкалы "xscale" к полю данных category.

Ширина width берётся из полосовой шкалы xscale. Такая шкала делит пространственный диапазон на полосы одинакового размера. "band": 1 выбирает ширину всей полосы: число указывает используемую долю. При "band": 0.5 столбец занимал бы половину полосы.

Свойства y и y2 определяют вертикальное положение и высоту столбца. Можно было бы использовать y и height, как для горизонтали, но здесь удобнее задать две границы: верхнюю y и нижнюю y2. Постоянное значение 0 проходит через линейную шкалу yscale, поэтому один край каждого столбца совпадает с нулём. Порядок значений y и y2 не важен: Vega расположит столбец правильно. Аналогично можно сочетать x и x2, например для горизонтальных столбчатых диаграмм или временных шкал.

Помимо обычных примитивов — прямоугольников, дуг, символов и других — Vega поддерживает вложенность через особый тип group. Группа содержит другие примитивы, например для создания набора небольших диаграмм. Внутри группы можно определить собственные scales и axes.

Подробнее см. раздел «Примитивы».

Сигналы

Сигналы работают как динамические переменные: их выражения автоматически пересчитываются при изменении других сигналов или поступлении событий. Каждый сигнал должен иметь уникальное name. Начальное значение можно задать свойством value или выражением инициализации; другие свойства определяют, как значение сигнала меняется.

Здесь сигнал создаёт всплывающую подсказку. Значение tooltip меняется при событиях mouseover и mouseout на прямоугольниках rect. При каждом событии вычисляется соответствующее выражение. Когда указатель находится над столбцом, tooltip содержит связанную с ним запись данных; когда уходит — пустой объект.

  "signals": [
    {
      "name": "tooltip",
      "value": {},
      "on": [
        {"events": "rect:mouseover", "update": "datum"},
        {"events": "rect:mouseout",  "update": "{}"}
      ]
    }
  ],

Сигнал tooltip отслеживает данные выделенного столбца. Используем его, чтобы динамически менять кодирование текстовой подписи:

{
  "marks": [
    ...,
    {
      "type": "text",
      "encode": {
        "enter": {
          "align": {"value": "center"},
          "baseline": {"value": "bottom"},
          "fill": {"value": "#333"}
        },
        "update": {
          "x": {"scale": "xscale", "signal": "tooltip.category", "band": 0.5},
          "y": {"scale": "yscale", "signal": "tooltip.amount", "offset": -2},
          "text": {"signal": "tooltip.amount"},
          "fillOpacity": [
            {"test": "isNaN(tooltip.amount)", "value": 0},
            {"value": 1}
          ]
        }
      }
    }
  ]

Один экземпляр текстового примитива служит текстом подсказки; обратите внимание, что from опущено. Положение и текст берутся прямо из сигнала tooltip. Чтобы подпись появлялась только при наведении на прямоугольник, свойство fillOpacity задаётся условными правилами — цепочкой проверок. Если tooltip пуст, isNaN(tooltip.amount) равно true и текст полностью прозрачен. Иначе он видим.

Сигналы можно использовать по всей спецификации: например, для свойств преобразований, шкал и кодирования примитивов. Подробнее см. раздел «Сигналы».

Что дальше

Теперь вы разобрали целую визуализацию Vega. Попробуйте изменить пример: скопируйте полную спецификацию выше в редактор Vega и поэкспериментируйте.

  • Сможете изменить шкалы и оси?
  • Сможете превратить вертикальную столбчатую диаграмму в горизонтальную?
  • Сможете визуализировать другой набор данных похожей структуры?

После этого вы сможете разбирать и менять другие примеры. Во многих более сложных визуализациях используются преобразования для организации и размещения данных. При работе с ними обращайтесь к разделам документации по соответствующим компонентам спецификации.