Привязка выделения к шкале: zoom и pan

Интерактивные визуализации редко ограничиваются статичным отображением данных. При работе с временными рядами, большими массивами точек, финансовыми графиками или аналитическими панелями возникает необходимость перемещаться по данным, изменять масштаб отображения и исследовать отдельные фрагменты графика. В экосистеме Vega и Vega-Lite эти возможности реализуются через механизм привязки выделения к шкале.

В Vega-Lite подобная техника строится вокруг параметров (params) и свойства bind: "scales", которое автоматически синхронизирует область выделения с доменом шкал визуализации.


Основы zoom и pan в Vega-Lite

Под zoom понимается изменение масштаба отображения данных. Под pan — перемещение области просмотра без изменения масштаба.

В Vega-Lite эти механики реализуются через интервальное выделение (interval selection), привязанное к шкалам:

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",

  "data": {
    "url": "data/stocks.csv"
  },

  "params": [
    {
      "name": "zoom",
      "select": "interval",
      "bind": "scales"
    }
  ],

  "mark": "line",

  "encoding": {
    "x": {
      "field": "date",
      "type": "temporal"
    },

    "y": {
      "field": "price",
      "type": "quantitative"
    },

    "color": {
      "field": "symbol",
      "type": "nominal"
    }
  }
}

Ключевой элемент:

"bind": "scales"

Именно он связывает выделение со шкалами осей.

После этого:

  • прокрутка колёсиком мыши изменяет масштаб;
  • перетаскивание перемещает область просмотра;
  • двойной клик сбрасывает состояние.

Как работает bind: “scales”

При использовании:

"bind": "scales"

Vega-Lite перестаёт трактовать интервальное выделение как обычный прямоугольник выбора. Вместо этого:

  • координаты выделения синхронизируются с доменом осей;
  • изменение выделения изменяет диапазон шкалы;
  • изменение шкалы обновляет выделение.

Фактически выделение становится механизмом управления отображаемой областью данных.


Поведение по умолчанию

После активации привязки шкал доступны стандартные интерактивные действия.

Масштабирование (zoom)

Используется колесо мыши или жест pinch на тач-устройствах.

Пример:

  • прокрутка вверх приближает данные;
  • прокрутка вниз отдаляет график.

Перемещение (pan)

Происходит через drag-перетаскивание.

Область данных сдвигается по оси.

Сброс состояния

Двойной клик:

"clear": "dblclick"

по умолчанию возвращает исходный домен шкалы.


Полноценный пример zoom/pan

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",

  "width": 700,
  "height": 400,

  "data": {
    "url": "data/sp500.csv"
  },

  "params": [
    {
      "name": "view",
      "select": {
        "type": "interval"
      },
      "bind": "scales"
    }
  ],

  "mark": {
    "type": "line",
    "interpolate": "monotone"
  },

  "encoding": {
    "x": {
      "field": "date",
      "type": "temporal",
      "title": "Дата"
    },

    "y": {
      "field": "price",
      "type": "quantitative",
      "title": "Цена"
    }
  }
}

Такой график уже поддерживает:

  • zoom;
  • pan;
  • reset;
  • адаптивное изменение домена.

Интервальное выделение

Основа zoom/pan — интервальное выделение:

"select": {
  "type": "interval"
}

Интервал хранит диапазоны:

{
  "x": [100, 400],
  "y": [20, 80]
}

При привязке к шкале эти значения становятся доменом осей.


Ограничение zoom только одной осью

Часто необходимо масштабировать только горизонтальную ось.

Например:

  • временные ряды;
  • финансовые графики;
  • графики логов;
  • телеметрия.

Для этого используется свойство encodings.

Zoom только по X

{
  "params": [
    {
      "name": "zoomX",

      "select": {
        "type": "interval",
        "encodings": ["x"]
      },

      "bind": "scales"
    }
  ]
}

Теперь:

  • масштабируется только ось X;
  • Y остаётся фиксированной.

Zoom только по Y

{
  "params": [
    {
      "name": "zoomY",

      "select": {
        "type": "interval",
        "encodings": ["y"]
      },

      "bind": "scales"
    }
  ]
}

Подобный режим применяется реже, но полезен:

  • в спектральном анализе;
  • при работе с амплитудами;
  • в научных визуализациях.

Одновременный zoom по двум осям

Стандартный режим:

"encodings": ["x", "y"]

или отсутствие encodings.

Пример:

{
  "select": {
    "type": "interval",
    "encodings": ["x", "y"]
  }
}

Управление событиями zoom

Vega-Lite позволяет переопределять события взаимодействия.

Изменение события начала drag

{
  "select": {
    "type": "interval",
    "translate": "[mousedown, window:mouseup] > window:mousemove!"
  }
}

Здесь:

  • mousedown начинает pan;
  • mousemove двигает область;
  • mouseup завершает действие.

Отключение pan

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

