События View: addEventListener

В библиотеке Vega ключевой объект исполнения визуализаций — View. Он представляет собой активный экземпляр спецификации, в котором происходит рендеринг сценографа, обработка сигналов, вычисление данных и управление взаимодействиями.

View выступает мостом между декларативным описанием графика и реальной DOM/Canvas-сценой. Любое интерактивное поведение — клики, наведение, перетаскивание, выделение — проходит через событийную систему этого объекта.


Система событий View

Событийная модель View построена вокруг регистрации обработчиков через addEventListener. Она не ограничивается только DOM-событиями, а расширяет их за счёт:

  • событий сценографа (scenegraph events),
  • событий пользовательского ввода,
  • событий обновления данных и сигналов,
  • событий жизненного цикла визуализации.

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


Метод View.addEventListener

Базовая форма регистрации обработчика:

view.addEventListener(type, handler)

Параметры

type — строка, определяющая тип события. handler — функция обратного вызова, вызываемая при возникновении события.

Общая сигнатура обработчика

function handler(event, item) {
    // event — объект события
    // item — элемент сцены (если применимо)
}

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


Типы событий в View

Событийная система Vega включает несколько категорий событий, которые могут быть перехвачены через addEventListener.

События мыши

Наиболее часто используемая группа событий:

  • click
  • dblclick
  • mousedown
  • mouseup
  • mouseover
  • mouseout
  • mousemove
  • wheel

Пример:

view.addEventListener('click', function(event, item) {
    console.log('Клик по элементу:', item);
});

События мыши связаны с элементами сценографа, поэтому item часто содержит информацию о визуальном примитиве (mark), данных и вычисленных значениях.


События касания

Для сенсорных устройств:

  • touchstart
  • touchmove
  • touchend
view.addEventListener('touchstart', (event, item) => {
    console.log('Начало касания');
});

События клавиатуры

  • keydown
  • keyup

Эти события обрабатываются на уровне всего View, а не отдельных графических элементов:

view.addEventListener('keydown', function(event) {
    if (event.key === 'Escape') {
        console.log('Сброс состояния');
    }
});

События сценографа

Внутренние события рендеринга и взаимодействия:

  • render — завершение отрисовки
  • beforeRender — перед отрисовкой
  • resize — изменение размеров view
view.addEventListener('resize', function(event) {
    console.log('Размеры обновлены');
});

События данных и сигналов

Хотя чаще используются специализированные методы (addDataListener, addSignalListener), часть системных событий может быть доступна через addEventListener:

  • обновление данных
  • изменения сигналов

Объект события

Объект event содержит контекст взаимодействия и является центральным элементом обработки.

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

{
    type: "click",
    x: 120,
    y: 80,
    item: { /* описание mark */ },
    timestamp: 1710000000000,
    event: MouseEvent
}

Основные поля

type Строка, тип события.

x, y Координаты в системе координат view.

item Ссылка на элемент сцены:

  • тип mark (rect, circle, text и т.д.)
  • данные, связанные с элементом
  • вычисленные визуальные свойства

event Оригинальный DOM-событие (MouseEvent, KeyboardEvent).


Работа с элементами сцены

Одним из ключевых сценариев является извлечение данных из визуального элемента:

view.addEventListener('click', function(event, item) {
    if (item && item.datum) {
        console.log('Данные точки:', item.datum);
    }
});

datum — это исходная запись данных, привязанная к визуальному элементу.


Интерактивная фильтрация и выделение

События часто используются для реализации взаимодействий:

Выделение точки

let selected = null;

view.addEventListener('click', function(event, item) {
    selected = item.datum;
    view.signal('selectedPoint', selected).run();
});

Здесь происходит связка событийной модели и системы сигналов.


Наведение и подсветка

view.addEventListener('mouseover', function(event, item) {
    view.signal('hovered', item.datum).run();
});

view.addEventListener('mouseout', function() {
    view.signal('hovered', null).run();
});

Интеграция с сигналами

Сигналы в Vega представляют реактивные переменные, которые управляют состоянием визуализации.

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

view.addSignalListener('selection', function(name, value) {
    console.log('Сигнал изменён:', value);
});

Комбинация событий и сигналов создаёт двустороннюю модель:

  • событие → изменение сигнала
  • изменение сигнала → перерисовка сцены

Удаление обработчиков

Для управления жизненным циклом взаимодействий предусмотрен метод:

view.removeEventListener(type, handler)

Важно сохранять ссылку на функцию:

function onClick(event, item) {
    console.log(item);
}

view.addEventListener('click', onClick);
view.removeEventListener('click', onClick);

Без сохранённой ссылки удаление невозможно.


Особенности обработки событий

1. Приоритет сценографа

События обрабатываются после построения сцены, что позволяет получать актуальные позиции элементов.


2. Делегирование через marks

В Vega события не привязаны к отдельным DOM-узлам, а проходят через сценограф:

  • один canvas
  • логическое разделение элементов
  • вычисление hit-test

3. Производительность

Чрезмерное количество обработчиков может влиять на:

  • частоту перерисовки
  • скорость hit-testing
  • использование памяти

Оптимизация достигается через:

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

Особенности Vega-Lite

В Vega-Lite работа с событиями чаще абстрагируется через декларативные конструкции selection.

Однако при компиляции Vega-Lite в Vega создаётся полноценный View, где addEventListener становится доступным на уровне результирующей спецификации.

Типичный сценарий:

  • Vega-Lite описывает selection
  • компилятор превращает его в signals + event listeners
  • Vega View обрабатывает события напрямую

Взаимодействие с пользовательскими слоями

При расширении визуализации кастомной логикой события используются для:

  • синхронизации нескольких графиков
  • построения linked views
  • создания инструментов brush & zoom
view.addEventListener('brush', function(event) {
    const range = event.selection;
    console.log('Выделенный диапазон:', range);
});

Композиция нескольких обработчиков

Один тип события может иметь несколько независимых обработчиков:

view.addEventListener('click', handlerA);
view.addEventListener('click', handlerB);

Порядок выполнения не гарантируется, что требует аккуратного проектирования взаимодействий.


Отладка событий

Для диагностики поведения часто используется логирование:

view.addEventListener('*', function(event) {
    console.log(event);
});

Хотя wildcard-события не всегда поддерживаются напрямую, аналогичный эффект достигается через регистрацию на ключевые типы событий.