Настройка визуализаций

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

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

В зависимости от ситуации внешний вид диаграммы можно задать двумя или тремя способами. Рассмотрим диаграмму рассеяния по набору cars:

import altair as alt
from altair.datasets import data
cars = data.cars.url

alt.Chart(cars).mark_point().encode( 
    x='Acceleration:Q',
    y='Horsepower:Q'
)

Предположим, нужно сделать точки красными и задать непрозрачность 20%. Есть три подхода:

  1. Общие настройки действуют на всю диаграмму.
  2. Локальные настройки действуют на конкретный графический примитив.
  3. Каналы кодирования также позволяют задавать некоторые свойства.

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

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

Altair предоставляет методы configure_*. Здесь используется Chart.configure_mark:

alt.Chart(cars).mark_point().encode(
    x='Acceleration:Q',
    y='Horsepower:Q'
).configure_mark(
    opacity=0.2,
    color='red'
)

У общих настроек есть две особенности:

  1. Они распространяются на все примитивы диаграммы.
  2. Они допустимы только на верхнем уровне. Например, попытка поместить настроенную выше диаграмму в слой с другой диаграммой приведёт к ошибке.

Все возможности описаны в разделе конфигурации верхнего уровня.

Локальные настройки

Локальные настройки изменяют только указанный элемент диаграммы. Свойства примитивов удобнее всего передавать аргументами метода mark_*, в данном случае Chart.mark_point:

alt.Chart(cars).mark_point(opacity=0.2, color='red').encode(
    x='Acceleration:Q',
    y='Horsepower:Q'
)

Такую диаграмму можно использовать как слой или фасет составной диаграммы. Локальные настройки всегда имеют приоритет над общими.

Кодирование

Свойства можно задавать и через каналы кодирования. Вместо связи с полем данных функция value позволяет передать постоянное значение:

alt.Chart(cars).mark_point().encode(
    x='Acceleration:Q',
    y='Horsepower:Q',
    opacity=alt.value(0.2),
    color=alt.value('red')
)

С кодированиями связывается только ограниченный набор свойств. Для некоторых, например fillOpacity и strokeOpacity, этот подход недоступен.

Настройки кодирования имеют приоритет и над локальными, и над общими настройками.

Какой способ выбрать

Приоритет от низшего к высшему: общие настройки → локальные настройки → кодирование. Если свойство задано и глобально, и локально, действует локальное значение. Если оно задано настройкой и кодированием, действует кодирование.

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

Заголовок диаграммы

По умолчанию диаграммы Altair не имеют заголовка:

import altair as alt
from altair.datasets import data

iowa = data.iowa_electricity.url

alt.Chart(iowa).mark_area().encode(
    x="year:T",
    y=alt.Y("net_generation:Q").stack("normalize"),
    color="source:N"
)

Простой заголовок добавляется аргументом title вместе с данными:

alt.Chart(iowa, title="Iowa's green energy boom").mark_area().encode(
    x="year:T",
    y=alt.Y("net_generation:Q").stack("normalize"),
    color="source:N"
)

Объект alt.Title позволяет добавить подзаголовок:

alt.Chart(
   iowa,
   title=alt.Title(
       "Iowa's green energy boom",
       subtitle="A growing share of the state's energy has come from renewable sources"
   )
).mark_area().encode(
    x="year:T",
    y=alt.Y("net_generation:Q").stack("normalize"),
    color="source:N"
)

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

alt.Chart(
   iowa,
   title=alt.Title(
       "Iowa's green energy boom",
       subtitle=["A growing share of the state's energy", "has come from renewable sources"]
   )
).mark_area().encode(
    x="year:T",
    y=alt.Y("net_generation:Q").stack("normalize"),
    color="source:N"
)

Объект Title настраивает и другие свойства. Например, anchor='start' выравнивает заголовок по левому краю, а orient='bottom' размещает его снизу. Другие возможности описаны в конфигурации:

alt.Chart(
   iowa,
   title=alt.Title(
       "Iowa's green energy boom",
       subtitle="A growing share of the state's energy has come from renewable sources",
       anchor='start',
       orient='bottom',
       offset=20
   )
).mark_area().encode(
    x="year:T",
    y=alt.Y("net_generation:Q").stack("normalize"),
    color="source:N"
)

Здесь заголовок выровнен по самому левому краю, на уровне подписи оси y. Чтобы выровнять его относительно линии оси, задайте frame='group': тогда точка привязки определяется областью данных без подписей и заголовков:

alt.Chart(
   iowa,
   title=alt.Title(
       "Iowa's green energy boom",
       subtitle=["A growing share of the state's energy has come from", "renewable sources"],
       anchor='start',
       frame='group',
       orient='bottom',
       offset=20
   )
).mark_area().encode(
    x="year:T",
    y=alt.Y("net_generation:Q").stack("normalize"),
    color="source:N"
)

Границы осей

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

import altair as alt
from altair.datasets import data

cars = data.cars.url

alt.Chart(cars).mark_point().encode(
    x='Acceleration:Q',
    y='Horsepower:Q'
)

Altair наследует соглашение Vega-Lite включать ноль в количественные оси. Для отключения этого поведения задайте zero=False в scale канала X:

alt.Chart(cars).mark_point().encode(
    alt.X('Acceleration:Q').scale(zero=False),
    y='Horsepower:Q'
)

Точные границы задаются свойством шкалы domain:

alt.Chart(cars).mark_point().encode(
    alt.X('Acceleration:Q').scale(domain=(5, 20)),
    y='Horsepower:Q'
)

Однако за границами шкалы всё ещё есть данные. Один вариант — обрезать их отображение, задав примитиву clip=True:

alt.Chart(cars).mark_point(clip=True).encode(
    alt.X('Acceleration:Q').scale(domain=(5, 20)),
    y='Horsepower:Q'
)

Другой вариант — ограничить координаты: clamp перемещает точки за пределами диапазона к его краю:

alt.Chart(cars).mark_point().encode(
    alt.X('Acceleration:Q').scale(domain=(5, 20), clamp=True),
    y='Horsepower:Q'
).interactive()

В интерактивной диаграмме ограничение действует динамически. Это позволяет сохранять представление о выбросах при перемещении и масштабировании.

Подписи осей

Altair позволяет настраивать внешний вид подписей осей. Рассмотрим диаграмму:

import pandas as pd
df = pd.DataFrame(
    {'x': [0.03, 0.04, 0.05, 0.12, 0.07, 0.15],
    'y': [10, 35, 39, 50, 24, 35]
})

alt.Chart(df).mark_circle().encode(
    x='x',
    y='y'
)

Метод axis каналов X и Y принимает настройки подписей, а title задаёт заголовок. Здесь значения x форматируются как проценты, а значения y — как суммы в долларах:

alt.Chart(df).mark_circle().encode(
    alt.X('x').axis(format='%').title('percentage'),
    alt.Y('y').axis(format='$').title('dollar amount')
)

Подписи можно убрать:

alt.Chart(df).mark_circle().encode(
    alt.X('x').axis(labels=False),
    alt.Y('y').axis(labels=False)
)

Заголовок оси можно также повернуть:

alt.Chart(df).mark_circle().encode(
    alt.X('x').axis(title="x"),
    alt.Y('y').axis(
        title="Y Axis Title",
        titleAngle=0,
        titleAlign="left",
        titleY=-2,
        titleX=0,
    )
)

Доступны и другие коды форматирования; их перечень приведён в документации d3-format.

Легенда

При передаче color, shape или size методу encode легенда добавляется автоматически. Здесь используется color:

import altair as alt
from altair.datasets import data

cars = data.cars()

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

Для настройки легенды используйте класс Color и его метод legend. У каналов shape и size есть соответствующие классы.

Настройки легенды соответствуют свойствам объекта Legend: он управляет многими аспектами её оформления. Например, аргумент orient меняет положение:

import altair as alt
from altair.datasets import data

cars = data.cars()

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

Заголовок можно задать коротким методом title или аргументом title внутри legend:

import altair as alt
from altair.datasets import data

cars = data.cars()

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

Передав пустое значение None, можно полностью убрать легенду:

import altair as alt
from altair.datasets import data

cars = data.cars()

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

Удаление рамки диаграммы

Обычная диаграмма Altair имеет сетку и внешнюю рамку. Чтобы убрать границы, нужно отключить обе. Начнём с диаграммы рассеяния:

import altair as alt
from altair.datasets import data

cars = data.cars()

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

Сначала уберём сетку методом configure_axis:

import altair as alt
from altair.datasets import data

cars = data.cars()

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

Внутренние линии исчезли, но внешняя рамка осталась. Уберём её, задав stroke=None в configure_view. Также подходят strokeWidth=0 и strokeOpacity=0:

import altair as alt
from altair.datasets import data

cars = data.cars()

alt.Chart(cars).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color='Origin:N'
).configure_axis(
    grid=False
).configure_view(
    stroke=None
)

