Adding a tooltip to a cell in a Bootstrap Table using Dash - python

I am using Dash to build an application and part of the application is displaying a data table. I convert the table from a pandas dataframe to a Bootstrap Table in Dash using the Table() function from dash-bootstrap-components.
With my table set up
import dash-bootstrap-components as dbc
import dash_html_components as html
import dash
data_table = dbc.Table(# code to construct table)
I'd like to use the dbc.Tooltip to add a tooltip to items (html.td() elements) in my data_table. My first thought was to assign each element in the table an id corresponding to its position and then attach a tooltip
tooltip = dbc.Tooltip("Tooltip text", target = id)
Then we put it all together.
app = dash.Dash(_name_)
app.layout = html.Div(
children = [data_table, tooltip]
)
However, this doesn't seem to be working and I am struggling with how to implement this theoretically and cannot find much help.
UPDATE:
Here is some example code of what I am trying to do. It gives an error, if you remove the sectioned commented "REMOVE TO WORK" the code will function.
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc
import pandas as pd
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
data = {
"Name" : ["Tom", "Jack", "Jill", "Jane"],
"Age" : [21, 30, 45, 80]
}
fixt_df = pd.DataFrame.from_dict(data)
#Setup table
table = dbc.Table(
# Header
[html.Thead([html.Tr([html.Th(col) for col in fixt_df.columns])])] +
# Body
[html.Tbody([html.Tr([html.Td(html.A(fixt_df.iloc[i][col], id = ""+str(i)+"_" + col))
for col in fixt_df.columns])
for i in range(len(fixt_df))])]
,
hover = True,
className = "table-responsive table-hover"
)
items = [table]
# REMOVE TO WORK
for col in fixt_df.columns:
for i in range(len(fixt_df)):
items.extend([dbc.Tooltip("test", target = str(i)+"_"+col)])
# REMOVE TO WORK
app.layout = html.Div(
children = [
#header
html.Div(
html.H1("Example")
),
#table
html.Div(
items
)
]
)
if __name__ == '__main__':
app.run_server(debug=True)

I found your problem - for some reason the dbc.Tooltip element doesn't play well with elements that have IDs starting with a number.
To overcome this, in the element IDs and tooltip targets simply change:
str(i)+"_"+col
to:
col+"_"+str(i)
Alternatively, you can add a letter prefix:
"p_"+str(i)+"_"+col

Related

Filter a Dash data table via callback without having to re-send the data?

In Dash, I'm trying to filter the data in response to another component (e.g. dropdown), without the user clicking on the table. In addition, I don't want to filter the data with pandas behind the scenes.
I'm looking for a table property that takes a filtering instruction if you will, and acts on already-loaded data.
I fond a solution by using the Dash data table 'filter_query' property.
Just send the query in string form, and to turn off the filters send another empty string.
Example string to filter column 'Name' where entries contain 'Ana':
"{Name} contains 'Ana'"
This way you don't need to slice the data with pandas, just use the native Dash data table filtering capabilities.
Minimal Dash example with a table and a button to filter/unfilter data via 'query_string'.
import dash
from dash import dcc, dash_table, html, dcc
from dash.dependencies import Input, Output
from dash.exceptions import PreventUpdate
import pandas as pd
#Initialize Dash
#––––––––––––––––––––––––––––––––––––––––––––––––––
app = dash.Dash(__name__,)
#Data
#––––––––––––––––––––––––––––––––––––––––––––––––––
data = {'Name': ['Ana', 'nick', 'Tom', 'jack', 'Tom'],
'Age': [19, 21, 19, 18, 32]}
df = pd.DataFrame(data)
#App callbacks (enable interactivity):
#––––––––––––––––––––––––––––––––––––––––––––––––––
#app.callback(
Output('data-table', 'filter_query'),
Output('button-filter-ana','children'),
Input('button-filter-ana', 'n_clicks'),
)
def update_output(n_clicks):
if n_clicks is not None:
if n_clicks%2 != 0 :
return ["({Name} contains 'Ana')","Turn off 'Ana' filter"]
elif n_clicks%2 == 0:
return ["","Only show entries that contain 'Ana'"]
else:
raise PreventUpdate
#App Layout:
#––––––––––––––––––––––––––––––––––––––––––––––––––
app.layout = html.Div(
children=[
dcc.Loading(
id='loading-component',
children=[
dash_table.DataTable(
id='data-table',
data=df.to_dict('records'),
columns=[{"name": i, "id": i} for i in df.columns],
style_table={'height': '300px', 'width': '300px'},
filter_action="native",
sort_mode="single",
),
html.Button('Only show entries that contain Ana', n_clicks=0,id='button-filter-ana'),
]
),
]
)
if __name__ == '__main__':
#Run the app
app.run_server(debug=True)
Gif: Table filtering via 'filter_query'
Here's Dash documentation regarding the filtering syntax:
https://dash.plotly.com/datatable/filtering

