Веб-разработка с Python: Dash (полное руководство)

Рисование с помощью Plotly, внедрение Bootstrap CSS, загрузка и скачивание файлов, изменение ввода после выбора, панели навигации, счетчики и многое другое…

Резюме

Добро пожаловать в это хардкорное руководство по Dash. После этой статьи вы сможете создать и развернуть базовый прототип (минимально жизнеспособный продукт) для любого типа веб-приложения.

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

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



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



В частности, я пройду через:

  • Настройка среды
  • Модель данных
  • Подготовить базовую структуру приложения тире (панель навигации, тело, макет)
  • Входные данные (форма, ползунок, руководство, загрузка файла, изменение входных данных после события)
  • Вывод (Plotly, загрузка файла, счетчик при загрузке)
  • Разверните приложение на Heroku

Настраивать

Первым делом я установлю через терминал следующие библиотеки:

## for application
dash==1.20.0
dash-bootstrap-components==0.12.2
## to produce random data
names==0.3.0
## for data processing
numpy==1.19.5
pandas==1.1.5
## for plotting
plotly==4.14.3
## for read and write excel files
openpyxl==3.0.7
xlrd==1.2.0

Чтобы панель управления выглядела красиво, мы будем использовать Bootstrap, фреймворк CSS / JS, который содержит шаблоны дизайна для форм, кнопок, навигации и других компонентов интерфейса . Пакет D ash-Bootstrap-Components позволяет легко интегрировать Bootstrap в наше приложение Dash.

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

Обратите внимание, что последняя версия xlrd (2.0.0) не принимает файлы .xlsx, поэтому используйте 1.2.0 если вы хотите загрузить файлы Excel.

Модель данных

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

import names
import pandas as pd
import numpy as np

'''
Generate random guests list
:parameter
    :param n: num - number of guests and length of dtf
    :param lst_categories: list - ["family", "friends", ...]
    :param n_rules: num - number of restrictions to apply (ex. if 1 
                    then 2 guests can't be sit together)
:return
    dtf with guests
'''
def random_data(n=100, lst_categories=["family","friends",
                "work","university","tennis"], n_rules=0):
    ## basic list
    lst_dics = []
    for i in range(n):
        name = names.get_full_name()
        category = np.random.choice(lst_categories) if 
                   len(lst_categories) > 0 else np.nan
        lst_dics.append({"id":i, "name":name, "category":category, 
                         "avoid":np.nan})
    dtf = pd.DataFrame(lst_dics)
    ## add rules
    if n_rules > 0:
        for i in range(n_rules):
            choices = dtf[dtf["avoid"].isna()]["id"]
            ids = np.random.choice(choices, size=2)
            dtf["avoid"].iloc[ids[0]] = int(ids[1]) if 
              int(ids[1]) != ids[0] else int(ids[1])+1
    return dtf

Эта функция создает таблицу с информацией о гостях.

Я воспользуюсь столбцом «Категория», чтобы отображать гостей разными цветами:

Столбец «избегать» будет использоваться для того, чтобы два гостя, ненавидящие друг друга, не оказались за одним столом.

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

  • награда +1, когда два человека, принадлежащих к одной категории, сидят рядом друг с другом
  • штраф -1, когда два человека, которые хотят избежать друг друга, помещаются за один стол.

Эта функция возвращает тот же фрейм данных с новым столбцом для назначенной таблицы:

Структура приложения

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

  • Ввод - раздел приложения, в котором пользователь может вставлять и выбирать параметры и данные, которые будут использоваться серверной частью для возврата желаемого результата (для панели навигации ввод не требуется).
  • Вывод - раздел приложения, в котором пользователь может визуализировать результаты.
  • Обратные вызовы - декоратор, оборачивающий функцию, в которой нужно указать выходы, входы и состояния; последние позволяют передавать дополнительные значения без использования обратных вызовов (обратные вызовы могут выглядеть пугающе, но на самом деле они ваши лучшие друзья).
# setup
import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc
# App Instance
app = dash.Dash(name="name")
app.title = "name"
########################## Navbar ##########################
# Input
# Output
navbar = dbc.Nav()
# Callbacks
@app.callback()
def function():
    return 0
########################## Body ##########################
# Input
inputs = dbc.FormGroup()
# Output
body = dbc.Row([
        ## input
        dbc.Col(md=3),
        ## output
        dbc.Col(md=9)
])
# Callbacks
@app.callback()
def function():
    return 0
########################## App Layout ##########################
app.layout = dbc.Container(fluid=True, children=[
    html.H1("name", id="nav-pills"),
    navbar,
    html.Br(),html.Br(),html.Br(),
    body
])
########################## Run ##########################
if __name__ == "__main__":
    app.run_server(debug=True)

Начнем со стиля . Замечательный Dash-Bootstrap-Components предлагает огромное разнообразие предопределенных стилей. Вы можете ознакомиться с ними здесь. Выбрав один из них, вы можете вставить его в экземпляр приложения как внешнюю таблицу стилей. Вы даже можете использовать более одного:

theme = dbc.themes.LUX
css = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'
# App Instance
app = dash.Dash(name="name",
                external_stylesheets=[theme, css])

Перейдем к верхней панели навигации. Я добавлю ссылку, всплывающее окно и раскрывающееся меню.

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

Навигационная панель содержит 3 навигационных элемента: логотип, кнопку «О программе», раскрывающееся меню. Кнопка «О программе» включает в себя 2 элемента: навигационную ссылку (которая обычно используется для навигации по многостраничному приложению, но в данном случае href = ”/”) и всплывающее окно (зеленый и красный Метки). Эти 2 элемента вызываются в обратном вызове как выходы, входы и состояния, как это, если щелкнуть навигационную ссылку «О программе», тогда всплывающее окно станет активным и появится. Функция python about_popover () ожидает 3 аргумента, потому что обратный вызов имеет один вход и два состояния, и возвращает 2 переменные, поскольку обратный вызов имеет два выхода. Когда приложение запускается и кнопка не нажимается n = 0.

Обратите внимание, что раскрывающееся меню (синяя часть) включает шрифты, импортированные с помощью внешней таблицы стилей (т. Е. className = ”fa fa-linkedin”).

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

Теперь поговорим о слоне в комнате… основном теле. Он состоит из входов (слева) и выходов (справа), которые взаимодействуют друг с другом благодаря обратным вызовам.

Входы

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

Вот как можно создать обычный слайдер:

dcc.Slider(id="n-guests", min=10, max=100, step=1, value=50, 
           tooltip={'always_visible':False})

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

dcc.Slider(id="n-iter", min=10, max=1000, step=None, 
           marks={10:"10", 100:"100", 500:"500", 1000:"1000"}, 
           value=0),

Это простой ручной ввод:

dbc.Input(id="max-capacity", placeholder="table capacity", 
          type="number", value="10"),

Давайте увеличим сложность и разберемся с ситуацией «Загрузка файла». Я собираюсь дать пользователям возможность загрузить файл Excel, содержащий аналогичный набор данных, который мы сгенерировали случайным образом:

При загрузке файла я хочу, чтобы произошло два события:

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

Как нам этого добиться? Легко, с волшебным обратным вызовом, который изменяет стиль CSS компонентов HTML:

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

import pandas as pd
import base64
import io
'''
When a file is uploaded it contains "contents", "filename", "date"
:parameter
    :param contents: file
    :param filename: str
:return
    pandas table
'''
def upload_file(contents, filename):
    content_type, content_string = contents.split(',')
    decoded = base64.b64decode(content_string)
    try:
        if 'csv' in filename:
            return pd.read_csv(io.StringIO(decoded.decode('utf-8')))
        elif 'xls' in filename:
            return pd.read_excel(io.BytesIO(decoded))
    except Exception as e:
        print("ERROR:", e)
        return 'There was an error processing this file.'

Прежде чем перейти к выводам, давайте подведем итоги того, что мы видели до сих пор. Вот полный код входов в основном теле:

########################## Body ##########################
# Input
inputs = dbc.FormGroup([
    ## hide these 2 inputs if file is uploaded
    html.Div(id='hide-seek', children=[
        dbc.Label("Number of Guests", html_for="n-guests"), 
        dcc.Slider(id="n-guests", min=10, max=100, step=1, value=50, 
                   tooltip={'always_visible':False}),
        dbc.Label("Number of Rules", html_for="n-rules"), 
        dcc.Slider(id="n-rules", min=0, max=10, step=1, value=3, 
                   tooltip={'always_visible':False})
    ], style={'display':'block'}),
    ## always visible
    dbc.Label("Number of Trials", html_for="n-iter"), 
    dcc.Slider(id="n-iter", min=10, max=1000, step=None, 
               marks={10:"10", 100:"100", 500:"500", 1000:"1000"}, 
               value=0),
    html.Br(),
    dbc.Label("Max Guests per Table", html_for="max-capacity"), 
    dbc.Input(id="max-capacity", placeholder="table capacity", 
              type="number", value="10"),
    ## upload a file
    html.Br(),
    dbc.Label("Or Upload your Excel", html_for="upload-excel"), 
    dcc.Upload(id='upload-excel', children=html.Div([
               'Drag and Drop or ', html.A('Select Files')]),
               style={'width':'100%', 'height':'60px', 
                      'lineHeight':'60px', 'borderWidth':'1px',  
                      'borderStyle':'dashed',
                      'borderRadius':'5px', 'textAlign':'center', 
                      'margin':'10px'} ),
    html.Div(id='excel-name', style={"marginLeft":"20px"}),
    ## run button
    html.Br(),html.Br(),
    dbc.Col(dbc.Button("run", id="run", color="primary"))
])

# Callbacks
@app.callback(
 output=[
  Output(component_id="hide-seek", component_property="style"),
  Output(component_id="excel-name", component_property="children")], 
 inputs=[
  Input(component_id="upload-excel",component_property="filename")])
def upload_event(filename):
    div = "" if filename is None else "Use file "+filename
    return {'display':'block'} if filename is None else 
           {'display':'none'}, div