Для полного удаления границ и осей совместите эти настройки с axis=None в кодированиях:

import altair as alt
from altair.datasets import data

cars = data.cars()

alt.Chart(cars).mark_point().encode(
    alt.X('Horsepower:Q').axis(None),
    alt.Y('Miles_per_Gallon:Q').axis(None),
    color='Origin:N'
).configure_axis(
    grid=False
).configure_view(
    stroke=None
)

Цвета

Как описано в разделе типов данных, легенд и шкал, Altair выбирает цветовую схему по типу данных канала цвета. Настройки изменяются методом scale класса Color.

Цветовые схемы

Altair содержит именованные цветовые схемы Vega для категориальных и последовательных данных. Полная галерея приведена в документации Vega. Имя схемы передаётся аргументу scheme метода scale:

import altair as alt
from altair.datasets import data

cars = data.cars()

alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color=alt.Color('Acceleration').scale(scheme="lightgreyred")
)

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

alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color=alt.Color('Acceleration').scale(scheme="lightgreyred", reverse=True)
)

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

Собственная цветовая шкала задаётся аргументами domain и range метода scale: первый содержит значения данных, второй — цвета. Это работает с непрерывными шкалами, позволяя выделять конкретные диапазоны:

domain = [5, 8, 10, 12, 25]
range_ = ['#9cc8e2', '#9cc8e2', 'red', '#5ba3cf', '#125ca4']

alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color=alt.Color('Acceleration').scale(domain=domain, range=range_)
)

И с дискретными шкалами:

domain = ['Europe', "Japan", "USA"]
range_ = ['seagreen', 'firebrick', 'rebeccapurple']

alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color=alt.Color('Origin').scale(domain=domain, range=range_)
)

Непосредственные значения цветов

Шкала переводит исходные значения в цвета. Если данные уже содержат названия или коды цветов, используйте scale(None) для их непосредственного отображения:

import pandas as pd
import altair as alt

data = pd.DataFrame({
    'x': range(6),
    'color': ['red', 'steelblue', 'chartreuse', '#F4D03F', '#D35400', '#7D3C98']
})

alt.Chart(data).mark_point(
    filled=True,
    size=100
).encode(
    x='x',
    color=alt.Color('color').scale(None)
)

Ширина столбцов

Ширина столбцов задаётся свойством size метода mark_bar:

import altair as alt
import pandas as pd

data = pd.DataFrame({'name': ['a', 'b'], 'value': [4, 10]})

alt.Chart(data).mark_bar(size=10).encode(
    x='name:O',
    y='value:Q'
)

Однако mark_bar(size=10) изменяет только ширину самих столбцов. Ширина диаграммы может не подстроиться под них:

alt.Chart(data).mark_bar(size=30).encode(
    x='name:O',
    y='value:Q'
)

Поэтому часто удобнее задавать ширину всей диаграммы относительно числа категорий через Step. Пример приведён ниже.

Размер диаграммы

Размер задаётся свойствами width и height:

import altair as alt
from altair.datasets import data

cars = data.cars()

alt.Chart(cars).mark_bar().encode(
    x='Origin',
    y='count()'
).properties(
    width=200,
    height=150
)

Для фасетных и других составных диаграмм эти размеры относятся к отдельной поддиаграмме, а не ко всей композиции:

alt.Chart(cars).mark_bar().encode(
    x='Origin',
    y='count()',
    column='Cylinders:Q'
).properties(
    width=100,
    height=100
).resolve_scale(
    x='independent'
)

Чтобы размер зависел от числа категорий, задайте через Step ширину или высоту одной категории:

alt.Chart(cars).mark_bar().encode(
    x='Origin',
    y='count()',
    column='Cylinders:Q'
).properties(
    width=alt.Step(35),
    height=100
).resolve_scale(
    x='independent'
)

Чтобы диаграмма реагировала на размер HTML-страницы или контейнера, задайте width или height строкой "container":

alt.Chart(cars).mark_bar().encode(
    x='Origin',
    y='count()',
).properties(
    width='container',
    height=200
)

Размер родительского элемента должен определяться независимо от диаграммы. Например, у <div> может быть стиль width: 100%; height: 300px.

Темы диаграмм

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

Реестр тем Altair позволяет применять настройки диаграмм глобально в сеансе Python. Модуль altair.theme предоставляет вспомогательные функции для работы с реестром.

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