Columns not displaying correctly using python dash and mongo db

I am working on a project and after finally getting around some Type errors, I am finally getting my chart to display but it is listing each character in each mongo db json document instead of in proper json form. I am a total noob at this and I cannot seem to get it right so any help that can be offered in getting my interactive chart displayed as well as my pie chart and location chart will be greatly appreciated. I think the problem is derived from how the data is being entered into the dataframe but I can not figure it out.
Here is the read functions that are being used to take the info from the mongo db and send it to the Python script:
def read(self, data):
if data !=None: # checks to make sure that the received data is not null
result = self.database.animals.find(data).limit(35)
return result
else:
raise Exception("Data entered is null!")
def readAll(self, data):
found = self.database.animals.find({}).limit(35)
return(found)
Here is the code for the python script that has imported dash:
from jupyter_plotly_dash import JupyterDash
import dash
import dash_leaflet as dl
import dash_core_components as dcc
import dash_html_components as html
import plotly.express as px
import dash_table
from dash.dependencies import Input, Output
from bson.json_util import dumps,loads
import bson
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pymongo import MongoClient
from AAC import AnimalShelter
###########################
# Data Manipulation / Model
###########################
# FIX ME update with your username and password and CRUD Python module name
username = "aacuser"
password = "Melvin1234!"
shelter = AnimalShelter()
shelter._init_(username,password)
# class read method must support return of cursor object and accept projection json input
df =pd.DataFrame.from_records(list(dumps(shelter.readAll({}))))
dff = pd.DataFrame.from_records(list(dumps(shelter.readAll({}))))
#########################
# Dashboard Layout / View
#########################
app = JupyterDash('SimpleExample')
import base64
app.layout = html.Div([
html.Div(id='hidden-div', style={'display':'none'}),
html.Center(html.B(html.H1('SNHU CS-340 Dashboard'))),
html.Div(className='header',
children=[html.Center(html.Img(src='/home/16514911_snhu/Downloads/Grazioso Salvare Logo.jpg',width="500", height="600")), #image logo
html.Center(html.B(html.H6('Jacqueline Woods')))]), #Unique Handle
html.Br(),
html.Div(className ='row',
children=[
html.Button(id='submit-button-one',n_clicks=0, children='Cats'),
html.Button(id='submit-button-two',n_clicks=0,children='Dogs') ]),
html.Div(dash_table.DataTable(
id='datatable-interactivity',
columns=[
{"name": i, "id": i,"age_upon_outcome":i, 'animal_id':i,'animal_type':i,'breed':i,"color":i,
'date_of_birth':i,'datetime':i,'monthyear':i,'outcome_subtype':i,'outcome_type':i,'sex_upon_outcome':i,
'location_lat':i,'location_long':i,'age_upon_outcome_in_weeks':i,
"deletable": False, "selectable": True} for i in df.columns]
,data=df.to_dict('records'),editable = False,filter_action='native')),
html.Br(),
html.Div(
dcc.Graph( #piechart
id ='graph_id',
figure=list(dumps(shelter.readAll({})))
),
title="Outcome_Type"),
html.Br(),
html.Div( #location map
id='map-id',
className='col s12 m6',
title="Location"
)])
##Interaction Between Components / Controller
#This callback will highlight a row on the data table when the user selects it
#app.callback(
Output('datatable-interactivity',"data"),
[Input('submit-button-one','n_clicks'),
Input('submit-button-two','n_clicks')
]
)
def on_click(bt1, bt2):
if(int(bt1) ==0 and int(bt2) ==0):
df =pd.DataFrame.from_records(shelter.readAll({}))
elif (int(bt1) > int(bt2)):
df = pd.DataFrame(dumps(shelter.read({"animal__type":"Cat"})))
df =pd.DataFrame(dumps(shelter.read({"animal_type":"Dog"})))
return df.to_dict('records')
#app.callback(
Output('datatable-interactivity', 'style_data_conditional'),
[Input('datatable-interactivity', 'selected_columns')]
)
def update_styles(selected_columns):
return [{
'if': { 'column_id': i },
'background_color': '#D2F3FF'
} for i in selected_columns]
#app.callback(
Output('graph_id','figure'),
[Input('datatable-interactivity',"derived_virtual_data")])
def update_Graph(allData):
dff= pd.DataFrame(allData)
pieChart = px.pie(
data_frame=dff,
names=dff['outcome_type'],
hole = 3,
)
return pieChart
#app.callback(
Output('map-id', "children"),
[Input('datatable-interactivity', 'derived_viewport_data'),
Input('datatable-interactivity', 'derived_virtual_selected_rows')
])
def update_map(viewData):
#FIXME Add in the code for your geolocation chart
dff = pd.DataFrame.from_dict(viewData)
dff = df if viewData is None else pd.DataFrame(viewData)
selected_animal = None
if not derived_virtual_selected_rows:
slected_animal = dff.iloc[0]
else:
slected_animal = dff.iloc[derived_vertual_selected_rows[0]]
latitude = selected_animal[12]
longitude =selected_animal[13]
breed = selected_animal[3]
name = selected_animal[0]
# Austin TX is at [30.75,-97.48]
return [
dl.Map(style={'width': '1000px', 'height': '500px'}, center=[30.75,-97.48], zoom=10, children=[
dl.TileLayer(id="base-layer-id"),
# Marker with tool tip and popup
dl.Marker(position=[latitude,longitude], children=[
dl.Tooltip(breed),
dl.Popup([
html.H1("Animal Name"),
html.P(name)
])
])
])
]
app
I was in the same boat as you searching for answers online. I happened to found the answer. Hopefully, this can help others who are experiencing this problem.
#Convert the pymongo cursor into a pandas DataFrame
df = pd.DataFrame(list(shelter.readAll({})))
#Drop the _id column generated by Mongo
df = df.iloc[:, 1:]
After that, you can access the data of the DataFrame by using
df.to_dict('records')