{
  "select": {
    "type": "interval",
    "translate": false
  }
}

Теперь drag-перемещение отключено.


Отключение zoom

{
  "select": {
    "type": "interval",
    "zoom": false
  }
}

Панорамирование остаётся активным.


Настройка колеса мыши

Можно задать собственное событие zoom:

{
  "select": {
    "type": "interval",
    "zoom": "wheel![event.shiftKey]"
  }
}

Теперь zoom работает только при зажатом Shift.


Пример кастомного управления

{
  "params": [
    {
      "name": "custom",

      "select": {
        "type": "interval",

        "zoom": "wheel!",

        "translate":
          "[mousedown[event.shiftKey], window:mouseup] > window:mousemove!"
      },

      "bind": "scales"
    }
  ]
}

Особенности:

  • zoom всегда активен;
  • pan работает только с Shift + drag.

Синхронизация нескольких графиков

Одна из сильнейших возможностей Vega-Lite — общий zoom между несколькими представлениями.


Общая шкала для layered charts

{
  "layer": [
    {
      "mark": "line"
    },
    {
      "mark": "point"
    }
  ],

  "params": [
    {
      "name": "zoom",
      "select": "interval",
      "bind": "scales"
    }
  ]
}

Все слои используют одну систему координат.


Синхронизация concat-графиков

Вертикальная композиция

{
  "vconcat": [
    {
      "mark": "line"
    },
    {
      "mark": "area"
    }
  ],

  "resolve": {
    "scale": {
      "x": "shared"
    }
  }
}

Теперь zoom верхнего графика влияет на нижний.


Focus + Context

Классический паттерн аналитических интерфейсов.

Состоит из:

  1. основного графика;
  2. мини-карты навигации;
  3. интервального выделения.

Пример focus + context

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",

  "vconcat": [

    {
      "width": 700,
      "height": 300,

      "mark": "line",

      "encoding": {
        "x": {
          "field": "date",
          "type": "temporal",
          "scale": {
            "domain": {
              "param": "brush"
            }
          }
        },

        "y": {
          "field": "price",
          "type": "quantitative"
        }
      }
    },

    {
      "width": 700,
      "height": 80,

      "params": [
        {
          "name": "brush",

          "select": {
            "type": "interval",
            "encodings": ["x"]
          }
        }
      ],

      "mark": "area",

      "encoding": {
        "x": {
          "field": "date",
          "type": "temporal"
        },

        "y": {
          "field": "price",
          "type": "quantitative"
        }
      }
    }
  ]
}

Здесь нижний график управляет масштабом верхнего.


Использование domain с param

Важнейший механизм:

"domain": {
  "param": "brush"
}

Он связывает домен шкалы с параметром выделения.

Это фундамент для:

  • overview/detail;
  • mini-map;
  • timeline navigation;
  • range filtering.

Zoom в scatter plot

Особенно полезен для больших наборов точек.

{
  "mark": "point",

  "params": [
    {
      "name": "zoom",
      "select": "interval",
      "bind": "scales"
    }
  ]
}

При увеличении:

  • точки не теряют точность;
  • исследуются плотные области;
  • облегчается поиск кластеров.

Производительность при больших объёмах данных

Zoom/pan может становиться дорогой операцией.

Основные причины:

  • перерасчёт шкал;
  • обновление mark;
  • перерисовка canvas/SVG;
  • вычисление transforms.

Оптимизация производительности

Использование canvas renderer

vegaEmbed("#view", spec, {
  renderer: "canvas"
});

Canvas значительно быстрее SVG при:

  • десятках тысяч точек;
  • плотных scatter plot;
  • потоковых данных.

Уменьшение количества mark

Например:

{
  "mark": {
    "type": "line",
    "point": false
  }
}

Точки часто становятся узким местом.


Downsampling

Перед отображением:

  • агрегируются данные;
  • уменьшается плотность;
  • исключаются лишние элементы.

Ограничение области zoom

Иногда нельзя разрешать бесконечное приближение.

В Vega это контролируется через сигналы и ограничения домена.

В Vega-Lite прямого API меньше, но ограничения можно реализовать через:

  • пользовательские сигналы Vega;
  • transform;
  • фильтрацию диапазонов.

Переход к Vega для сложного zoom

Vega-Lite покрывает большинство сценариев, но сложные случаи требуют Vega.

Например:

  • инерционный pan;
  • multi-touch;
  • ограничения масштаба;
  • динамический zoom speed;
  • кастомная физика взаимодействия.

Реализация zoom в Vega

В Vega масштабирование строится через сигналы (signals).

Пример:

{
  "signals": [
    {
      "name": "xDomain",
      "update": "domain('xscale')"
    }
  ]
}

Сигналы:

  • отслеживают события;
  • обновляют домен;
  • управляют шкалами.

Wheel-события в Vega

{
  "name": "zoom",

  "on": [
    {
      "events": "wheel!",
      "update": "pow(1.001, event.deltaY)"
    }
  ]
}

