Параметры, условия и фильтры

Параметры

Параметры — строительные блоки интерактивности Altair. Они бывают двух типов: переменные и выборы. Рассмотрим эти понятия на примерах.

Этот материал существенно изменился с выходом Altair 5.

Переменные: повторное использование значений

Переменные параметры позволяют определить значение один раз, а затем использовать его в разных частях диаграммы. Начнём с простой диаграммы рассеяния по набору cars:

import altair as alt
from altair.datasets import data

cars = data.cars.url

alt.Chart(cars).mark_circle().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color='Origin:N'
)

Переменные параметры создаются функцией param. Здесь свойство value задаёт начальное значение 0.1:

op_var = alt.param(value=0.1)

Чтобы использовать переменную в спецификации, явно добавим её к диаграмме методом add_params. Затем можно ссылаться на неё при задании кодирований, условий и фильтров. В этом примере параметр op_var задаёт непрозрачность:

op_var = alt.param(value=0.1)

alt.Chart(cars).mark_circle(opacity=op_var).encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color='Origin:N'
).add_params(
    op_var
)

Возникает вопрос: нужны ли все эти действия? Тот же результат можно получить более привычным способом, без param и add_params:

op_var2 = 0.1

alt.Chart(cars).mark_circle(opacity=op_var2).encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color='Origin:N'
)

Преимущество param становится очевидным при добавлении ещё одного компонента. Свойство bind связывает параметр с элементом ввода, в данном случае с ползунком:

slider = alt.binding_range(min=0, max=1, step=0.05, name='opacity:')
op_var = alt.param(value=0.1, bind=slider)

alt.Chart(cars).mark_circle(opacity=op_var).encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color='Origin:N'
).add_params(
    op_var
)

Теперь ползунок динамически меняет непрозрачность точек. Подробнее о связывании параметров с элементами ввода см. «Привязки и виджеты».

Интерактивность Altair полностью выполняется в браузере. Диаграммы можно сохранить в HTML и передать коллегам: для взаимодействия с ними достаточно браузера, устанавливать Python не требуется.

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

Параметры выбора задают запросы к данным, управляемые действиями пользователя на диаграмме: например, щелчками или перетаскиванием. Есть два типа выбора: selection_interval и selection_point.

Создадим простую диаграмму и добавим интервальный выбор. Его можно задать через param(select="interval"), но удобнее использовать сокращённую функцию selection_interval.

Вот диаграмма рассеяния по набору cars:

import altair as alt
from altair.datasets import data

cars = data.cars.url

alt.Chart(cars).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color='Origin:N'
)

Сначала создадим интервальный выбор функцией selection_interval. Такой выбор также называют brush, то есть выделением области:

brush = alt.selection_interval()

Добавим его к диаграмме через add_params:

alt.Chart(cars).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color='Origin:N'
).add_params(
    brush
)

Теперь можно нажать кнопку мыши и перетащить указатель, чтобы создать область выбора, а затем перемещать эту область.

Как и в примере с переменной, мы создали параметр — brush = alt.selection_interval() — и добавили его методом add_params. Однако пока не определили, как диаграмма должна реагировать на выбор. Это показано в следующем разделе.

Условия

Этот материал существенно изменился с выходом Altair 5.5.0. Функция when появилась в версии 5.4.0; рекомендуется использовать её вместо condition.

В предыдущем примере область выбора пока не влияет на диаграмму. Для реакции на выбор нужно обратиться к brush в спецификации. Создадим условное кодирование цвета функцией when:

conditional = alt.when(brush).then("Origin:N").otherwise(alt.value("lightgray"))

alt.Chart(cars).mark_point().encode(
    x="Horsepower:Q",
    y="Miles_per_Gallon:Q",
    color=conditional,
).add_params(
    brush
)

Цвет точек теперь зависит от того, попадают ли они в область выбора. Параметр brush используется как предикат — выражение, принимающее значение True или False.

Это задаёт определение conditional:

conditional = alt.when(brush).then("Origin:N").otherwise(alt.value("lightgray"))

Для точек внутри выбора предикат равен True, для точек снаружи — False. Строка "Origin:N" определяет цвет точек внутри области, а alt.value('lightgray') задаёт постоянный цвет остальных точек.

Как работает when

