So I have a sidebar. When "Go" in the sidebar is selected, the main dashboard should update. This works.
What doesn't work is when the radio button (marketing consent) within the main dashboard is selected, the entire dashboard resets. It will revert to the original view as opposed to what it looked like after "Go" was selected. It's as if the "Go" button resets and gets unselected, when the radio button is clicked...
"""
# My first app
Here's our first attempt at using data to create a table:
"""
##Packages
from unicodedata import numeric
import streamlit as st
import numpy as np
import pandas as pd
from PIL import Image
import pandasql as ps
import altair as alt
import plotly.express as px
st.set_page_config(page_title = "Dashboard")
st.title("Dashboard 💪")
##Dataframe
df = pd.DataFrame({
'first': [1, 2, 3, 4],
'second': [10, 20, 30, 40],
'third': ['apple', 'banana', 'grape', 'grape'],
'fourth': ['walter', 'skyler', 'hank', 'marie']
})
####
###Sidebar things
####
###Logo
with st.sidebar.container():
st.sidebar.title("Sidebar")
with st.sidebar.container():
add_selectbox = st.sidebar.multiselect(
'Choose some properties',
(df.third.unique())
)
fourth_box = st.sidebar.selectbox(
'Choose your country',
(df.fourth.unique()),
index = 1
)
####Page 1
if st.sidebar.button('Go!'):
with st.container():
status = st.radio(
"Marketing Consent Status",
('Yes', 'All'))
df_iris = px.data.iris()
if marketing_status == 'Yes':
fig_1 = px.bar(df_iris, x="sepal_width", y="sepal_length", color="species",
hover_data=['petal_width'], barmode = 'stack')
st.plotly_chart(fig_1, use_container_width=True)
elif marketing_status == 'All':
st.write('Hello, *World!')
else:
with st.container():
df_map = px.data.gapminder().query("year==2007")
fig_map = px.choropleth(df_map, locations="iso_alpha",
color="lifeExp", # lifeExp is a column of gapminder
hover_name="country", # column to add to hover information
color_continuous_scale=px.colors.sequential.Plasma)
st.plotly_chart(fig_map, use_container_width=True)
As you'll see, when you use the radio button (Marketing Consent Button) after it appears , it will revert to the map view of the main.
To prevent widget interactions from causing the entire app to rerun, you can make your widgets part of a form with a submit button, e.g.:
with st.form("my_form"):
st.write("Inside the form")
slider_val = st.slider("Form slider")
checkbox_val = st.checkbox("Form checkbox")
# Every form must have a submit button.
submitted = st.form_submit_button("Submit")
if submitted:
st.write("slider", slider_val, "checkbox", checkbox_val)
st.write("Outside the form")
Related
I am looking for a clever/interactive way to modify the wrong values in a database by clicking on the plotly graph showing them. In other words, I want to add a sort of data modification zone, preferably on top of my plotly graphs (maybe a form with submit button or so ... ? )
For now, I am able to access the data point informations when clicking on it using the clickData property of the graph.
Here is a simplified version of the callback function I used.
#app.callback(
Output('output', 'children'),
[Input('graph_interaction', 'clickData')])
def update_output(clickData):
if clickData:
point_index = clickData['points'][0]['pointIndex']
point_value = clickData['points'][0]['y']
print(clickData)
# update the point value in the database using SQLAlchemy
# ...
return 'You clicked on data point {} with value {}'.format(point_index, point_value)
return ''
Any insights on how to add a modification area (form ?) to interact with the database and modify wrong values ?
Thank you
I've written a dash app that allows the user to interactively select and deselect data points to remove from a sample dataset. The main considerations are the following:
we should use dcc.Store to store data because global variables will break your app (see this example on storing data in the documentation). we can store both the dataframe (in the form of a dictionary with the index as keys, which will guarantee uniqueness), and also store the index of the points we click
clicking points on the figure will update the clicked points we store, and also populate a textbox so the user can see which points they are removing. clicking the same point again will remove that point from storage (and the textbox)
there are two buttons: the update button will remove the clicked points from the stored points and update the figure. there is also a button to clear all points we want to remove from storage and the textbox (this is because it appears dash cannot process selecting and then immediately deselecting a point, so we'll use this button instead)
import numpy as np
import pandas as pd
import plotly.express as px
import dash
from dash import Input, Output, dcc, html, ctx
from typing import List
app = dash.Dash(__name__)
df = pd.DataFrame({
'x': list(range(5,10)),
'y': list(range(1,6))
})
fig = px.scatter(df, x='x', y='y')
app.layout = html.Div([
html.Div(
[
dcc.Textarea(id='selected-points-textbox'),
html.Br(),
html.Button('Clear Selection', id='clear-textbox', n_clicks=0),
html.Button('Update Data', id='update-data', n_clicks=0),
],
style={"padding-left": "80px"},
),
html.Div([
dcc.Graph(figure=fig, id='graph-interaction'),
dcc.Store(id='store-selected-points'),
dcc.Store(id='store-data')
])
])
#app.callback(
Output('store-selected-points','data'),
Output('store-data','data'),
Output('selected-points-textbox','value'),
Output('graph-interaction','figure'),
[Input('graph-interaction', 'clickData'),
Input('clear-textbox', 'n_clicks'),
Input('update-data', 'n_clicks'),
Input('store-selected-points','data'),
Input('store-data','data'),
Input('graph-interaction','figure'),
])
def show_selection(clickData, clearButton, updateButton, storedSelectedPoints, storedData, fig):
## initialize storedSelectedPoints and storedData
## we will store the pointIndex in the storedSelectedPoints
if storedSelectedPoints is None:
storedSelectedPoints = []
if storedData is None:
storedData = df.to_dict('index')
## storedData is in the following format:
# {
# 0: {'x': 5, 'y': 1},
# 1: {'x': 6, 'y': 2},
# 2...
# }
if ctx.triggered_id == "clear-textbox":
storedSelectedPoints = []
storedSelectedPointsText = '[]'
elif ctx.triggered_id == "update-data":
for p in storedSelectedPoints:
del storedData[p]
storedSelectedPoints = []
storedSelectedPointsText = '[]'
print(f"storedData with points removed: {storedData}")
df_new = pd.DataFrame.from_dict(storedData, orient='index')
fig = px.scatter(df_new, x='x', y='y')
## update the point value in the database using SQLAlchemy
elif clickData is not None:
## note that these index values will need to be strings
point_index = str(clickData['points'][0]['pointIndex'])
if point_index not in storedSelectedPoints:
storedSelectedPoints.append(point_index)
else:
storedSelectedPoints.remove(point_index)
storedSelectedPointsText = str(
[[storedData[p]['x'], storedData[p]['y']] for p in storedSelectedPoints]
)
return storedSelectedPoints, storedData, storedSelectedPointsText, fig
storedSelectedPointsText = str(storedSelectedPoints)
return storedSelectedPoints, storedData, storedSelectedPointsText, fig
if __name__ == "__main__":
app.run_server(debug=True)
I created a form with two drop down menu's:
My goal is to make one dropdown dependent on the other dropdown.
This picture illustrates my goal and the current situation.
The sample code below can be run in Google Colab or Jupyter notebook to replicate the current situation.
##title
import ipywidgets as widgets
from ipywidgets import HBox, Label
from ipywidgets import Layout, Button, Box, FloatText, Textarea, Dropdown, Label, IntSlider
import time
import pandas as pd
#Create DF
df = df = pd.DataFrame(columns = ['Dropdown_column', 'Float_column'])
df
# Layout
form_item_layout = Layout(
display='flex',
flex_flow='row',
justify_content='space-between',
)
button_item_layout = Layout(
display='flex',
flex_flow='row',
justify_content='center',
padding = '5%'
)
# Independent dropdown item
drop_down_input = 'Dropdown_input_1'
drop_down = widgets.Dropdown(options=('Dropdown_input_1', 'Dropdown_input_2'))
def dropdown_handler(change):
global drop_down_input
print('\r','Dropdown: ' + str(change.new),end='')
drop_down_input = change.new
drop_down.observe(dropdown_handler, names='value')
# Dependent drop down
# Dependent drop down elements
dependent_drop_down_elements = {}
dependent_drop_down_elements['Dropdown_input_1'] = ['A', 'B']
dependent_drop_down_elements['Dropdown_input_2'] = ['C', 'D', 'E']
# Define dependent drop down
dependent_drop_down = widgets.Dropdown(options=(dependent_drop_down_elements['Dropdown_input_1']))
def dropdown_handler(change):
global drop_down_input
print('\r','Dropdown: ' + str(change.new),end='')
drop_down_input = change.new
drop_down.observe(dropdown_handler, names='value')
# Button
button = widgets.Button(description='Add row to dataframe')
out = widgets.Output()
def on_button_clicked(b):
global df
button.description = 'Row added'
time.sleep(1)
with out:
new_row = {'Dropdown_column': drop_down_input, 'Float_column': float_input}
df = df.append(new_row, ignore_index=True)
button.description = 'Add row to dataframe'
out.clear_output()
display(df)
button.on_click(on_button_clicked)
# Form items
form_items = [
Box([Label(value='Independent dropdown'),
drop_down], layout=form_item_layout),
Box([Label(value='Dependent dropdown'),
dependent_drop_down], layout=form_item_layout)
]
form = Box(form_items, layout=Layout(
display='flex',
flex_flow='column',
border='solid 1px',
align_items='stretch',
width='30%',
padding = '1%'
))
display(form)
display(out)
Is this possible? If so, can you explain how?
You can do this by setting a if statement in your dropdown_handler function that checks if the value chosen in your independent dropdown is Dropdown_input_1 or Dropdown_input2. Still in the dropdown_handler, you can then modify accordingly the options arguments to either dependent_drop_down_elements['Dropdown_input_1'] or dependent_drop_down_elements['Dropdown_input_2'].
See code below:
import ipywidgets as widgets
from ipywidgets import HBox, Label
from ipywidgets import Layout, Button, Box, FloatText, Textarea, Dropdown, Label, IntSlider
import time
import pandas as pd
#Create DF
df = df = pd.DataFrame(columns = ['Dropdown_column', 'Float_column'])
df
# Layout
form_item_layout = Layout(
display='flex',
flex_flow='row',
justify_content='space-between',
)
button_item_layout = Layout(
display='flex',
flex_flow='row',
justify_content='center',
padding = '5%'
)
# Independent dropdown item
drop_down_input = 'Dropdown_input_1'
drop_down = widgets.Dropdown(options=('Dropdown_input_1', 'Dropdown_input_2'))
# Dependent drop down
# Dependent drop down elements
dependent_drop_down_elements = {}
dependent_drop_down_elements['Dropdown_input_1'] = ['A', 'B']
dependent_drop_down_elements['Dropdown_input_2'] = ['C', 'D', 'E']
# Define dependent drop down
dependent_drop_down = widgets.Dropdown(options=(dependent_drop_down_elements['Dropdown_input_1']))
def dropdown_handler(change):
global drop_down_input
print('\r','Dropdown: ' + str(change.new),end='')
drop_down_input = change.new
#If statement checking on dropdown value and changing options of the dependent dropdown accordingly
if change.new=='Dropdown_input_2':
dependent_drop_down.options=dependent_drop_down_elements['Dropdown_input_2']
elif change.new=='Dropdown_input_1':
dependent_drop_down.options=dependent_drop_down_elements['Dropdown_input_1']
drop_down.observe(dropdown_handler, names='value')
# Button
button = widgets.Button(description='Add row to dataframe')
out = widgets.Output()
def on_button_clicked(b):
global df
button.description = 'Row added'
time.sleep(1)
with out:
new_row = {'Dropdown_column': drop_down_input, 'Float_column': float_input}
df = df.append(new_row, ignore_index=True)
button.description = 'Add row to dataframe'
out.clear_output()
display(df)
button.on_click(on_button_clicked)
# Form items
form_items = [
Box([Label(value='Independent dropdown'),
drop_down], layout=form_item_layout),
Box([Label(value='Dependent dropdown'),
dependent_drop_down], layout=form_item_layout)
]
form = Box(form_items, layout=Layout(
display='flex',
flex_flow='column',
border='solid 1px',
align_items='stretch',
width='30%',
padding = '1%'
))
display(form)
display(out)
As an example, this is what the output looks like when you pick Dropdown_input_2 on the first widget:
I want to implement a windrose (with Dash) for displaying the direction of wind data. I am using a callback function and the idea is that the data is displayed for different times - for this I am using a slider, where the time can be selected. The problem here is that after selecting the time, I have to double click on the windrose so that it is updated and displays the data.
I used the same code with a normal line plot, and there it worked fine (meaning that it updated right away without double clicking).
Thanks in advance!
# visit http://127.0.0.1:8050/ in your web browser.
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import pandas as pd
from dash.dependencies import Input, Output
import logging
# Load data
df = pd.read_csv('..\\..\\data\\raw\\fake_wind.csv', #
index_col=0,
parse_dates=True) # finds dates "automatically"
df.index = pd.to_datetime(df['Date']) # convert argument to datetime
# Initialize the app
app = dash.Dash(__name__)
# app.config.suppress_callback_exceptions = True # ??
app.layout = html.Div(
children=[
html.H1("Wind Direction"),
dcc.Slider(id='windrose',
min=1,#min(df.index),
max=5,#max(df.index),
value=5,
marks={
0: '0 °F',
3: '3 °F',
5: '5 °F',
7.65: '7.65 °F',
10: '10 °F'
}
),
html.Div(id='output_container', children=[]),
#html.Br(),
#dcc.Graph(id='sun_map', figure={})
dcc.Graph(id='wind_map2', config={'displayModeBar': False}, animate=True)
])
import numpy as np
#app.callback(
dash.dependencies.Output(component_id='wind_map2', component_property='figure'),
[dash.dependencies.Input('windrose', 'value')])
def update_output(value):
#fig=make_subplots(rows=2, cols=2, specs=[[{'type': 'polar'}]*2]*2)
row = df.iloc[value,:]
barplot = go.Barpolar(
r = [np.random.randint(1,10)],
width=[10],
theta = [np.random.randint(1,360)],
marker_color=["#E4FF87", '#709BFF', '#709BFF', '#FFAA70', '#FFAA70', '#FFDF70', '#B6FFB4'],
marker_line_color="black",
marker_line_width=2,
opacity=0.8
)
fig = go.Figure(data=barplot)
return fig
#return 'You have selected "{}"'.format(value)
fig.update_layout(
xaxis=dict(title="time"),
template='plotly_dark',
paper_bgcolor='rgba(0, 0, 0, 0)',
plot_bgcolor='rgba(0, 0, 0, 0)',
#yaxis="W/s^2",
#yaxis2=dict(title="Celsius",
#overlaying='y',
#side='right'),
font=dict(
family="Courier New, monospace",
size=18,
color="RebeccaPurple"
)
)
return fig
# Run the app
if __name__ == '__main__':
app.run_server(debug=True) # "hot-reloading" (Dash automatically refreshes browser when changes)
might be a bit late to answer. However I think you should change the "updatemode " in your slider from 'mouseup' to drag. As per the docs here https://dash.plotly.com/dash-core-components/slider
updatemode (a value equal to: 'mouseup', 'drag'; default 'mouseup'):
Determines when the component should update its value property. If
mouseup (the default) then the slider will only trigger its value when
the user has finished dragging the slider. If drag, then the slider
will update its value continuously as it is being dragged. If you want
different actions during and after drag, leave updatemode as mouseup
and use drag_value for the continuously updating value.
I'm making a quick heatmap view to help track some user activities using jupyter-dash and plotly express. My colorscale legend doesn't update unless data is completely depopulated/repopulated (with checkbox component not included in the minimal example). I've narrowed it down to the radioitems component filtering I use. It's unclear to me if I'm running into an interaction between dash/plotly or if it's the way I approach creating a week/day specific dataframe for plotting.
I've verified it shows up independent of the mode used in app.run_server(), and the same issue appears when using dash.Dash rather than JupyterDash to initialize the app. Here's a minimal reproduction of the issue with all other components removed.
import datetime
import pandas as pd
import numpy as np
import plotly.express as px
from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import random
def week_from_day(years, days):
assert len(years) == len(days), "The length of years and days series should match."
weeks = [0]*len(years)
for i, (year, day) in enumerate(zip(years, days)):
weeks[i] = datetime.date.isocalendar(datetime.datetime(year, 1, 1) + datetime.timedelta(days=int(day-1)))[1]
return weeks
def date_from_yearday(year_day):
return (datetime.datetime(year_day[0], 1, 1) + datetime.timedelta(days=int(year_day[1]-1))).date()
def fake_dashboard_data(users, days):
data_lists = []
for user in range(1, users):
for day in range(70, days+70):
act_count = random.randint(0,5)
temp_dict = {'user': user,
'year': 2021,
'day': day,
'field1': random.randint(0,2),
'field2': act_count,
'field3': random.randint(0,act_count),
'field4': random.randint(0,act_count)
}
data_lists.append(temp_dict)
fake_df = pd.DataFrame.from_dict(data_lists)
fake_df['date'] = fake_df[['year', 'day']].apply(date_from_yearday, axis=1)
return fake_df
data = fake_dashboard_data(30, 60)
#Initializations needed for dash app
app = JupyterDash(__name__)
app.layout = html.Div([
dcc.RadioItems(
id='week_or_day',
options=[
{'label': 'Weekly', 'value': 'week'},
{'label': 'Daily', 'value': 'daily'}],
value='week'),
dcc.Graph(id='user_summary_figure')])
# Define callback to update graph
#app.callback(
Output('user_summary_figure', 'figure'),
Input('week_or_day', 'value'))
def filter_heatmap(week_or_day):
#conditional DF formatting based on radio button
if week_or_day == 'week':
#Don't modify original data within callbacks.
summary_df = data.copy()
#Add week field and drop day.
summary_df['week'] =week_from_day(summary_df['year'], summary_df['day'])
summary_df.drop(['day', 'date'], axis=1, inplace=True)
#melt into long form
summary_df = summary_df.melt(id_vars=['user', 'year', 'week'], var_name='response_type', value_name='count')
#sort and create a year-week column before returning
summary_df = summary_df.reset_index().sort_values(['year', 'week', 'user'])
summary_df.insert(0, 'time_period', summary_df['year'].astype(str) + ' - ' + summary_df['week'].astype(str))
#week plot formatting
bins_time = summary_df['week'].nunique()
else: #day formatting
#Don't modify original data within callbacks.
summary_df = data.copy()
#melt into long form for plotly
summary_df = summary_df.melt(id_vars=['user', 'year', 'day', 'date'], var_name='response_type', value_name='count')
#sort and create a year-day column before returning
summary_df = summary_df.reset_index().sort_values(['year', 'day', 'user'])
summary_df.insert(0, 'time_period', summary_df['year'].astype(str) + ' - ' + summary_df['day'].astype(str))
#week plot formatting
bins_time = summary_df['day'].nunique()
#need user column that cannot be forced to any date type. Dash will force conversion
summary_df.insert(0, 'user_str', 'user: ' + summary_df['user'].astype(str))
user_summary_fig = px.density_heatmap(summary_df,
x='time_period',
y='user_str',
z='count',
nbinsx=bins_time,
histfunc='sum',
color_continuous_scale='RdYlGn')
return user_summary_fig
app.run_server(mode='inline', debug=False)
Tried a number of different things. The filtering issues were ultimately unrelated to which filtering component was being used. Dropdowns, date selectors and checkboxes all had the same issues.
resolving it was as easy as adding an update_traces call as the last step of the callback, specifying the colorscale.
fig.update_traces(dict(colorscale='Tealrose_r', coloraxis=None))
I would like to change the DataTable object row selection programmatically (without JS, just python). I have tried to so using the selected property of the underlying ColumnsSource with no success. How can this be done?
See an example app (needs bokeh serve to run) where pressing the button changes the selected rows, then updates both a table and plot. Is this all the functionality you need?
By the way you could just do it in JS and not need to use bokeh server, but if you have more python functionality going on then i guess you need it.
from datetime import date
from random import randint
from bokeh.io import output_file, show, curdoc
from bokeh.plotting import figure
from bokeh.layouts import widgetbox, row
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn,Button
output_file("data_table.html")
data = dict(
dates=[date(2014, 3, i+1) for i in range(10)],
downloads=[randint(0, 100) for i in range(10)],
)
def update():
#set inds to be selected
inds = [1,2,3,4]
source.selected = {'0d': {'glyph': None, 'indices': []},
'1d': {'indices': inds}, '2d': {}}
# set plot data
plot_dates = [data['dates'][i] for i in inds]
plot_downloads = [data['downloads'][i] for i in inds]
plot_source.data['dates'] = plot_dates
plot_source.data['downloads'] = plot_downloads
source = ColumnDataSource(data)
plot_source = ColumnDataSource({'dates':[],'downloads':[]})
table_button = Button(label="Press to set", button_type="success")
table_button.on_click(update)
columns = [
TableColumn(field="dates", title="Date", formatter=DateFormatter()),
TableColumn(field="downloads", title="Downloads"),
]
data_table = DataTable(source=source, columns=columns, width=400, height=280)
p = figure(plot_width=400, plot_height=400)
# add a circle renderer with a size, color, and alpha
p.circle('dates','downloads',source=plot_source, size=20, color="navy", alpha=0.5)
curdoc().add_root(row([table_button,data_table,p]))
You can select DataTable rows programmatically in python in this way:
source.selected.indices = [list of indices to be selected]
where source is the ColumnDataSource for the DataTable. If you have any callbacks for the source.selected here, remember to select the rows only after registering the callbacks so that they will get called.
Just for clarity you have to replace the source.selected property completely to trigger the changes. So the important line is:
source.selected = {'0d': {'glyph': None, 'indices': []},
'1d': {'indices': inds}, '2d': {}}
Individually setting the items in source.selected doesn't work
source.selected['1d']['indices'] = inds # Doesn't work