Efficient filtering of GeoJSON components with python dash-leaflet

I'm using the Dash Leaflet map component dash-leaflet for my interactive map visualizations.
My goal is to filter large GeoJSON components (dl.GeoJSON) by values of dash components (e.g. dcc.Slider).
My current approach is the following:
import random
import dash
import dash_html_components as html
import dash_leaflet as dl
import dash_leaflet.express as dlx
import dash_core_components as dcc
from dash.dependencies import Input, Output
# Create some markers.
points = [dict(lat=55.5 + random.random(), lon=9.5 + random.random(), value=random.random()*100) for i in range(100)]
data = dlx.dicts_to_geojson(points)
app = dash.Dash()
app.layout = html.Div([
dl.Map([
dl.TileLayer(),
dl.GeoJSON(id="data-id", data=data)
], center=(56, 10), zoom=8, style={'height': '50vh'}),
html.Div([
html.H5('Filtering'),
dcc.Slider(id='my-slider', min=0, max=100, step=1, value=100),
html.Div(id='slider-output-container')
], style={'width': '30%'}),
])
#app.callback(
Output('slider-output-container', 'children'),
Output('data-id', 'data'),
[Input('my-slider', 'value')])
def update_output(value):
points_new = [p for p in points if p['value'] <= value]
data_new = dlx.dicts_to_geojson(points_new)
return 'You have selected value "{}"'.format(value), data_new
if __name__ == '__main__':
app.run_server()
In this example, the data object of the GeoJSON component "data-id" is filtered with the value of a dash component "my-slider" by filtering the list of points depending on the input value and returning a new geoJSON object created with the dlx.dicts_to_geojson function:
points_new = [p for p in points if p['value'] <= value]
data_new = dlx.dicts_to_geojson(points_new)
Is this the right way to filter a geoJSON object?
I can imagine that there exists better ways to define such a filter function on the client side with the options-feature of the GeoJSON component and a javascript function but I don't know how.
I appreciate any advice/code example I can get.
While it is possible to filter the data in Python, it can introduce significant network overhead depending on the data size (the data is transferred from the server to the client each time the filter changes). If you do the filtering client side, you only have to transfer the data once, i.e. the performance difference can be dramatic.
The client side filtering can be implemented by adding a JavaScript asset (i.e. a .js file placed in the assets folder) with the filtering function as per the documentation,
window.myNamespace = Object.assign({}, window.myNamespace, {
mySubNamespace: {
filter_features: function(feature, context) {
// code should return true if feature is included, otherwise false
const value = context.props.hideout['value']
...
}
}
});
To apply the filtering, pass a function handle to the filter function to the GeoJSON component,
import dash_leaflet as dl
from dash_extensions.javascript import Namespace
...
ns = Namespace("myNamespace ", "mySubNamespace")
dl.GeoJSON(id="geojson", options=dict(filter=ns("filter_features"), ...)
Finally, by making the filter dependent of the hideout prop (as indicated in the example code above), you can achieve interactivity by updating this prop via a callback,
#app.callback(Output("geojson", "hideout"), ...)
def update(...):
...
return {"value": value}
EDIT: As per request in the comments, here is a small self-contained example demonstrating interactive geojson filtering using only client side logic,
import dash_html_components as html
import dash_leaflet as dl
import dash_core_components as dcc
import dash_leaflet.express as dlx
from dash import Dash
from dash.dependencies import Output, Input
from dash_extensions.javascript import assign
# A few cities in Denmark.
cities = [dict(name="Aalborg", lat=57.0268172, lon=9.837735),
dict(name="Aarhus", lat=56.1780842, lon=10.1119354),
dict(name="Copenhagen", lat=55.6712474, lon=12.5237848)]
# Create drop down options.
dd_options = [dict(value=c["name"], label=c["name"]) for c in cities]
dd_defaults = [o["value"] for o in dd_options]
# Generate geojson with a maker for each city and name as tooltip.
geojson = dlx.dicts_to_geojson([{**c, **dict(tooltip=c['name'])} for c in cities])
# Create javascript function that filters on feature name.
geojson_filter = assign("function(feature, context){return context.props.hideout.includes(feature.properties.name);}")
# Create example app.
app = Dash()
app.layout = html.Div([
dl.Map(children=[
dl.TileLayer(),
dl.GeoJSON(data=geojson, options=dict(filter=geojson_filter), hideout=dd_defaults, id="geojson")
], style={'width': '100%', 'height': '50vh', 'margin': "auto", "display": "block"}, id="map"),
dcc.Dropdown(id="dd", value=dd_defaults, options=dd_options, clearable=False, multi=True)
])
# Link drop down to geojson hideout prop (could also be done with a normal callback).
app.clientside_callback("function(x){return x;}", Output("geojson", "hideout"), Input("dd", "value"))
if __name__ == '__main__':
app.run_server()
Note that it requires dash-extensions==0.0.55.

Python Dash refresh page not updating source data

I have written a basic plotly dash app that pulls in data from a csv and displays it on a chart.
You can then toggle values on the app and the graph updates.
However, when I add new data to the csv (done once each day) the app doesn't update the data on refreshing the page.
The fix is normally that you define your app.layout as a function, as outlined here (scroll down to updates on page load). You'll see in my code below that I've done that.
Here's my code:
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import numpy as np
import pandas as pd
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
path = 'https://raw.githubusercontent.com/tbuckworth/Public/master/CSVTest.csv'
df = pd.read_csv(path)
df2 = df[(df.Map==df.Map)]
def layout_function():
df = pd.read_csv(path)
df2 = df[(df.Map==df.Map)]
available_strats = np.append('ALL',pd.unique(df2.Map.sort_values()))
classes1 = pd.unique(df2["class"].sort_values())
metrics1 = pd.unique(df2.metric.sort_values())
return html.Div([
html.Div([
dcc.Dropdown(
id="Strategy",
options=[{"label":i,"value":i} for i in available_strats],
value=list(available_strats[0:1]),
multi=True
),
dcc.Dropdown(
id="Class1",
options=[{"label":i,"value":i} for i in classes1],
value=classes1[0]
),
dcc.Dropdown(
id="Metric",
options=[{"label":i,"value":i} for i in metrics1],
value=metrics1[0]
)],
style={"width":"20%","display":"block"}),
html.Hr(),
dcc.Graph(id='Risk-Report')
])
app.layout = layout_function
#app.callback(
Output("Risk-Report","figure"),
[Input("Strategy","value"),
Input("Class1","value"),
Input("Metric","value"),
])
def update_graph(selected_strat,selected_class,selected_metric):
if 'ALL' in selected_strat:
df3 = df2[(df2["class"]==selected_class)&(df2.metric==selected_metric)]
else:
df3 = df2[(df2.Map.isin(selected_strat))&(df2["class"]==selected_class)&(df2.metric==selected_metric)]
df4 = df3.pivot_table(index=["Fund","Date","metric","class"],values="value",aggfunc="sum").reset_index()
traces = []
for i in df4.Fund.unique():
df_by_fund = df4[df4["Fund"] == i]
traces.append(dict(
x=df_by_fund["Date"],
y=df_by_fund["value"],
mode="lines",
name=i
))
if selected_class=='USD':
tick_format=None
else:
tick_format='.2%'
return {
'data': traces,
'layout': dict(
xaxis={'type': 'date', 'title': 'Date'},
yaxis={'title': 'Values','tickformat':tick_format},
margin={'l': 40, 'b': 40, 't': 10, 'r': 10},
legend={'x': 0, 'y': 1},
hovermode='closest'
)
}
if __name__ == '__main__':
app.run_server(debug=True)
Things I've tried
Removing the initial df = pd.read_csv(path) before the def layout_function():. This results in an error.
Creating a callback button to refresh the data using this code:
#app.callback(
Output('Output-1','children'),
[Input('reload_button','n_clicks')]
)
def update_data(nclicks):
if nclicks == 0:
raise PreventUpdate
else:
df = pd.read_csv(path)
df2 = df[(df.Map==df.Map)]
return('Data refreshed. Click to refresh again')
This doesn't produce an error, but the button doesn't refresh the data either.
Defining df within the update_graph callback. This updates the data every time you toggle something, which is not practicable (my real data is > 10^6 rows, so i don't want to read it in every time the user changes a toggle value)
In short, i think that defining app.layout = layout_function should make this work, but it doesn't. What am I missing/not seeing?
Appreciate any help.
TLDR; I would suggest that you simply load the data from within the callback. If load time is too long, you could change the format (e.g. to feather) and/or reduce the data size via pre processing. If this is still not fast enough, the next step would be to store the data in a server-side in-memory cache such as Redis.
Since you are reassigning df and df2 in the layout_function, these variables are considered local in Python, and you are thus not modifying the df and df2 variables from the global scope. While you could achieve this behavior using the global keyword, the use of global variables is discouraged in Dash.
The standard approach in Dash would be to load the data in a callback (or in the the layout_function) and store it in a Store object (or equivalently, a hidden Div). The structure would be something like
import pandas as pd
import dash_core_components as dcc
from dash.dependencies import Output, Input
app.layout = html.Div([
...
dcc.Store(id="store"), html.Div(id="trigger")
])
#app.callback(Output('store','data'), [Input('trigger','children')], prevent_initial_call=False)
def update_data(children):
df = pd.read_csv(path)
return df.to_json()
#app.callback(Output("Risk-Report","figure"), [Input(...)], [State('store', 'data')])
def update_graph(..., data):
if data is None:
raise PreventUpdate
df = pd.read_json(data)
...
However, this approach will typically be much slower than just reading the data from disk inside the callback (which seems to be what you are trying to avoid) as it results in the data being transferred between the server and client.

Periodically updating figures in plotly

Below is a simple script which retrieves live population data. It updates periodically and updates the plotly figure:
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import dash_bootstrap_components as dbc
# Retrieve data
link = requests.get("https://countrymeters.info/en/World").text
table = pd.read_html(link)[0]
table = table
figure = table.iplot(asFigure=True, kind='bar',xTitle='Source: World',yTitle='Live Pop',title='Population')
# Dash app
app = dash.Dash(external_stylesheets=[dbc.themes.LUX])
# Bootstrap CSS
app.css.append_css({"external_url": "https://codepen.io/amyoshino/pen/jzXypZ.css"})
link = session.get("https://countrymeters.info/en/World").text
app.layout = html.Div(children=[
html.H1("Population Data Scrape"),
html.Div(children=
'''
A summary of the latest population data across the globe.
''' ),
# Graph 1
html.Div([
dcc.Tabs([ #Tab 1
dcc.Tab(label="Population Data", children=[
html.Div([
dcc.Graph(
id = "Data",
figure = table.iplot(asFigure=True, kind='bar',xTitle='Source: World',yTitle='Live Pop',title='Population')
),
dcc.Interval(
id="4secinterval",
interval="4000",
n_intervals=0
)], className = "six columns"),
]),
])
])
])
# Update graph
#app.callback(Output("Data", "figure"),
[Input("4secinterval", "n_intervals")])
def draw_figure(n):
test = session.get("https://countrymeters.info/en/World").text
table = pd.read_html(test)[0]
table = table
figure = table.iplot(asFigure=True, kind='bar',xTitle='Source: World',yTitle='Live Pop',title='Population')
return figure
if __name__ == "__main__":
app.run_server(debug=False)
In the "update graph" section of my code, for the graph to update I have to call the web scrape again to retrieve the latest data and define it in a whole function. I've tried defining the function before and using:
#app.callback(Output("Data", "figure"),
[Input("4secinterval", "n_intervals")])
draw_figure(n)
which I was hoping would just return the figure, however, this doesn't work. Is there a way in plotly/Dash to update the figure in a shorter way (i.e without having to scrape and format the data all over again)?
The catch here is in the dcc.Graph section. You are calling a global variable table.iplot() where table is defined as a global variable in your retrieve section.
Try to put all the functions in a separate file say `useful_functions.py'
def draw_graph():
link = requests.get("https://countrymeters.info/en/World").text
table = pd.read_html(link)[0]
table = table
figure = table.iplot(asFigure=True, kind='bar',xTitle='Source: World',yTitle='Live Pop',title='Population')
return figure
the_graph = draw_graph()
Now, in your main file as above, remove the global declaration of table and figure. To display the graph, in your graph section call the draw_graph() function as:
import useful_functions as uf
<rest of the html app code>
dcc.Graph(
id = "graph_id",
figure = uf.the_graph
),
This will call the graph for the first time on load. Now for the refresh bit, the callback would look like:
#app.callback(Output("Data", "figure"),
[Input("4secinterval", "n_intervals")])
def draw_figure(n):
fig = uf.draw_graph()
return fig

Categories