Leo Matyushkin 12 января 2021

📊 Туториал: визуализация данных в вебе с помощью Python и Dash

В этом руководстве мы рассмотрим, как с помощью Python и библиотеки Dash создать, оформить и опубликовать на хостинге интерактивное веб-приложение с результатами анализа данных.

Данная публикация является незначительно сокращенным переводом статьи Дилана Кастильо Develop Data Visualization Interfaces in Python With Dash.

***

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

В этом руководстве мы рассмотрим:

  • Как создать приложение Dash
  • Основные компоненты библиотеки
  • Как настроить стиль приложения
  • Как сделать приложение интерактивным
  • Как развернуть приложение на удаленном сервере (на примере Heroku)

Что такое Dash?

Dash ― это платформа с открытым исходным кодом для создания интерфейсов визуализации данных. После выпуска в 2017 году в виде библиотеки Python Dash вскоре был расширен для R и Julia.

Библиотека создана и поддерживается канадской компанией Plotly. Возможно, вы знаете о ней по популярным графическим библиотекам, носящим ее название. Plotly открыла исходный код Dash и выпустила его по лицензии MIT, так что библиотеку можно использовать бесплатно.

В основе Dash лежат три технологии:

  1. Flask предоставляет функциональность веб-сервера.
  2. React отображает веб-интерфейс.
  3. Plotly.js генерирует диаграммы.

Не нужно беспокоиться о совместной работе этих технологий. Необходимо лишь написать код на Python, R или Julia и добавить немного CSS.

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

Другие интересные варианты использования вы найдете в галерее приложений Dash.

Начинаем работу с Dash на Python

В качестве примера в этом руководстве мы шаг за шагом создадим информационную панель для набора данных Kaggle о продажах и ценах на авокадо в США за период с 2015 по 2018 год.

Настройка виртуального окружения

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

Windows. Откройте командную строку и выполните следующие команды:

        mkdir avocado_analytics && cd avocado_analytics
python -m venv venv
venv\Scripts\activate.bat
    

Первая команда создаст каталог проекта и поменяет текущую рабочую директорию. Вторая команда создаст виртуальную среду, а последняя команда её активирует. Вместо команды python может потребоваться указать путь к файлу python.exe.

macOS или Linux. Смысл следующих команд терминала идентичен командам для Windows:

        mkdir avocado_analytics && cd avocado_analytics
python3 -m venv venv
source venv/bin/activate
    

Далее необходимо установить в виртуальное окружение следующие библиотеки:

        python -m pip install dash==1.13.3 pandas==1.0.5
    

В виртуальную среду будут установлены библиотеки Dash и pandas. Виртуальное окружение позволяет использовать определенные версии библиотек, аналогичные тем, что использовались в этом руководстве.

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

Сохраните файл с данными avocado.csv в корневом каталоге проекта. К настоящему моменту у вас должна быть виртуальная среда с необходимыми библиотеками и данными в корневой папке проекта. Структура проекта выглядит так:

        avocado_analytics/
├── venv/
└── avocado.csv
    

Как с помощью Dash создать приложение

Разобьем процесс создания приложения Dash на два этапа:

  1. Инициализируем приложение и определим внешний вид с помощью макета приложения (layout).
  2. Определим посредством обратных вызовов (callbacks), какие части приложения являются интерактивными и на что они реагируют.

Инициализируем Dash-приложение

Создадим пустой файл app.py. Далее мы будем шаг за шагом его заполнять и пояснять происходящее, а в конце раздела вы найдете его содержимое целиком.

Вот несколько первых строк app.py:

        import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd

data = pd.read_csv("avocado.csv")
data = data.query("type == 'conventional' and region == 'Albany'")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)

app = dash.Dash(__name__)
    

Вначале мы импортируем необходимые библиотеки:

  • dash поможет инициализировать приложение
  • dash_core_components позволяет создавать интерактивные компоненты: графики, раскрывающиеся списки, диапазоны дат и т. д.
  • dash_html_components позволяет получить доступ к тегам HTML
  • pandas помогает читать и выводить данные в организованной форме