Так вычисляется коэффициент масштабирования.


Drag-pan в Vega

{
  "name": "delta",

  "on": [
    {
      "events": "[mousedown, window:mouseup] > window:mousemove",
      "update": "x() - xdown"
    }
  ]
}

Связь сигналов со scale domain

{
  "scales": [
    {
      "name": "xscale",

      "domain": {
        "signal": "xDomain"
      }
    }
  ]
}

Разница между Vega и Vega-Lite

Возможность Vega-Lite Vega
Базовый zoom Да Да
Pan Да Да
bind: scales Да Нет
Полный контроль событий Ограниченно Полностью
Кастомная физика Нет Да
Multi-touch логика Частично Полностью
Сложные сигналы Нет Да
Простота разработки Очень высокая Средняя

Когда использовать bind: “scales”

Подход особенно эффективен для:

  • аналитических панелей;
  • временных рядов;
  • мониторинга;
  • финансовых графиков;
  • scientific visualization;
  • BI-интерфейсов;
  • exploratory data analysis.

Когда bind: “scales” недостаточно

Ограничения Vega-Lite становятся заметны при:

  • extremely large datasets;
  • custom gesture systems;
  • сложных synchronized dashboards;
  • nonlinear zoom;
  • deep interaction state management.

В таких случаях применяется Vega.


Комбинирование zoom с фильтрацией

Zoom может работать вместе с transform filter.

Пример:

{
  "transform": [
    {
      "filter": "datum.price > 100"
    }
  ]
}

После фильтрации шкалы продолжают масштабироваться.


Zoom и tooltip

Tooltip автоматически адаптируется к новой области просмотра.

{
  "mark": {
    "type": "line",
    "tooltip": true
  }
}

При сильном приближении tooltip становится значительно полезнее.


Zoom и temporal шкалы

Наиболее частый сценарий.

{
  "x": {
    "field": "date",
    "type": "temporal"
  }
}

При zoom Vega-Lite автоматически:

  • пересчитывает ticks;
  • изменяет density labels;
  • обновляет форматирование дат.

Поведение temporal axis при zoom

При разных уровнях масштаба:

Масштаб Формат
Годы 2020
Месяцы Jan 2020
Дни 12 Jan
Часы 12:30
Секунды 12:30:15

Это делает навигацию особенно удобной.


Использование zoom в dashboard

Типичная архитектура:

  1. overview chart;
  2. detail chart;
  3. shared selection;
  4. interactive filters;
  5. tooltip;
  6. linked highlighting.

Zoom становится центральным механизмом исследования данных.


Влияние renderer на взаимодействие

SVG

Преимущества:

  • качественный DOM;
  • доступность;
  • стилизация CSS.

Недостатки:

  • хуже масштабируется по производительности.

Canvas

Преимущества:

  • высокая скорость;
  • плавный zoom;
  • большие объёмы данных.

Недостатки:

  • ограниченная DOM-интеграция.

Практический паттерн для time-series

Наиболее распространённая структура:

{
  "params": [
    {
      "name": "zoom",

      "select": {
        "type": "interval",
        "encodings": ["x"]
      },

      "bind": "scales"
    }
  ]
}

Почему используется только x:

  • время почти всегда располагается горизонтально;
  • вертикальный масштаб должен оставаться стабильным;
  • уменьшается дезориентация пользователя.

Частые ошибки

Конфликт scale domain и bind: scales

Ошибка:

"scale": {
  "domain": [0, 100]
}

при активном:

"bind": "scales"

Фиксированный домен мешает интерактивному zoom.


Использование ordinal scale

Категориальные шкалы плохо подходят для zoom/pan.

Проблемный пример:

"type": "ordinal"

Лучше использовать:

  • quantitative;
  • temporal;
  • continuous scales.

Слишком плотный scatter plot

При миллионах точек даже canvas начинает тормозить.

Решения:

  • binning;
  • aggregation;
  • WebGL;
  • sampling;
  • progressive rendering.

Интеграция с vegaEmbed

Стандартное подключение:

vegaEmbed("#chart", spec);

С canvas:

vegaEmbed("#chart", spec, {
  renderer: "canvas"
});

Реактивность шкал

При zoom Vega/Vega-Lite автоматически:

  • пересчитывает scale domain;
  • обновляет axis;
  • меняет ticks;
  • пересчитывает mark positions;
  • обновляет hover-state;
  • синхронизирует selections.

Именно реактивная архитектура делает zoom/pan в Vega-Lite чрезвычайно компактным по коду.


Архитектурная модель bind: “scales”

Внутренне механизм работает как реактивная цепочка:

input event
    ↓
selection update
    ↓
scale domain update
    ↓
axis recomputation
    ↓
mark recomputation
    ↓
rerender

Эта модель лежит в основе всей интерактивности Vega и Vega-Lite.