Выходы

Серверная часть должна выдавать 3 вывода: заголовок, ссылку для загрузки результатов в виде файла Excel и, очевидно, график.

Приступим к сюжету, сделанному с помощью Plotly. По сути, есть два основных модуля этой удивительной графической библиотеки: plotly express и graph_objects. Первый - это графический инструмент высокого уровня, содержащий функции, которые могут создавать целые фигуры сразу (я нахожу это похожим на seaborn), в то время как последний позволяет вам строить фигуру по кирпичику (на самом деле это то, что сюжетно выражает работает под капотом).

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

Итак ... что это?

Это может выглядеть как рисунок, но это правильная геометрическая фигура: это диаграмма рассеяния, цвет на которой основан на категории, к которой принадлежат гости (семья, друзья,…), размер определяется тем, хочет ли гость избегать кого-то. или нет (в основном я выделяю проблемные), а фасетный компонент связан с таблицей, назначенной каждому гостю. Фасетные графики - это фигуры, составленные из нескольких подзаголовков, которые имеют одинаковый набор осей, где каждый подзаголовок показывает подмножество данных.

Прежде всего, мне нужно добавить координаты x и y для графика, используя уравнение круга: (x, y) = (r * cosθ, r * sinθ). Затем я добавляю столбец «размер» на основе столбца «избегать»:

def prepare_data(dtf):
    ## mark the rules
    dtf["avoid"] = dtf["avoid"].apply(lambda x: 
                     dtf[dtf["id"]==x]["name"].iloc[0] if 
                     pd.notnull(x) else "none")
    dtf["size"] = dtf["avoid"].apply(lambda x: 1 if x == "none" 
                     else 3)
    ## create axis
    dtf_out = pd.DataFrame()
    lst_tables = []
    for t in dtf["table"].unique():
        dtf_t = dtf[dtf["table"]==t]
        n = len(dtf_t)
        theta = np.linspace(0, 2*np.pi, n)
        dtf_t["x"] = 1*np.cos(theta)
        dtf_t["y"] = 1*np.sin(theta)
        dtf_out = dtf_out.append(dtf_t)
    return dtf_out.reset_index(drop=True).sort_values("table")

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

import plotly.express as px
fig = px.scatter(dtf, x="x", y="y", color="category", 
                 hover_name="name", facet_col="table", 
                 facet_col_wrap=3, size="size",                         
                 hover_data={"x":False, "y":False, "category":True, 
                             "avoid":True, "size":False,  
                             "table":False}
              )         
fig.add_shape(type="circle", opacity=0.1, fillcolor="black", 
              col="all", row="all", exclude_empty_subplots=True,                       
              x0=dtf["x"].min(), y0=dtf["y"].min(), 
              x1=dtf["x"].max(), y1=dtf["y"].max()
              )         
fig.update_layout(plot_bgcolor='white', 
                  legend={"bordercolor":"black", 
                          "borderwidth":1, 
                          "orientation":"h"}
              )

Полный код для сюжета (и заголовок):

Теперь, когда график готов, как загрузить результаты в виде файла Excel? Нам просто нужна функция, которая преобразует фрейм данных pandas в файл и передает ссылку для его загрузки в пользовательский интерфейс:

import pandas as pd
import io
import base64
'''
Write excel
:parameter
    :param dtf: pandas table
:return
    link
'''
def download_file(dtf):
    xlsx_io = io.BytesIO()
    writer = pd.ExcelWriter(xlsx_io)
    dtf.to_excel(writer, index=False)
    writer.save()
    xlsx_io.seek(0)
    media_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    data = base64.b64encode(xlsx_io.read()).decode("utf-8")
    link = f'data:{media_type};base64,{data}'
    return link

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

Как вы могли заметить, выходные данные (заголовок, загрузка, сюжет) заключены в Spinner, который воспроизводит этот приятный эффект состояния загрузки при разработке входных данных:

Развертывать

Наконец, мы готовы развернуть это приложение. Вот полный код приложения Dash (с остальной частью репозитория вы можете ознакомиться на GitHub):

Лично мне нравится Heroku за развертывание прототипов . Вам нужно будет добавить requirements.txt и Procfile. Если вам нужна помощь, вы можете найти подробные руководства здесь и здесь.

Вывод

Эта статья представляет собой (почти) полное руководство о том, как создать хорошее веб-приложение с помощью Python Dash. Это приложение довольно простое, поскольку в нем нет базы данных и функции входа в систему (может быть, материал для следующего руководства?). Здесь я просто хотел продемонстрировать, как вы можете легко превратить свои идеи в прототип, чтобы показать миру. А теперь, когда вы знаете, как это работает, вы можете разработать собственное приложение.

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

LinkedIn | Инстаграм | Twitter | GitHub

Эта статья является частью серии Веб-разработка с помощью Python, см. Также:





«Создание и развертывание бота Telegram с краткосрочной и долгосрочной памятью
Создайте чат-бота с нуля, который запоминает и напоминает события с помощью Python pub.towardsai. сеть"