Далее мы считываем данные и обрабатываем их для использования в панели управления. В последней строке мы создаем экземпляр класса Dash.

Если вы уже использовали Flask, то инициализация класса Dash вам уже знакома. Во Flask мы обычно инициализируем WSGI-приложение с помощью Flask(__name__). Для приложений Dash мы используем Dash(__name__).

Определение макета приложения Dash

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

        app.layout = html.Div(
    children=[
        html.H1(children="Avocado Analytics",),
        html.P(
            children="Analyze the behavior of avocado prices"
            " and the number of avocados sold in the US"
            " between 2015 and 2018",
        ),
        dcc.Graph(
            figure={
                "data": [
                    {
                        "x": data["Date"],
                        "y": data["AveragePrice"],
                        "type": "lines",
                    },
                ],
                "layout": {"title": "Average Price of Avocados"},
            },
        ),
        dcc.Graph(
            figure={
                "data": [
                    {
                        "x": data["Date"],
                        "y": data["Total Volume"],
                        "type": "lines",
                    },
                ],
                "layout": {"title": "Avocados Sold"},
            },
        ),
    ]
)
    

Этот код определяет свойство layout объекта app. Внешний вид приложения описывается с помощью древовидной структуры, состоящей из Dash-компонентов.

Мы начинаем с определения родительского компонента html.Div, затем в качестве дочерних элементов добавляем заголовок html.H1 и абзац html.P. Эти компоненты эквивалентны HTML-тегам div, h1 и p. Для изменения атрибутов или содержимого тегов используются аргументы компонентов. Например, чтобы указать, что находится внутри тега div, мы используем в html.Div аргумент children.

В компонентах есть и другие аргументы, такие как style, className или id, которые относятся к атрибутам HTML-тегов. В следующем разделе мы увидим, как использовать эти свойства для стилизации панели инструментов.

Таким образом, Python-код будет преобразован в следующий HTML-код:

        <div>
  <h1>Avocado Analytics</h1>
  <p>
    Analyze the behavior of avocado prices and the number
    of avocados sold in the US between 2015 and 2018
  </p>
  <!-- Остальная часть приложения -->
</div>
    

Далее описаны два компонента dcc.Graph. Первая диаграмма отображает средние цены на авокадо за период исследования, а вторая ― количество авокадо, проданных в США за тот же период.

Под капотом Dash использует для создания графиков Plotly.js. Компоненты dcc.Graph ожидают figure object или словарь Python, содержащий данные графика и layout, что мы и передаем в нашем случае.

Остались две строки кода, которые помогут запустить приложение:

        if __name__ == "__main__":
    app.run_server(debug=True,
                   host = '127.0.0.1')
    

Эти строки позволяют запускать приложение Dash локально, используя встроенный сервер Flask. Параметр debug = True из app.run_server разрешает горячую перезагрузку: когда мы вносим изменения в приложение, оно автоматически перезагружается без перезапуска сервера.

Наконец, полная версия app.py. Вы можете скопировать код в пустой app.py и проверить результат.

app.py
        import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd

data = pd.read_csv("avocado.csv")
data = data.query("type == 'conventional' and region == 'Albany'")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)

app = dash.Dash(__name__)

app.layout = html.Div(
    children=[
        html.H1(children="Avocado Analytics",),
        html.P(
            children="Analyze the behavior of avocado prices"
            " and the number of avocados sold in the US"
            " between 2015 and 2018",
        ),
        dcc.Graph(
            figure={
                "data": [
                    {
                        "x": data["Date"],
                        "y": data["AveragePrice"],
                        "type": "lines",
                    },
                ],
                "layout": {"title": "Average Price of Avocados"},
            },
        ),
        dcc.Graph(
            figure={
                "data": [
                    {
                        "x": data["Date"],
                        "y": data["Total Volume"],
                        "type": "lines",
                    },
                ],
                "layout": {"title": "Avocados Sold"},
            },
        ),
    ]
)

