В предыдущей статье мы говорили о Python-фреймворке визуализации данных — Dash. Но со временем Data Science проекты могут усложняться, и уже простой визуализацией не отделаться. Поэтому сегодня продолжим углубляться в тему обратных вызовов (callback) в Dash и рассмотрим множество Input и Output, а также сохранение состояния с помощью State.
Множество Input
В предыдущий раз мы реализовали обратный вызов (callback) Dash — отобразили на странице график ожидаемой продолжительности жизни в разные периоды. Также на странице есть слайдер, на котором можно выбрать год. От выбранного года изменяется и вид графика. Это и есть основа реактивного программирования, реализуемое через обратные вызовы, — с изменением входных данных изменяются выходные.
До этого у нас был всего один Input, но их на веб-странице может быть сколько угодно. Так, например, мы можем добавить переключатель для выбора между логарифмической или линейной шкалой.
Добавить такой переключатель можно с помощью RadioItems — одного из компонентов Dash. В options
указываем возможные опции: Linear и Log. Код на Python выглядит следующим образом:
import dash import dash_html_components as html import dash_core_components as dcc import pandas as pd df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv') app = dash.Dash('python-school') app.layout = html.Div([ dcc.RadioItems( id='scale-button', options=[{'label': i, 'value': i} for i in ['Linear', 'Log']], value='Linear', ), dcc.Graph(id='graph'), dcc.Slider( id='year-slider', min=df['year'].min(), max=df['year'].max(), value=df['year'].min(), marks={str(year): str(year) for year in df['year'].unique()}, step=None ) ])
Поскольку теперь два Input, то и декорируемая функция тоже должна иметь два аргумента. Второй Input должен принимать id с переключателем и его значением. Для инициализации линейной или логарифмической шкалы передается соответствующий аргумент log_x
. Код на Python:
@app.callback( Output('graph', 'figure'), [Input('year-slider', 'value'), Input('scale-button', 'value')]) def update_figure(selected_year, scale_value): filtered_df = df[df.year == selected_year] if scale_value == 'Linear': log_x = False else: log_x = True fig = px.scatter(filtered_df, x="gdpPercap", y="lifeExp", size="pop", color="continent", hover_name="country", log_x=log_x, size_max=55) return fig app.run_server(debug=True)
Множество Output
Dash-приложение может иметь также множество Output, т.е. отображать на веб-странице множество обновляемых компонент. Нужно лишь в декораторе callback поместить список Output’ов, которые будут обновляться в зависимости от Input’ов. Декорируемая функция должна возвращать столько значений Output, сколько указано в декораторе.
Например, ниже показано простое приложение, которое показывает результаты возведения в степень в виде таблицы.
app.layout = html.Div([ dcc.Input( id='num-multi', type='number', value=5 ), html.Table([ html.Tr([html.Td(['x', html.Sup(2)]), html.Td(id='square')]), html.Tr([html.Td(['x', html.Sup(3)]), html.Td(id='cube')]), html.Tr([html.Td([2, html.Sup('x')]), html.Td(id='twos')]), html.Tr([html.Td([3, html.Sup('x')]), html.Td(id='threes')]), html.Tr([html.Td(['x', html.Sup('x')]), html.Td(id='x^x')]), ]), ]) @app.callback( [Output('square', 'children'), Output('cube', 'children'), Output('twos', 'children'), Output('threes', 'children'), Output('x^x', 'children')], [Input('num-multi', 'value')]) def callback_a(x): return x**2, x**3, 2**x, 3**x, x**x
Взаимосвязанные обратные вызовы
Также имеется возможность связать Input и Ouput вместе — выход одной декорируемой функции может быть входом другой. Такое использование может пригодиться для создания динамических пользовательских интерфейсов, когда один Input обновляет параметры следующего Input. Ниже Python-код с отображением стран и городов этих стран.
all_options = { 'США': ['Нью-Йорк', 'Сан-Франциско', 'Вашингтон'], 'Россия': ['Москва', 'Новосибирск', 'Питер'] } app.layout = html.Div([ dcc.RadioItems( id='countries-radio', options=[{'label': k, 'value': k} for k in all_options.keys()], value='США' ), html.Hr(), dcc.RadioItems(id='cities-radio'), html.Hr(), html.Div(id='display-selected-values') ]) @app.callback( Output('cities-radio', 'options'), [Input('countries-radio', 'value')]) def set_cities_options(selected_country): return [{'label': i, 'value': i} for i in all_options[selected_country]] @app.callback( Output('cities-radio', 'value'), [Input('cities-radio', 'options')]) def set_cities_value(available_options): return available_options[0]['value'] @app.callback( Output('display-selected-values', 'children'), [Input('countries-radio', 'value'), Input('cities-radio', 'value')]) def set_display_children(selected_country, selected_city): return '{} - это город {}'.format( selected_city, selected_country, )
Отметим основные положения:
- Первый callback обновляет опции выбора города в зависимости от страны, которая указана в RadioItmes;
- Второй callback устанавливает начальное значение города (в списке выбирается 0-й элемент, т.е. первый);
- Третий callback отображает выбранную страну и город. Изменяя значение страны, Dash будет ждать обновления города во втором callback. Благодаря такому поведению мы не получим результат в виде «Нью Йорк — это город России».
Фиксируем состояние
В предыдущем примере отображение «Нью Йорк — это город США» происходит сразу после соответствующего выбора. А что если требуется, чтобы результат появился, например, после нажатия на кнопку, а не сразу? Тогда нужно сохранить состояния выбранных значений. Для этого используется зависимость State, которая позволяет передавать значения, не вызывая callback. Ниже представлен пример на Python.
from dash.dependencies import Input, Output, State app = dash.Dash('my-app') app.layout = html.Div([ dcc.Input(id='input-1-state', type='text', value='Питер'), dcc.Input(id='input-2-state', type='text', value='Москва'), html.Button(id='submit-button-state', n_clicks=0, children='Submit'), html.Div(id='output-state') ]) @app.callback(Output('output-state', 'children'), [Input('submit-button-state', 'n_clicks')], [State('input-1-state', 'value'), State('input-2-state', 'value')]) def update_output(n_clicks, input1, input2): return u''' Кнопка была нажата {} раз, Input 1 - это "{}", и Input 2 - это "{}" '''.format(n_clicks, input1, input2) app.run_server(debug=True)
В этом примере изменение текста в dcc.Input
не вызывает callback. Он вызывается только по значению n_clicks
, т.е. после нажатия на кнопку Submit. Количество кликов также сохраняется.
В следующей статье поговорим об отладке callback’ов в Dash. А о том, как визуализировать данные в Plotly и Dash для решения реальных задач Data Science, вы узнаете на специализированном курсе «VIP: Визуализация данных на языке Python» в лицензированном учебном центре обучения и повышения квалификации IT-специалистов в Москве.