Синтаксис when-then-otherwise основан на polars.when и напоминает конструкцию if-else в Python:

# alt.when(brush)
if brush:
    # .then("Origin:N")
    color = "Origin:N"
else:
    # .otherwise(alt.value("lightgray"))
    color = alt.value("lightgray")

Если не указывать .otherwise(), используется значение канала по умолчанию:

source = data.cars()
brush = alt.selection_interval()

points = alt.Chart(source).mark_point().encode(
    x="Horsepower",
    y="Miles_per_Gallon",
    color=alt.when(brush).then(alt.value("goldenrod"))
).add_params(
    brush
)

points

Несколько условных ветвей, соответствующих if, elif, ..., elif в Python, задаются цепочкой вызовов when. После знакомства с типами выбора мы рассмотрим работающий пример.

Более сложные случаи описаны в справочнике функции when и в примерах галереи:

Связь условий между диаграммами

Условные кодирования особенно полезны, когда один выбор действует в нескольких представлениях данных составной диаграммы. Создадим Chart по предыдущему примеру и расположим рядом две версии: в первой канал x связан с "Horsepower", во второй — с "Acceleration":

chart = alt.Chart(cars).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color=alt.when(brush).then("Origin:N").otherwise(alt.value("lightgray")),
).properties(
    width=250,
    height=250
).add_params(
    brush
)

chart | chart.encode(x='Acceleration:Q')

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

Поведение каждого типа выбора настраивается его атрибутами. Например, можно ограничить выделение каналом "x", чтобы сосредоточиться на этом признаке. Изменим определение brush, оставив остальные настройки прежними:

brush = alt.selection_interval(encodings=['x'])

chart = alt.Chart(cars).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color=alt.when(brush).then("Origin:N").otherwise(alt.value("lightgray")),
).properties(
    width=250,
    height=250
).add_params(
    brush
)

chart | chart.encode(x='Acceleration:Q')

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

# Lower numbers for `order` means further into the background
selected_on_top = alt.when(brush).then(alt.value(1)).otherwise(alt.value(0))
chart.encode(order=selected_on_top) | chart.encode(order=selected_on_top, x='Acceleration:Q')

Другой пример приведён в галерее: «Порядок отрисовки выбранных точек».

Фильтры

Параметр выбора фильтрует данные почти так же, как используется в when. В transform_filter(brush) параметр brush снова выступает предикатом. Точки с результатом True, то есть внутри области выбора, сохраняются; точки с результатом False исключаются.

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

brush = alt.selection_interval()

points = alt.Chart(cars).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color='Origin:N'
).add_params(
    brush
)

bars = alt.Chart(cars).mark_bar().encode(
    x='count()',
    y='Origin:N',
    color='Origin:N'
).transform_filter(
    brush
)

points & bars

Типы выбора

Рассмотрев основы взаимодействия, изучим типы параметров выбора систематически. Для действий мышью используются два основных типа:

  1. Интервальный выбор — выделение диапазонов данных нажатием и перетаскиванием.
  2. Точечный выбор — выделение отдельных элементов щелчком или наведением.

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

def make_example(selector: alt.Parameter) -> alt.Chart:
    cars = data.cars.url

    return alt.Chart(cars).mark_rect().encode(
        x="Cylinders:O",
        y="Origin:N",
        color=alt.when(selector).then("count()").otherwise(alt.value("lightgray")),
    ).properties(
        width=300,
        height=180
    ).add_params(
        selector
    )

Интервальный выбор

Интервальный выбор выделяет диапазон элементов данных нажатием и перетаскиванием. Такой выбор часто называют brush; его применяют для фильтрации, масштабирования и выделения диапазонов точек.

Простой интервальный выбор

Самый простой вариант создаётся функцией selection_interval:

interval = alt.selection_interval()
make_example(interval)

Нажатие и перетаскивание создаёт область выбора. Её можно перемещать, изменяя выбранные элементы данных.

Настройка интервального выбора

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

interval_x = alt.selection_interval(encodings=['x'], empty=False)
make_example(interval_x)

Аргумент empty=False можно также передать функции when, чтобы отдельно настроить реакцию конкретного условия на пустой выбор.

По умолчанию empty=True: при пустом выборе предикат равен True для всех точек. При empty=False он равен False для всех точек. Это определяет поведение до первого выделения:

brush = alt.selection_interval()

# Default behavior (empty=True): When no selection is made in this example, all points will be colored according to their "Origin" category (they are considered as *selected*, and colored according to the `then` clause)
color=alt.when(brush).then("Origin:N").otherwise(alt.value("lightgray"))

# Custom behavior (empty=False): When no selection is made in this example, all points will be lightgray (they are considered as *unselected*, and colored according to the `otherwise` clause )
color=alt.when(brush, empty=False).then("Origin:N").otherwise(alt.value("lightgray"))

Начальные значения

Часто полезно сразу показать выбранную область, чтобы направить внимание пользователя. Для этого служит аргумент value функции selection_interval.

Начальные значения особенно полезны для:

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

В этом примере временного ряда начальный выбор охватывает период финансового кризиса 2008 года:

import altair as alt
from altair.datasets import data
import datetime as dt

source = data.sp500.url

# Define initial date range to select
date_range = (dt.date(2007, 6, 30), dt.date(2009, 6, 30))

# Create interval selection with initial value
brush = alt.selection_interval(
    encodings=['x'],
    value={'x': date_range}
)

# Create base chart for both panels
base = alt.Chart(source, width=600, height=200).mark_area().encode(
    x = 'date:T',
    y = 'price:Q'
)

# Upper panel shows detailed view filtered by the brush
upper = base.encode(
    alt.X('date:T').scale(domain=brush)
)

# Lower panel shows overview with the brush control
lower = base.properties(
    height=60
).add_params(brush)

# Combine the two charts
upper & lower

Пример показывает приём «обзор и детали»: одна диаграмма отображает весь набор данных, а другая — подробное представление выбранной части.

Для дат можно напрямую использовать объекты Python datetime.date и datetime.datetime. Altair автоматически преобразует их в формат Vega-Lite.

Формат value зависит от каналов выбора:

  • канал x: value={'x': [min, max]};
  • канал y: value={'y': [min, max]};
  • оба канала: value={'x': [xmin, xmax], 'y': [ymin, ymax]}.

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

cylinder_select = alt.selection_interval(
    encodings=['x'],
    value={"x": [4, 6]}  # Select 4 and 6 (but not 5) cylinder cars
)
make_example(cylinder_select)

Точечный выбор

Точечный выбор выделяет отдельные элементы данных. Он работает с дискретными точками и подходит для интерактивных легенд, подсказок и выделения конкретных объектов.

Простой точечный выбор

Самый простой вариант создаётся функцией selection_point. По умолчанию точки выбираются щелчком:

point = alt.selection_point()
make_example(point)

Значение on='pointerover' задаёт выбор при наведении. Аргумент on принимает любой селектор потока событий Vega. Более сложный пример показан в конце раздела композиции параметров.

Флаг nearest=True выделяет ближайшую точку, не требуя точного попадания указателя:

point_nearest = alt.selection_point(on='pointerover', nearest=True)
make_example(point_nearest)

Точечные выборы поддерживают множественное выделение. По умолчанию щелчок с зажатой клавишей Shift добавляет или удаляет точку из выбора. Попробуйте это в примерах выше.

Начальные значения

Начальные значения точечного выбора тоже задаются аргументом value. Для такого выбора значения указывают через поля или каналы:

point_initial = alt.selection_point(
    fields=['Origin'],
    value=[{'Origin': 'USA'}]  # Initially select USA
)
make_example(point_initial)

Что именно выбирается

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

Например, создадим небольшую диаграмму, выполняющую роль интерактивной легенды. Выбор fields=['Origin'] нацелен на поле Origin. Щелчок по точке в правой верхней диаграмме-легенде выбирает все точки с соответствующим значением Origin:

selection = alt.selection_point(fields=['Origin'])
color = (
    alt.when(selection)
    .then(alt.Color("Origin:N").legend(None))
    .otherwise(alt.value("lightgray"))
)

scatter = alt.Chart(cars).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color=color,
    tooltip='Name:N'
)

legend = alt.Chart(cars).mark_point().encode(
    alt.Y('Origin:N').axis(orient='right'),
    color=color
).add_params(
    selection
)

scatter | legend

Вместо fields=['Origin'] можно написать encodings=['color'], поскольку канал color связан с 'Origin'. В Altair также есть сокращённый способ создания интерактивных легенд, описанный в разделе привязки легенды.

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

selection = alt.selection_point(fields=['Origin', 'Cylinders'])
color = (
    alt.when(selection)
    .then(alt.Color("Origin:N").legend(None))
    .otherwise(alt.value("lightgray"))
)

scatter = alt.Chart(cars).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color=color,
    tooltip='Name:N'
)

legend = alt.Chart(cars).mark_rect().encode(
    alt.Y('Origin:N').axis(orient='right'),
    x='Cylinders:O',
    color=color
).add_params(
    selection
)

scatter | legend

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

Объединение параметров

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

Несколько условий

В этом примере точки увеличиваются при наведении, а после щелчка получают красную заливку. Аргумент empty=False гарантирует отсутствие начального выбора. Попробуйте удерживать Shift, чтобы выбирать несколько точек наведением или щелчками:

click = alt.selection_point(empty=False)
hover = alt.selection_point(on='pointerover', empty=False)

points = alt.Chart(cars).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    fill=alt.when(click).then(alt.value('red')),
    size=alt.when(hover).then(alt.value(1000))
).add_params(
    click, hover
)

points

Условные ветви

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

Здесь точка при наведении становится жёлтой, а после щелчка — красной. При щелчке указатель находится над точкой, поэтому активны оба условия. Приоритет имеет более ранняя ветвь. Поменяйте местами две конструкции when.then: тогда щелчок не будет менять заливку на красную.

click = alt.selection_point(empty=False)
hover = alt.selection_point(on='pointerover', empty=False)

points = alt.Chart(cars).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    fill=(
        alt.when(click)
        .then(alt.value('red'))
        .when(hover)
        .then(alt.value('gold'))
    ),
    size=alt.when(hover).then(alt.value(1000))
).add_params(
    click, hover
)

points

Логическая композиция параметров

Altair поддерживает объединение параметров операторами &, | и ~: логическими И, ИЛИ и НЕ соответственно. Полученные выражения используются и в фильтрах, и в условиях.

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

# empty=False ensure that no points are selected before a selection is drawn
brush = alt.selection_interval(empty=False)
brush2 = alt.selection_interval(empty=False)

points = alt.Chart(cars).mark_point(size=10).encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color='Origin:N'
)

points2 = points.encode(
    x='Acceleration:Q',
    y='Miles_per_Gallon:Q',
)

bars = alt.Chart(cars).mark_bar().encode(
    x='count()',
    y='Origin:N',
    color='Origin:N'
).transform_filter(
    brush & brush2
)

(points.add_params(brush) | points2.add_params(brush2)) & bars

Для более сложного условного кодирования вернёмся к тепловой карте. Представим, что два человека выбирают области на одной диаграмме. Алекс выделяет область с зажатой клавишей Alt (Option в macOS), а Морган — с зажатой Shift.

Цветом отмечаются элементы, попавшие в выбор Алекса или Моргана, но не в оба одновременно. Чтобы увидеть эффект, создайте оба выделения. Класс BrushConfig задаёт области Моргана другой стиль, позволяя различать их:

alex = alt.selection_interval(
    on="[pointerdown[event.altKey], pointerup] > pointermove",
    empty=False,
)
morgan = alt.selection_interval(
    on="[pointerdown[event.shiftKey], pointerup] > pointermove",
    mark=alt.BrushConfig(fill="#fdbb84", fillOpacity=0.5, stroke="#e34a33"),
    empty=False,
)

exlusive_or = (alex | morgan) & ~(alex & morgan)

alt.Chart(cars).mark_rect().encode(
    x='Cylinders:O',
    y='Origin:O',
    color=alt.when(exlusive_or).then("count()").otherwise(alt.value("grey")),
).add_params(
    alex, morgan
).properties(
    width=300,
    height=180
)

Эти операторы позволяют произвольно объединять выборы:

  • alex | morgan — прямоугольники внутри выбора Алекса или Моргана;
  • ~(alex & morgan) — прямоугольники, не входящие одновременно в оба выбора;
  • alex | ~morgan — прямоугольники внутри выбора Алекса или вне выбора Моргана.

Другие настройки выбора, включая события мыши и клавиатуры, описаны в документации выбора Vega-Lite. Настройки внешнего вида области выбора описаны в справочнике класса BrushConfig.