if __name__ == "__main__":
    app.run_server(debug=True,
                   host = '127.0.0.1')
    

Пришло время запустить приложение. Откройте терминал в корневом каталоге проекта и в виртуальной среде проекта. Запустите python app.py, затем перейдите по адресу http://localhost:8050.

Панель управления должна выглядеть примерно так:

Теперь у нас есть рабочая версия, но мы ее еще улучшим.

Управление оформлением панели

Dash очень гибок в настройке внешнего вида приложения. Мы можем использовать собственные файлы CSS или JavaScript, встраивать изображения и настраивать дополнительные параметры.

Как применить стиль к компонентам Dash

Стилизовать компоненты можно двумя способами:

  • Использовать аргумент style отдельных компонентов.
  • Предоставить внешний CSS-файл.

Аргумент style принимает словарь Python с парами ключ-значение, состоящими из имен свойств CSS и значений, которые мы хотим установить.

Примечание
При указании свойств CSS в аргументе style необходимо использовать синтаксис вида mixedCase вместо слов, разделенных дефисом. Например, чтобы изменить цвет фона элемента, необходимо указывать backgroundColor, а не background-color.

Захотев изменить размер и цвет элемента H1 в app.py, мы можем установить аргумент style следующим образом:

        html.H1(
    children="Avocado Analytics",
    style={"fontSize": "48px", "color": "red"},
),
    

В этом случае заголовок будет оформлен красным шрифтом размером в 48 пикселей.

Обратная сторона простоты использования style ― такой код будет всё труднее поддерживать по мере роста кодовой базы. Если на панели управления присутствует несколько одинаковых компонентов, большая часть кода будет повторяться. Вместо этого можно использовать CSS-файл.

Если вы хотите включить собственные локальные CSS- или JavaScript-файлы, необходимо создать в корневом каталоге проекта папку с именем assets/ и сохранить в ней необходимые файлы.

Затем вы можете использовать аргументы className или id компонентов, чтобы настроить с помощью CSS их стили. При преобразовании в HTML-теги эти аргументы соответствуют атрибутам class и id.

Захотев настроить размер шрифта и цвет текста элемента H1 в app.py, мы можем использовать аргумент className:

        html.H1(
    children="Avocado Analytics",
    className="header-title",
),

    

Установка аргумента className определяет атрибут класса для элемента H1. Затем в CSS-файле style.css в папке assets/ мы указываем, как хотим, чтобы он выглядел:

        .header-title {
  font-size: 48px;
  color: red;
}
    

Как улучшить внешний вид панели инструментов

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

  • Добавим иконку сайта (favicon) и title.
  • Изменим семейство шрифтов.
  • Используем внешний CSS-файл для стилизации компонентов Dash.

Добавление в приложение внешних ресурсов

Создадим папку assets/ в корневом каталоге проекта. Сохраним в ней значок favicon.ico и файл style.css.

К настоящему моменту структура проекта должна выглядеть так:

        avocado_analytics/
├── assets/
│   ├── favicon.ico
│   └── style.css
├── venv/
├── app.py
└── avocado.csv
    

