Я создал панель мониторинга RShiny с помощьюplotly-r, используя набор данных пингвинов Палмера, чтобы, когда я нажимаю на сегмент гистограммы, она использовала эти данные о событиях для фильтрации набора данных.
При наведении:
По клику:
Я хотел создать аналогичную панель мониторинга с использованием Shiny для Python, но не смог заставить customdata
работать так же, как в R, и не могу понять, как обрабатывать событие щелчка.
Мне удалось создать объект customdata для сбора нужных категориальных данных для каждого сегмента панели, но мне не удалось найти функцию event_click()
вplotly для Python , аналогичную функцииplotly-r .
Может кто-нибудь, пожалуйста, скажите мне, как я могу заставить эту функцию работать? В настоящее время этот код запускает панель управления Shiny для Python, но не реагирует ни на какие события щелчка где-либо на гистограмме.
Я думаю, мне, вероятно, понадобится помощь в части @render_widget функции сервера. Вот мой Исходный код Python :
# Load data and compute static values
from shiny import App, reactive, render, ui
from shinywidgets import output_widget, render_widget, render_plotly
from plotnine import ggplot, aes, geom_bar
import plotly.graph_objects as go
import palmerpenguins
from plotly.callbacks import Points, InputDeviceState
points, state = Points(), InputDeviceState()
df_penguins = palmerpenguins.load_penguins()
dict_category = {'species':'Species','island':'Island','sex':'Gender'}
def filter_shelf():
return ui.card(
ui.card_header(
"Filters",
align = "center",
),
# Gender Filter
ui.input_checkbox_group(
'sex_filter',
label='Gender',
choices = {value:(value.capitalize() if (type(value)==str) else value) for value in df_penguins['sex'].unique()},
selected=list(df_penguins['sex'].unique()),
),
# Species Filter
ui.input_checkbox_group(
'species_filter',
label='Species',
choices=list(df_penguins['species'].unique()),
selected=list(df_penguins['species'].unique()),
),
# Island Filter
ui.input_checkbox_group(
'island_filter',
label='Island',
choices=list(df_penguins['island'].unique()),
selected=list(df_penguins['island'].unique()),
),
)
def parameter_shelf():
return ui.card(
ui.card_header(
'Parameters',
align='center',
),
# Category Selector
ui.input_radio_buttons(
'category',
label = 'View Penguins by:',
choices=dict_category,
selected = 'species',
),
),
app_ui = ui.page_fluid(
ui.h1("Palmer Penguins Analysis"),
ui.layout_sidebar(
# Left Sidebar
ui.sidebar(
filter_shelf(),
parameter_shelf(),
width=250,
),
# Main Panel
ui.card( # Plot
ui.card_header(ui.output_text('chart_title')),
output_widget('penguin_plot'),
),
ui.card( # Table
ui.card_header(ui.output_text('total_rows')),
ui.column(
12, #width
ui.output_table('table_view'),
style = "height:300px; overflow-y: scroll"
)
)
),
)
def server (input, output, session):
@reactive.calc
def category():
'''This function caches the appropriate Capitalized form of the selected category'''
return dict_category[input.category()]
# Dynamic Chart Title
@render.text
def chart_title():
return "Number of Palmer Penguins by Year, colored by "+category()
@reactive.calc
def df_filtered_stage1():
'''This function caches the filtered datframe based on selections in the view'''
return df_penguins[
(df_penguins['species'].isin(input.species_filter())) &
(df_penguins['island'].isin(input.island_filter())) &
(df_penguins['sex'].isin(input.sex_filter()))]
@reactive.calc
def df_filtered_stage2():
df_filtered_st2 = df_filtered_stage1()
# Eventually add additional filters on dataset from segments selected on the visual
return df_filtered_st2
@reactive.calc
def df_summarized():
return df_filtered_stage2().groupby(['year',input.category()], as_index=False).count().rename({'body_mass_g':"count"},axis=1)[['year',input.category(),'count']]
@reactive.calc
def filter_fn():
print("Clicked!") # This never gets called
@render_widget
def penguin_plot():
df_plot = df_summarized()
bar_columns = list(df_plot['year'].unique()) # x axis column labels
bar_segments = list(df_plot[input.category()].unique()) # bar segment category labels
data = [go.Bar(name=segment, x=bar_columns,y=list(df_plot[df_plot[input.category()]==segment]['count'].values), customdata=[input.category()], customdatasrc='A') for segment in bar_segments]
fig = go.Figure(data)
fig.update_layout(barmode = "stack")
fig = go.FigureWidget(fig)
##### TRYING TO CAPTURE CLICK EVENT ON A BAR SEGMENT HERE #####
fig.data[0].on_click(
filter_fn
)
return fig
@render.text
def total_rows():
return "Total Rows: "+str(df_filtered_stage1().shape[0])
@render.table
def table_view():
df_this=df_summarized()
return df_filtered_stage1()
app = App(app_ui, server)
Похоже, я могу фиксировать события кликов только по трассировке. Мне интересно, есть ли лучший способ, чем то, что я делаю выше, потому что fig.data имеет 3 трассировки столбцов во время выполнения при просмотре с помощью «видов» (Gentoo, Chinstrap и Adelie), и кажется, что каждая полоска трассировки - это то, что получает метод on_click().
Ниже приведен вариант, в котором я изменил несколько деталей, вот самые важные:
В вашем примере filter_fn
никогда не вызывается, поскольку он не зависит ни от какого reactive
выражения. Shiny
нет необходимости звонить.
Вы можете сделать это следующим образом: мы определяем reactive.value
под названием filter
, который содержит информацию фильтра из события on_click
(filter = reactive.value({})
) и функцию
def setClickedFilterValues(trace, points, selector):
if not points.point_inds:
return
filter.set({"Year": points.xs, "Category": points.trace_name})
которое установлено как событие on_click
на каждой трассировке:
for trace in fig.data:
trace.on_click(setClickedFilterValues)
Предложение if
в функции проверяет, находитесь ли вы на следе щелчка, если нет, остановитесь. filter
тогда содержит правильные значения. Важным моментом здесь является то, что функция не получает декоратор reactive
, такой как @reactive.calc
. В этом нет необходимости, поскольку мы только обновляем значение.
Я изменил df_filtered_stage2()
, чтобы учесть filter
, это вычисляет фрейм данных для выходных данных, отображаемых ниже в приложении.
@reactive.calc
def df_filtered_stage2():
if filter.get() == {}:
return df_filtered_stage1()
df_filtered_st2 = df_filtered_stage1()[
(df_filtered_stage1()['year'].isin(filter.get()['Year'])) &
(df_filtered_stage1()[input.category()].isin([filter.get()['Category']]))
]
return df_filtered_st2
Как и выше, в вашем приложении R
можно реализовать событие on_hover
, это описано ниже.
Это выглядит так:
# Load data and compute static values
from shiny import App, reactive, render, ui
from shinywidgets import output_widget, render_widget, render_plotly
from plotnine import ggplot, aes, geom_bar
import plotly.graph_objects as go
import palmerpenguins
from plotly.callbacks import Points, InputDeviceState
points, state = Points(), InputDeviceState()
df_penguins = palmerpenguins.load_penguins()
dict_category = {'species':'Species','island':'Island','sex':'Gender'}
def filter_shelf():
return ui.card(
ui.card_header(
"Filters",
align = "center",
),
# Gender Filter
ui.input_checkbox_group(
'sex_filter',
label='Gender',
choices = {value:(value.capitalize() if (type(value)==str) else value) for value in df_penguins['sex'].unique()},
selected=list(df_penguins['sex'].unique()),
),
# Species Filter
ui.input_checkbox_group(
'species_filter',
label='Species',
choices=list(df_penguins['species'].unique()),
selected=list(df_penguins['species'].unique()),
),
# Island Filter
ui.input_checkbox_group(
'island_filter',
label='Island',
choices=list(df_penguins['island'].unique()),
selected=list(df_penguins['island'].unique()),
),
)
def parameter_shelf():
return ui.card(
ui.card_header(
'Parameters',
align='center',
),
# Category Selector
ui.input_radio_buttons(
'category',
label = 'View Penguins by:',
choices=dict_category,
selected = 'species',
),
),
app_ui = ui.page_fluid(
ui.h1("Palmer Penguins Analysis"),
ui.layout_sidebar(
# Left Sidebar
ui.sidebar(
filter_shelf(),
parameter_shelf(),
width=250,
),
# Main Panel
ui.card( # Plot
ui.card_header(ui.output_text('chart_title')),
output_widget('penguin_plot'),
),
ui.output_text_verbatim('hoverInfoOutput'),
ui.card( # Table
ui.card_header(ui.output_text('total_rows')),
ui.column(
12, #width
ui.output_table('table_view'),
style = "height:300px; overflow-y: scroll"
)
)
),
)
def server (input, output, session):
filter = reactive.value({})
hoverInfo = reactive.value({})
@reactive.calc
def category():
'''This function caches the appropriate Capitalized form of the selected category'''
return dict_category[input.category()]
# Dynamic Chart Title
@render.text
def chart_title():
return "Number of Palmer Penguins by Year, colored by "+category()
@reactive.calc
def df_filtered_stage1():
'''This function caches the filtered datframe based on selections in the view'''
return df_penguins[
(df_penguins['species'].isin(input.species_filter())) &
(df_penguins['island'].isin(input.island_filter())) &
(df_penguins['sex'].isin(input.sex_filter()))]
@reactive.calc
def df_filtered_stage2():
if filter.get() == {}:
return df_filtered_stage1()
df_filtered_st2 = df_filtered_stage1()[
(df_filtered_stage1()['year'].isin(filter.get()['Year'])) &
(df_filtered_stage1()[input.category()].isin([filter.get()['Category']]))
]
return df_filtered_st2
@reactive.calc
def df_summarized():
return df_filtered_stage1().groupby(['year',input.category()], as_index=False).count().rename({'body_mass_g':"count"},axis=1)[['year',input.category(),'count']]
def setClickedFilterValues(trace, points, selector):
if not points.point_inds:
return
filter.set({"Year": points.xs, "Category": points.trace_name})
def setHoverValues(trace, points, selector):
if not points.point_inds:
return
hoverInfo.set(points)
@render_widget
def penguin_plot():
df_plot = df_summarized()
bar_columns = list(df_plot['year'].unique()) # x axis column labels
bar_segments = list(df_plot[input.category()].unique()) # bar segment category labels
data = [go.Bar(name=segment, x=bar_columns,y=list(df_plot[df_plot[input.category()]==segment]['count'].values), customdata=[input.category()], customdatasrc='A') for segment in bar_segments]
fig = go.Figure(data)
fig.update_layout(barmode = "stack")
fig = go.FigureWidget(fig)
for trace in fig.data:
trace.on_click(setClickedFilterValues)
trace.on_hover(setHoverValues)
return fig
@render.text
def hoverInfoOutput():
return hoverInfo.get()
@render.text
def total_rows():
return "Total Rows: "+str(df_filtered_stage2().shape[0])
@render.table
def table_view():
#df_this=df_summarized()
return df_filtered_stage2()
app = App(app_ui, server)
Что касается вашего дополнительного вопроса, я думаю, мне нужно более глубоко изучить исходный код, я сделаю это и скажу вам...
Я ценю это! Кстати, даже когда я вытащил строки кода: изplotly.callbacks import Points, точки InputDeviceState, state = Points(), InputDeviceState(), метод трассировки.on_click() все еще, казалось, понимал точки и объекты селектора, даже несмотря на то, что когда Я поставил точку останова на этой строке, ни один из этих объектов не был определен ни в локальной, ни в глобальной области. Я подтвердил это, попытавшись получить к ним доступ в консоли отладки. Я полагаю, что трассировка — это просто экземпляр трассировки, передающий себя в setClickedFilterValues(trace,points,selector)?
Да, скорее всего, так и будет, хотя на данный момент я не могу подтвердить это в исходном коде. Возможно, я посмотрю позже, но, например. здесь вы видите определение on_click
, которое регистрирует только обратный вызов. Можно продолжить исследование того, как определяются _click_callbacks
, но я думаю, что следы будут похожи на Точки.
У вас все получилось, спасибо! Мне интересно, можете ли вы прояснить мой фундаментальный вопрос о нереактивной функции setClickedFilterValues(). Сначала я попробовал что-то подобное из документации по сюжету: plotly.com/python/click-events , но я не был уверен, как передать реактивность в Shiny. Я получал ошибки, связанные с функцией, которая принимает 3 аргумента (трассировка, точки, селектор), но ни один из них не был предоставлен. Почему ваша строка кода: трассировка.on_click(setClickedFilterValues) неявно знает об объектах трассировки, точках и селекторах?