>>> import altair as alt
>>> default = alt.theme.get()
>>> default()
{'config': {'view': {'continuousWidth': 300, 'continuousHeight': 300}}}

Настройки темы добавляются в спецификацию каждой созданной диаграммы:

import altair as alt
from altair.datasets import data

chart = alt.Chart(data.cars.url).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q'
)

chart.to_dict()

Они отражаются и в отображаемой диаграмме:

chart

Изменение темы

Функция altair.theme.enable включает другую тему на весь сеанс Python. Например, Altair содержит тему с непрозрачным фоном:

alt.theme.enable('opaque')
chart.to_dict()
chart

Фон стал белым. Тема 'none' отключает тему:

alt.theme.enable('none')
chart.to_dict()
chart

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

Чтобы применить тему только к одной диаграмме, временно включите её конструкцией with:

with alt.theme.enable('default'):
    spec = chart.to_json()

Преобразование или сохранение диаграммы должно выполняться внутри блока with: например, через to_dict, to_json или save. См. issue #3586.

Встроенные темы

Сейчас Altair предлагает немного собственных встроенных тем; в будущем планируется добавить больше вариантов.

Познакомиться с темами, унаследованными от Vega Themes, можно в демонстрации ниже:

Показать демонстрацию тем Vega-Altair

Собственная тема

Тема — функция, возвращающая словарь настроек по умолчанию, который добавляется к спецификации при отображении.

Функция altair.theme.register позволяет зарегистрировать и включить тему непосредственно при её определении. Например, создадим тему с чёрной заливкой всех примитивов, если не задана другая:

import altair as alt
from altair.datasets import data

# define, register and enable theme

@alt.theme.register("black_marks", enable=True)
def black_marks() -> alt.theme.ThemeConfig:
    return {
        "config": {
            "view": {"continuousWidth": 300, "continuousHeight": 300},
            "mark": {"color": "black", "fill": "black"},
        }
    }


# draw the chart
cars = data.cars.url
alt.Chart(cars).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q'
)

Для возвращения стандартной темы выполните:

alt.themes.enable('default')

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

Показать код демонстрации тем Vega-Altair
from __future__ import annotations

def alt_theme_test() -> ChartType:
    import altair as alt
    from altair.datasets import data

    us_10m = data.us_10m.url
    unemployment = data.unemployment.url
    movies = data.movies.url
    barley = data.barley.url
    iowa_electricity = data.iowa_electricity.url
    common_data = alt.InlineData(
        [
            {"Index": 1, "Value": 28, "Position": 1, "Category": "A"},
            {"Index": 2, "Value": 55, "Position": 2, "Category": "A"},
            {"Index": 3, "Value": 43, "Position": 3, "Category": "A"},
            {"Index": 4, "Value": 91, "Position": 4, "Category": "A"},
            {"Index": 5, "Value": 81, "Position": 5, "Category": "A"},
            {"Index": 6, "Value": 53, "Position": 6, "Category": "A"},
            {"Index": 7, "Value": 19, "Position": 1, "Category": "B"},
            {"Index": 8, "Value": 87, "Position": 2, "Category": "B"},
            {"Index": 9, "Value": 52, "Position": 3, "Category": "B"},
            {"Index": 10, "Value": 48, "Position": 4, "Category": "B"},
            {"Index": 11, "Value": 24, "Position": 5, "Category": "B"},
            {"Index": 12, "Value": 49, "Position": 6, "Category": "B"},
            {"Index": 13, "Value": 87, "Position": 1, "Category": "C"},
            {"Index": 14, "Value": 66, "Position": 2, "Category": "C"},
            {"Index": 15, "Value": 17, "Position": 3, "Category": "C"},
            {"Index": 16, "Value": 27, "Position": 4, "Category": "C"},
            {"Index": 17, "Value": 68, "Position": 5, "Category": "C"},
            {"Index": 18, "Value": 16, "Position": 6, "Category": "C"},
        ]
    )

    HEIGHT_SMALL = 140
    STANDARD = 180
    WIDTH_GEO = int(STANDARD * 1.667)

    bar = (
        alt.Chart(common_data, height=HEIGHT_SMALL, width=STANDARD, title="Bar")
        .mark_bar()
        .encode(
            x=alt.X("Index:O").axis(offset=1), y=alt.Y("Value:Q"), tooltip="Value:Q"
        )
        .transform_filter(alt.datum["Index"] <= 9)
    )
    line = (
        alt.Chart(common_data, height=HEIGHT_SMALL, width=STANDARD, title="Line")
        .mark_line()
        .encode(
            x=alt.X("Position:O").axis(grid=False),
            y=alt.Y("Value:Q").axis(grid=False),
            color=alt.Color("Category:N").legend(None),
            tooltip=["Index:O", "Value:Q", "Position:O", "Category:N"],
        )
    )
    point_shape = (
        alt.Chart(
            common_data, height=HEIGHT_SMALL, width=STANDARD, title="Point (Shape)"
        )
        .mark_point()
        .encode(
            x=alt.X("Position:O").axis(grid=False),
            y=alt.Y("Value:Q").axis(grid=False),
            shape=alt.Shape("Category:N").legend(None),
            color=alt.Color("Category:N").legend(None),
            tooltip=["Index:O", "Value:Q", "Position:O", "Category:N"],
        )
    )
    point = (
        alt.Chart(movies, height=STANDARD, width=STANDARD, title="Point")
        .mark_point(tooltip=True)
        .transform_filter(alt.datum["IMDB Rating"] != None)
        .transform_filter(
            alt.FieldRangePredicate("Release Date", [None, alt.DateTime(year=2019)])
        )
        .transform_joinaggregate(Average_Rating="mean(IMDB Rating)")
        .transform_calculate(
            Rating_Delta=alt.datum["IMDB Rating"] - alt.datum.Average_Rating
        )
        .encode(
            x=alt.X("Release Date:T").title("Release Date"),
            y=alt.Y("Rating_Delta:Q").title("Rating Delta"),
            color=alt.Color("Rating_Delta:Q").title("Rating Delta").scale(domainMid=0),
        )
    )
    bar_stack = (
        alt.Chart(barley, height=STANDARD, width=STANDARD, title="Bar (Stacked)")
        .mark_bar(tooltip=True)
        .encode(
            x="sum(yield):Q",
            y=alt.Y("variety:N"),
            color=alt.Color("site:N").legend(orient="bottom", columns=2),
        )
    )
    area = (
        alt.Chart(iowa_electricity, height=STANDARD, width=STANDARD, title="Area")
        .mark_area(tooltip=True)
        .encode(
            x=alt.X("year:T").title("Year"),
            y=alt.Y("net_generation:Q")
            .title("Share of net generation")
            .stack("normalize")
            .axis(format=".0%"),
            color=alt.Color("source:N")
            .title("Electricity source")
            .legend(orient="bottom", columns=2),
        )
    )
    geoshape = (
        alt.Chart(
            alt.topo_feature(us_10m, "counties"),
            height=STANDARD,
            width=WIDTH_GEO,
            title=alt.Title("Geoshape", subtitle="Unemployment rate per county"),
        )
        .mark_geoshape(tooltip=True)
        .encode(color="rate:Q")
        .transform_lookup(
            "id", alt.LookupData(alt.UrlData(unemployment), "id", ["rate"])
        )
        .project(type="albersUsa")
    )

    compound_chart = (
        (bar | line | point_shape) & (point | bar_stack) & (area | geoshape)
    ).properties(
        title=alt.Title(
            "Vega-Altair Theme Test",
            fontSize=20,
            subtitle="Adapted from https://vega.github.io/vega-themes/",
        )
    )
    return compound_chart

Другие идеи представлены в репозитории Vega Themes.

Локализация

Форматы чисел, дат и валют зависят от языка и региональных соглашений. Vega-Altair использует поддержку локализации D3. Настройки задаются глобальной функцией alt.renderers.set_embed_options.

Аргументы format_locale и time_format_locale принимают словари форматов D3 или имена готовых локалей. Здесь для валют и дат используется итальянская локаль it-IT:

import altair as alt
from altair.datasets import data

alt.renderers.set_embed_options(format_locale="it-IT", time_format_locale="it-IT")

source = data.stocks.url
chart = alt.Chart(source).mark_area().transform_filter('year(datum.date) == 2009').encode(
    x='date:T',
    y=alt.Y('price:Q', axis=alt.Axis(format="$.0f")),
    color='symbol:N'
)
chart

Диаграмма с областями для цен акций, использующая итальянскую локаль

Списки доступных локалей числовых форматов и форматов дат представлены в пакетах d3-format и d3-time-format соответственно.

Настройки локализации сохраняются при сохранении диаграммы.

Глобальные свойства format_locale и time_format_locale действуют на весь сеанс, а не на отдельные диаграммы. Для возврата к стандартной американской английской локали используйте:

alt.renderers.set_embed_options(format_locale="en-US", time_format_locale="en-US")