app.py требует нескольких изменений. Необходимо включить внешнюю таблицу стилей, добавить заголовок на панель инструментов и стилизовать компоненты с помощью файла style.css:

        external_stylesheets = [
    {
        "href": "https://fonts.googleapis.com/css2?"
                "family=Lato:wght@400;700&display=swap",
        "rel": "stylesheet",
    },
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.title = "Avocado Analytics: Understand Your Avocados!"
    

Здесь мы указываем CSS-файл и семейство шрифтов, которое хотим загрузить в приложение. Внешние файлы загружаются до загрузки тела приложения. Аргумент external_stylesheets используется для добавления внешних CSS-файлов, а external_scripts ― для внешних файлов JavaScript, таких как скрипт Google Analytics.

Настройка стилей компонентов

В приведенном ниже коде мы добавляем className с соответствующим селектором классов к каждому из компонентов, представляющих заголовок информационной панели:

        app.layout = html.Div(
    children=[
        html.Div(
            children=[
                html.P(children="🥑", className="header-emoji"),
                html.H1(
                    children="Avocado Analytics", className="header-title"
                ),
                html.P(
                    children="Analyze the behavior of avocado prices"
                    " and the number of avocados sold in the US"
                    " between 2015 and 2018",
                    className="header-description",
                ),
            ],
            className="header",
        ),
    

Класс header-description, назначенный компоненту абзаца, имеет соответствующий селектор в style.css:

        .header-description {
    color: #CFCFCF;
    margin: 4px auto;
    text-align: center;
    max-width: 384px;
}
    

Другое существенное изменение ― графики. Новый код для графика цены:

        html.Div(
    children=[
        html.Div(
            children=dcc.Graph(
                id="price-chart",
                config={"displayModeBar": False},
                figure={
                    "data": [
                        {
                            "x": data["Date"],
                            "y": data["AveragePrice"],
                            "type": "lines",
                            "hovertemplate": "$%{y:.2f}"
                                                "<extra></extra>",
                        },
                    ],
                    "layout": {
                        "title": {
                            "text": "Average Price of Avocados",
                            "x": 0.05,
                            "xanchor": "left",
                        },
                        "xaxis": {"fixedrange": True},
                        "yaxis": {
                            "tickprefix": "$",
                            "fixedrange": True,
                        },
                        "colorway": ["#17B897"],
                    },
                },
            ),
            className="card",
        ),
    

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

Так же мы настроили ось, цвет рисунка, формат заголовка в разделе макета графика. Еще мы обернули график в html.Div с классом card. Это придаст графику белый фон и добавит небольшую тень под ним. Аналогичные изменения внесены в графики продаж и объемов. Вот полный код обновленного app.py:

app.py
        import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd

data = pd.read_csv("avocado.csv")
data = data.query("type == 'conventional' and region == 'Albany'")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)

external_stylesheets = [
    {
        "href": "https://fonts.googleapis.com/css2?"
        "family=Lato:wght@400;700&display=swap",
        "rel": "stylesheet",
    },
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.title = "Avocado Analytics: Understand Your Avocados!"

app.layout = html.Div(
    children=[
        html.Div(
            children=[
                html.P(children="🥑", className="header-emoji"),
                html.H1(
                    children="Avocado Analytics", className="header-title"
                ),
                html.P(
                    children="Analyze the behavior of avocado prices"
                    " and the number of avocados sold in the US"
                    " between 2015 and 2018",
                    className="header-description",
                ),
            ],
            className="header",
        ),
        html.Div(
            children=[
                html.Div(
                    children=dcc.Graph(
                        id="price-chart",
                        config={"displayModeBar": False},
                        figure={
                            "data": [
                                {
                                    "x": data["Date"],
                                    "y": data["AveragePrice"],
                                    "type": "lines",
                                    "hovertemplate": "$%{y:.2f}"
                                                     "<extra></extra>",
                                },
                            ],
                            "layout": {
                                "title": {
                                    "text": "Average Price of Avocados",
                                    "x": 0.05,
                                    "xanchor": "left",
                                },
                                "xaxis": {"fixedrange": True},
                                "yaxis": {
                                    "tickprefix": "$",
                                    "fixedrange": True,
                                },
                                "colorway": ["#17B897"],
                            },
                        },
                    ),
                    className="card",
                ),
                html.Div(
                    children=dcc.Graph(
                        id="volume-chart",
                        config={"displayModeBar": False},
                        figure={
                            "data": [
                                {
                                    "x": data["Date"],
                                    "y": data["Total Volume"],
                                    "type": "lines",
                                },
                            ],
                            "layout": {
                                "title": {
                                    "text": "Avocados Sold",
                                    "x": 0.05,
                                    "xanchor": "left",
                                },
                                "xaxis": {"fixedrange": True},
                                "yaxis": {"fixedrange": True},
                                "colorway": ["#E12D39"],
                            },
                        },
                    ),
                    className="card",
                ),
            ],
            className="wrapper",
        ),
    ]
)

if __name__ == "__main__":
    app.run_server(debug=True)

    

Панель обновленной версии app.py выглядит так:

Добавляем в Dash-приложение интерактивные элементы

Интерактивность Dash базируется на парадигме реактивного программирования. Это означает, что мы можем связывать компоненты и элементы приложения, которые хотим обновить. Если пользователь взаимодействует с компонентом ввода, например, с раскрывающимся списком или ползунком, то объект вывода данных, например, график, будет автоматически реагировать на изменения ввода.

Давайте сделаем панель управления интерактивной. Новая версия позволит пользователю взаимодействовать со следующими фильтрами:

  • Регион производства.
  • Тип авокадо.
  • Диапазон дат.

Начнем с замены локального app.py на новую версию.

        import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import numpy as np
from dash.dependencies import Output, Input

data = pd.read_csv("avocado.csv")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)

external_stylesheets = [
    {
        "href": "https://fonts.googleapis.com/css2?"
        "family=Lato:wght@400;700&display=swap",
        "rel": "stylesheet",
    },
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.title = "Avocado Analytics: Understand Your Avocados!"

app.layout = html.Div(
    children=[
        html.Div(
            children=[
                html.P(children="🥑", className="header-emoji"),
                html.H1(
                    children="Avocado Analytics", className="header-title"
                ),
                html.P(
                    children="Analyze the behavior of avocado prices"
                    " and the number of avocados sold in the US"
                    " between 2015 and 2018",
                    className="header-description",
                ),
            ],
            className="header",
        ),
        html.Div(
            children=[
                html.Div(
                    children=[
                        html.Div(children="Region", className="menu-title"),
                        dcc.Dropdown(
                            id="region-filter",
                            options=[
                                {"label": region, "value": region}
                                for region in np.sort(data.region.unique())
                            ],
                            value="Albany",
                            clearable=False,
                            className="dropdown",
                        ),
                    ]
                ),
                html.Div(
                    children=[
                        html.Div(children="Type", className="menu-title"),
                        dcc.Dropdown(
                            id="type-filter",
                            options=[
                                {"label": avocado_type, "value": avocado_type}
                                for avocado_type in data.type.unique()
                            ],
                            value="organic",
                            clearable=False,
                            searchable=False,
                            className="dropdown",
                        ),
                    ],
                ),
                html.Div(
                    children=[
                        html.Div(
                            children="Date Range",
                            className="menu-title"
                            ),
                        dcc.DatePickerRange(
                            id="date-range",
                            min_date_allowed=data.Date.min().date(),
                            max_date_allowed=data.Date.max().date(),
                            start_date=data.Date.min().date(),
                            end_date=data.Date.max().date(),
                        ),
                    ]
                ),
            ],
            className="menu",
        ),
        html.Div(
            children=[
                html.Div(
                    children=dcc.Graph(
                        id="price-chart", config={"displayModeBar": False},
                    ),
                    className="card",
                ),
                html.Div(
                    children=dcc.Graph(
                        id="volume-chart", config={"displayModeBar": False},
                    ),
                    className="card",
                ),
            ],
            className="wrapper",
        ),
    ]
)


@app.callback(
    [Output("price-chart", "figure"), Output("volume-chart", "figure")],
    [
        Input("region-filter", "value"),
        Input("type-filter", "value"),
        Input("date-range", "start_date"),
        Input("date-range", "end_date"),
    ],
)
def update_charts(region, avocado_type, start_date, end_date):
    mask = (
        (data.region == region)
        & (data.type == avocado_type)
        & (data.Date >= start_date)
        & (data.Date <= end_date)
    )
    filtered_data = data.loc[mask, :]
    price_chart_figure = {
        "data": [
            {
                "x": filtered_data["Date"],
                "y": filtered_data["AveragePrice"],
                "type": "lines",
                "hovertemplate": "$%{y:.2f}<extra></extra>",
            },
        ],
        "layout": {
            "title": {
                "text": "Average Price of Avocados",
                "x": 0.05,
                "xanchor": "left",
            },
            "xaxis": {"fixedrange": True},
            "yaxis": {"tickprefix": "$", "fixedrange": True},
            "colorway": ["#17B897"],
        },
    }

    volume_chart_figure = {
        "data": [
            {
                "x": filtered_data["Date"],
                "y": filtered_data["Total Volume"],
                "type": "lines",
            },
        ],
        "layout": {
            "title": {"text": "Avocados Sold", "x": 0.05, "xanchor": "left"},
            "xaxis": {"fixedrange": True},
            "yaxis": {"fixedrange": True},
            "colorway": ["#E12D39"],
        },
    }
    return price_chart_figure, volume_chart_figure


if __name__ == "__main__":
    app.run_server(debug=True,
                   host='127.0.0.1')
    

Затем необходимо обновить style.css следующим кодом.

        body {
    font-family: "Lato", sans-serif;
    margin: 0;
    background-color: #F7F7F7;
}

.header {
    background-color: #222222;
    height: 288px;
    padding: 16px 0 0 0;
}

.header-emoji {
    font-size: 48px;
    margin: 0 auto;
    text-align: center;
}

.header-title {
    color: #FFFFFF;
    font-size: 48px;
    font-weight: bold;
    text-align: center;
    margin: 0 auto;
}

.header-description {
    color: #CFCFCF;
    margin: 4px auto;
    text-align: center;
    max-width: 384px;
}

.wrapper {
    margin-right: auto;
    margin-left: auto;
    max-width: 1024px;
    padding-right: 10px;
    padding-left: 10px;
    margin-top: 32px;
}

.card {
    margin-bottom: 24px;
    box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}

.menu {
    height: 112px;
    width: 912px;
    display: flex;
    justify-content: space-evenly;
    padding-top: 24px;
    margin: -80px auto 0 auto;
    background-color: #FFFFFF;
    box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}

.Select-control {
    width: 256px;
    height: 48px;
}

.Select--single > .Select-control .Select-value, .Select-placeholder {
    line-height: 48px;
}

.Select--multi .Select-value-label {
    line-height: 32px;
}

.menu-title {
    margin-bottom: 6px;
    font-weight: bold;
    color: #079A82;
}
    

Как создавать интерактивные компоненты

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

Вот как это выглядит в app.py:

        html.Div(
    children=[
        html.Div(
            children=[
                html.Div(children="Region", className="menu-title"),
                dcc.Dropdown(
                    id="region-filter",
                    options=[
                        {"label": region, "value": region}
                        for region in np.sort(data.region.unique())
                    ],
                    value="Albany",
                    clearable=False,
                    className="dropdown",
                ),
            ]
        ),
        html.Div(
            children=[
                html.Div(children="Type", className="menu-title"),
                dcc.Dropdown(
                    id="type-filter",
                    options=[
                        {"label": avocado_type, "value": avocado_type}
                        for avocado_type in data.type.unique()
                    ],
                    value="organic",
                    clearable=False,
                    searchable=False,
                    className="dropdown",
                ),
            ],
        ),
        html.Div(
            children=[
                html.Div(
                    children="Date Range",
                    className="menu-title"
                    ),
                dcc.DatePickerRange(
                    id="date-range",
                    min_date_allowed=data.Date.min().date(),
                    max_date_allowed=data.Date.max().date(),
                    start_date=data.Date.min().date(),
                    end_date=data.Date.max().date(),
                ),
            ]
        ),
    ],
    className="menu",
),
    

Раскрывающиеся списки и селектор диапазона дат служат в качестве меню для взаимодействия с данными:

Первый компонент в меню ― это раскрывающийся список Region. Код компонента:

        html.Div(
    children=[
        html.Div(children="Region", className="menu-title"),
        dcc.Dropdown(
            id="region-filter",
            options=[
                {"label": region, "value": region}
                for region in np.sort(data.region.unique())
            ],
            value="Albany",
            clearable=False,
            className="dropdown",
        ),
    ]
),
    

Вот что означает каждый из параметров:

  • id ― идентификатор элемента.
  • options ― параметры, отображаемые при выборе раскрывающегося списка. Ожидает словарь с метками и значениями.
  • value ― значение по умолчанию при загрузке страницы.
  • clearable ― позволяет пользователю оставить поле пустым, если установлено значение True.
  • className ― селектор классов, используемый для применения стилей

Селекторы Type и Data Range имеют ту же структуру, что и раскрывающееся меню Region.

Теперь взглянем на компоненты dcc.Graphs:

        html.Div(
    children=[
        html.Div(
            children=dcc.Graph(
                id="price-chart", config={"displayModeBar": False},
            ),
            className="card",
        ),
        html.Div(
            children=dcc.Graph(
                id="volume-chart", config={"displayModeBar": False},
            ),
            className="card",
        ),
    ],
    className="wrapper",
),
    

По сравнению с предыдущей версией панели инструментов в компонентах отсутствует аргумент figure. Это связано с тем, что аргумент figure теперь будет генерироваться функцией обратного вызова с использованием входных данных, которые пользователь устанавливает с помощью селекторов Region, Type и Data Range.

Как определить обратные вызовы

Мы определили, как пользователь будет взаимодействовать с приложением. Теперь нужно заставить приложение реагировать на действия пользователя. Для этого мы воспользуемся функциями обратного вызова (callbacks).

Функции обратного вызова Dash ― это обычные функции Python с декоратором app.callback. При изменении ввода запускается функция обратного вызова, выполняет заранее определенные операции (например, фильтрация набора данных), и возвращает результат в приложение. По сути, обратные вызовы связывают в приложении входные и выходные данные.

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

        @app.callback(
    [Output("price-chart", "figure"), Output("volume-chart", "figure")],
    [
        Input("region-filter", "value"),
        Input("type-filter", "value"),
        Input("date-range", "start_date"),
        Input("date-range", "end_date"),
    ],
)
def update_charts(region, avocado_type, start_date, end_date):
    mask = (
        (data.region == region)
        & (data.type == avocado_type)
        & (data.Date >= start_date)
        & (data.Date <= end_date)
    )
    filtered_data = data.loc[mask, :]
    price_chart_figure = {
        "data": [
            {
                "x": filtered_data["Date"],
                "y": filtered_data["AveragePrice"],
                "type": "lines",
                "hovertemplate": "$%{y:.2f}<extra></extra>",
            },
        ],
        "layout": {
            "title": {
                "text": "Average Price of Avocados",
                "x": 0.05,
                "xanchor": "left",
            },
            "xaxis": {"fixedrange": True},
            "yaxis": {"tickprefix": "$", "fixedrange": True},
            "colorway": ["#17B897"],
        },
    }

    volume_chart_figure = {
        "data": [
            {
                "x": filtered_data["Date"],
                "y": filtered_data["Total Volume"],
                "type": "lines",
            },
        ],
        "layout": {
            "title": {
                "text": "Avocados Sold",
                "x": 0.05,
                "xanchor": "left"
            },
            "xaxis": {"fixedrange": True},
            "yaxis": {"fixedrange": True},
            "colorway": ["#E12D39"],
        },
    }
    return price_chart_figure, volume_chart_figure
    

Сначала мы определяем выходные данные с помощью объектов Output. Эти объекты принимают два аргумента:

  • Идентификатор элемента, который они изменят при выполнении функции.
  • Свойство изменяемого элемента Например, Output("price-chart", "figure") обновит свойство figure элемента "price-chart".

Затем мы определяем входы с помощью объектов Input, они также принимают два аргумента:

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

То есть Input("region-filter", "value") будет следить за изменениями элемента "region-filter" и примет его свойство value, если элемент изменится.

Примечание
Обсуждаемый здесь объектInputимпортирован изdash.dependencies. Не спутайте его с компонентом, поступающим изdash_core_components. Эти объекты не взаимозаменяемы и имеют разное назначение.

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

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

Разворачиваем Dash-приложение на Heroku

Мы закончили сборку приложения. У нас есть красивая, полностью интерактивная панель инструментов. Теперь мы узнаем, как ее развернуть.

Фактически приложения Dash ― то же, что приложения Flask, поэтому они имеют те же возможности для развертывания. В этом разделе мы развернем приложение на хостинге Heroku (с бесплатным тарифным планом).

Прежде чем начать, убедитесь, что вы установили интерфейс командной строки Heroku (CLI) и Git. Чтобы убедиться, что обе программы присутствуют в системе, выполните в терминале команды проверки версий:

        git --version
heroku --version
    

Далее нам нужно внести небольшое изменение в app.py. После инициализации приложения добавим переменную с именем server:

        app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
server = app.server
    

Это дополнение необходимо для запуска приложения с использованием WSGI-сервера. Встроенный сервер Flask не рекомендуется использовать в производственной среде, поскольку он не может обрабатывать большой объем трафика.

В корневом каталоге проекта создадим файл с именем runtime.txt, в котором укажем версию Python для приложения Heroku:

        python-3.8.6
    

При развертывании Heroku автоматически определит, что это приложение Python, и будет использовать соответствующий пакет сборки. Если вы также предоставите файл runtime.txt, сервер определит версию Python, которую будет использовать приложение.

Затем в корневом каталоге проекта создадим файл requirements.txt, где перечислим библиотеки, необходимые для установки Dash-приложения на веб-сервере:

        dash==1.13.3
pandas==1.0.5
gunicorn==20.0.4
    

В файле requirements.txt есть пакет, о котором мы раньше не упоминали: gunicorn. Gunicorn ― это HTTP-сервер WSGI, который часто используется для развертывания приложений Flask в производственной среде.

Теперь создадим файл с именем Procfile со следующим содержимым:

        web: gunicorn app:server
    

Этот файл сообщает приложению Heroku, какие команды следует выполнить для запуска нашего приложения.

Затем нужно инициализировать репозиторий Git. Для этого перейдем в корневой каталог проекта и выполним следующую команду:

        git init
    

Эта команда инициирует создание репозитория Git для avocado_analytics/. То есть Git будет отслеживать изменения, которые мы вносим в файлы в этом каталоге.

Однако есть файлы, которые не стоит отслеживать с помощью Git. Например, обычно мы не хотим отслеживать содержимое каталога виртуальной среды, файлов с байт-кодом и файлов метаданных, таких как .DS_Store.

Создадим в корневом каталоге файл с именем .gitignore и следующим содержимым:

        venv
*.pyc
.DS_Store # Only if you are using macOS
    

Это гарантирует, что репозиторий не отслеживает ненужные файлы. Теперь зафиксируем состояние проекта:

        git add .
git commit -m 'Add dashboard files'
    

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

        avocado_analytics/
├── assets/
│   ├── favicon.ico
│   └── style.css
├── venv/
├── app.py
├── avocado.csv
├── Procfile
├── requirements.txt
└── runtime.txt
    

Наконец, нужно создать приложение в Heroku, отправить туда свой код с помощью Git и запустить приложение на одном из бесплатных серверных вариантов Heroku. Для этого выполним следующие команды:

        heroku create APP-NAME  # Подставьте имя приложения
git push heroku master
heroku ps:scale web=1
    

Вот и всё. Мы создали и развернули панель управления данными. Чтобы получить доступ к приложению, достаточно скопировать ссылку https://APP-NAME.herokuapp.com/ и замените APP-NAME на имя, которое вы определили на предыдущем шаге.

Если вам интересно сравнить получившийся результат, взгляните на образец приложения.

Заключение

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

Источники

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG