Тултипы через vega-tooltip

Механизм всплывающих подсказок в экосистеме Vega и Vega-Lite реализуется через отдельный слой взаимодействия, построенный вокруг библиотеки vega-tooltip. Эта библиотека отвечает за отображение контекстной информации при наведении курсора на визуальные элементы графика и тесно интегрируется с системой событий Vega Runtime.


Архитектура всплывающих подсказок

В Vega/Vega-Lite тултипы не являются частью базового рендеринга. Они формируются на уровне взаимодействий:

  • Vega генерирует события наведения (mouseover, mouseout, mousemove)
  • runtime извлекает datum (данные визуального элемента)
  • vega-tooltip форматирует содержимое
  • HTML-слой отображает подсказку поверх SVG/Canvas

Ключевая особенность: тултип отделён от графической сцены и живёт в DOM независимо от визуализации.


Базовая интеграция vega-tooltip

Подключение в классическом сценарии через vega-embed:

import embed from 'vega-embed';
import { handler } from 'vega-tooltip';

const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  data: { values: [
    {category: 'A', value: 28},
    {category: 'B', value: 55}
  ]},
  mark: 'bar',
  encoding: {
    x: {field: 'category', type: 'nominal'},
    y: {field: 'value', type: 'quantitative'}
  }
};

embed('#vis', spec, {
  tooltip: new handler().call
});

Здесь handler() создаёт обработчик, который связывает события Vega с DOM-слоем тултипов.


Формирование содержимого тултипа

vega-tooltip автоматически извлекает значения из datum. По умолчанию отображается сериализованный объект, но форматирование контролируется через конфигурацию.

Базовое отображение данных

{
  "category": "A",
  "value": 28
}

Отображается как:

category: A
value: 28

Настройка форматирования через tooltip handler

vega-tooltip поддерживает кастомизацию через параметры обработчика:

import { handler } from 'vega-tooltip';

const tooltipHandler = new handler({
  formatTooltip: (value, sanitize) => {
    if (typeof value === 'object') {
      return Object.entries(value)
        .map(([k, v]) => `<b>${k}</b>: ${sanitize(v)}`)
        .join('<br/>');
    }
    return sanitize(value);
  }
});

Роль sanitize

Функция sanitize предотвращает XSS при использовании HTML в тултипах. Любая строка проходит через фильтр:

  • удаление скриптов
  • экранирование опасных тегов
  • контроль вложенного HTML

HTML-тултипы

Vega позволяет включать HTML-разметку в tooltip при явном разрешении.

const tooltipHandler = new handler({
  formatTooltip: (value, sanitize) => {
    return `
      <div class="tooltip">
        <div><b>Значение:</b> ${sanitize(value.value)}</div>
        <div><i>Категория:</i> ${sanitize(value.category)}</div>
      </div>
    `;
  }
});

Важный аспект безопасности

HTML не включается автоматически. Только через formatTooltip, где разработчик полностью контролирует структуру.


Использование tooltip в Vega-Lite спецификации

Vega-Lite поддерживает упрощённое объявление тултипов без прямого обращения к vega-tooltip.

{
  "mark": "point",
  "encoding": {
    "x": {"field": "x", "type": "quantitative"},
    "y": {"field": "y", "type": "quantitative"},
    "tooltip": [
      {"field": "x", "type": "quantitative"},
      {"field": "y", "type": "quantitative"}
    ]
  }
}

В этом случае Vega-Lite:

  • добавляет сигнал tooltip
  • генерирует формат отображения
  • передаёт данные в vega-tooltip

Отличия Vega и Vega-Lite в обработке тултипов

Vega

  • требуется ручная настройка signal listeners
  • tooltip формируется через события runtime
  • полный контроль над визуализацией подсказки

Vega-Lite

  • декларативное описание через encoding.tooltip
  • автоматическая генерация событий
  • интеграция через vega-embed

Пользовательский tooltip через signal listeners (Vega)

В Vega можно полностью заменить стандартную механику:

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

Далее внешний обработчик:

view.addSignalListener('tooltip', (name, value) => {
  tooltipHandler.call(null, value, {});
});

Стилизация tooltip через CSS

vega-tooltip рендерит HTML в контейнер с фиксированным классом.

Основной контейнер:

.vega-tooltip {
  position: absolute;
  background: #1e1e1e;
  color: #fff;
  padding: 8px 10px;
  border-radius: 4px;
  font-family: sans-serif;
  font-size: 12px;
  pointer-events: none;
}

Дополнительные состояния:

.vega-tooltip.active {
  opacity: 1;
  transform: translate(0, 0);
}

Позиционирование тултипов

Позиционирование рассчитывается автоматически на основе координат события мыши:

  • pageX, pageY события DOM
  • размеры tooltip DOM-элемента
  • границы viewport

Алгоритм включает:

  • предотвращение выхода за экран
  • смещение при близости к краям
  • адаптацию под scroll offset

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

При работе с тысячами точек:

  • tooltip вызывается только на hover-элементах
  • DOM обновляется минимально
  • данные не копируются, используется ссылка на datum

Оптимизации:

  • отключение лишних encodings
  • использование canvas вместо svg
  • ограничение глубины сериализации объекта datum

Форматирование числовых значений

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

const tooltipHandler = new handler({
  formatTooltip: (value, sanitize) => {
    if (typeof value === 'number') {
      return value.toFixed(2);
    }
    return sanitize(value);
  }
});

Кастомные шаблоны отображения

Распространённый подход — шаблонизация:

const template = (d) => `
  <div>
    <div>Категория: ${d.category}</div>
    <div>Значение: ${d.value}</div>
  </div>
`;

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

formatTooltip: (value, sanitize) => template(value)

Tooltip в multi-view визуализациях

При работе с facet, repeat, concat:

  • каждый view имеет собственный tooltip context
  • datum содержит вложенные поля (row, column, datum)
  • требуется аккуратная сериализация

Пример структуры:

{
  "row": "North",
  "column": "2024",
  "datum": {
    "value": 42
  }
}

Обработка сложных структур данных

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

formatTooltip: (value, sanitize) => {
  return `
    Region: ${value.row}<br/>
    Year: ${value.column}<br/>
    Value: ${value.datum.value}
  `;
}

Интеграция с внешними UI-библиотеками

vega-tooltip может быть заменён или расширен:

  • React overlay
  • Vue popover компоненты
  • D3 tooltip layer
  • Tippy.js

Пример интеграции с внешним рендерингом:

const handler = new Handler({
  formatTooltip: (value) => {
    externalTooltip.render(value);
    return '';
  }
});

Поведение при отсутствии данных

Если datum отсутствует или пуст:

  • tooltip не отображается
  • событие mouseout принудительно очищает состояние
  • DOM элемент скрывается без анимации

Работа с агрегированными значениями

В Vega-Lite агрегаты часто отображаются в tooltip автоматически:

{
  "aggregate": "sum",
  "field": "value"
}

В тултипе:

sum(value): 128

Формат можно переопределить через formatTooltip.


Обновление данных и реактивность

При обновлении данных через view.change():

  • tooltip автоматически синхронизируется
  • старые datum ссылки инвалидируются
  • новые события hover пересчитываются

Ограничения механизма tooltip

  • отсутствие сложной интерактивности внутри tooltip без кастомного HTML
  • потенциальная деградация производительности при чрезмерном HTML
  • зависимость от DOM поверх canvas рендеринга
  • ограниченная типизация datum в runtime