I would like to use the excellent altair library to create dashboards. Is there a way to create the dashboards and not show any code? I see some really nice examples here: https://altair-viz.github.io/case_studies/exploring-weather.html but the code is visible too. Also, what is the best (well tested/easy to use) frontend for altair? Colab? Jupyter?
Any Altair chart can be saved as HTML using chart.save("filename.html"). If you open the resulting file with a web browser, you'll see the chart without any of the associated Python code.
Alternatively, you can use chart.to_json() to get out the JSON chart specification, which can then be embedded in any web page using vega-embed... this is exactly what is done in the page exported by chart.save.
As to your second question (please in the future try to limit your StackOverflow posts to a single question): Altair works with JupyterLab, Jupyter notebook, CoLab, nteract, and Hydrogen. You can use any of these frontends, though some require some extra setup. See https://altair-viz.github.io/getting_started/installation.html for details. I use JupyterLab, and would suggest starting with that.
In addition to creating standalone HTML files and using vega-embed, Altair is also compatible with common dashboarding packages such as Panel, Streamlit, and Dash. I have provided a simple example for each below:
Panel
Panel works both in the notebook and as a standalone dashboard. It also lets you embed dashboards in single HTML files.
import altair as alt
from vega_datasets import data
import panel as pn
from panel.interact import interact
pn.extension('vega')
cars = data.cars()
def scatter_plot(x_column):
chart = alt.Chart(cars).mark_point().encode(
x=x_column,
y='Displacement')
return chart
interact(scatter_plot, x_column=cars.select_dtypes('number').columns)
The capability to listen to Vega events and define custom callback for e.g. selected points was recently merged in Panel and is included in the 0.13 release! This is the only Python dashboarding package that supports custom callbacks on selections in Altair charts. Here is an example from the docs:
penguins_url = "https://raw.githubusercontent.com/vega/vega/master/docs/data/penguins.json"
brush = alt.selection_interval(name='brush') # selection of type "interval"
chart = alt.Chart(penguins_url).mark_point().encode(
x=alt.X('Beak Length (mm):Q', scale=alt.Scale(zero=False)),
y=alt.Y('Beak Depth (mm):Q', scale=alt.Scale(zero=False)),
color=alt.condition(brush, 'Species:N', alt.value('lightgray'))
).properties(
width=250,
height=250
).add_selection(
brush
)
vega_pane = pn.pane.Vega(chart, debounce=10)
vega_pane
df = pd.read_json(penguins_url)
def filtered_table(selection):
if not selection:
return '## No selection'
query = ' & '.join(
f'{crange[0]:.3f} <= `{col}` <= {crange[1]:.3f}'
for col, crange in selection.items()
)
return pn.Column(
f'Query: {query}',
pn.pane.DataFrame(df.query(query), width=600, height=300)
)
pn.Row(vega_pane, pn.bind(filtered_table, vega_pane.selection.param.brush))
Streamlit
Streamlit aims to be as close as possible to a regular Python script without having to focus programming a front end.
from vega_datasets import data
import streamlit as st
import altair as alt
cars = data.cars()
x_column = st.selectbox('Select x-axis column', cars.select_dtypes('number').columns)
chart = alt.Chart(cars).mark_point().encode(
x=x_column,
y='Displacement')
st.altair_chart(chart, use_container_width=True)
Dash
Dash is more verbose than the other two as it requires you to be explicit about many things in the frontend and the callbacks (which also gives more fine-grained control). There is no specific object for Altair graphs, so we're plugging the HTML plot into an iframe (I've asked about this).
import altair as alt
from vega_datasets import data
import dash
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output
cars = data.cars()
# Setup app and layout/frontend
app = dash.Dash(__name__, external_stylesheets=['https://codepen.io/chriddyp/pen/bWLwgP.css'])
app.layout = html.Div([
dcc.Dropdown(
id='x_column-widget',
value='Miles_per_Gallon', # REQUIRED to show the plot on the first page load
options=[{'label': col, 'value': col} for col in cars.columns]),
html.Iframe(
id='scatter',
style={'border-width': '0', 'width': '100%', 'height': '400px'})])
# Set up callbacks/backend
#app.callback(
Output('scatter', 'srcDoc'),
Input('x_column-widget', 'value'))
def plot_altair(x_column):
chart = alt.Chart(cars).mark_point().encode(
x=x_column,
y='Displacement',
tooltip='Horsepower').interactive()
return chart.to_html()
if __name__ == '__main__':
app.run_server(debug=True)
In addition to what suggested by #jakevdp, I found really useful to export the chart definition in json format and render it within the (still beta) Observable Notebook from Mike Bostock: graphs/interactions are produced with altair while boilerplate UI are easily added in plain HTML or javascript in a "reactive" environment (i.e. ... cells are automatically re-evaluated in topological order whenever their inputs change). The code is almost entirely hidden there, and, at the same time, you could exploit the idea of "computational essay" that has made Jupyter so popular. Create a reasonably complex and clean UI/Dashboard was for me easier there than using Jupyter + widgets and, thanks to altair, without the effort to program "complex" charts by hand.
Related
Here's a minimal example. It works perfectly in a Jupyter notebook, but when the html file is opened in a browser, clicking the plot produces no output.
If embed_minimal_html is called after the widget has been used in the Jupyter notebook several times and output is present, the same output will appear in the opened html file (without the black border for some reason), but additional clicks will not produce additional output.
import plotly.graph_objects as go
import ipywidgets as widgets
from ipywidgets.embed import embed_minimal_html
fig = go.FigureWidget(data=[go.Scatter(x=[1, 2], y=[1, 2])])
scatter = fig.data[0]
output = widgets.Output(layout={'border': '1px solid black'})
#scatter.on_click
def output_point(_, points, __):
with output:
print("Point: ({}, {})".format(points.xs[0], points.ys[0]))
minimal_onclick_widget = widgets.VBox([fig, output])
embed_minimal_html('minimal_onclick_widget.html', views=[minimal_onclick_widget], title="Minimal on_click widget")
minimal_onclick_widget
Any ideas what's going on, or how to fix it?
I think it might have something to do with server-side versus client-side event handling. The link and dlink widgets go through the Python kernel (server-side) and don't work in static html embeddings like the one above, but they have client-side alternatives jslink and jsdlink that do: https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html#Linking-widgets-attributes-from-the-client-side. It's not stated explicitly in the docs, but maybe on_click is a server-side event with no client-side alternative.
I am trying to build a grid plot that updates based on value selected from 'Select' widget using Bokeh.
The graph works but there is no interaction between the widget and the graph. I am not sure how to do this. The goal is to use the 'Select' to update dfPlot then follow the remaining steps.
Here is what i have so far:
output_file('layout.html')
select = Select(title="Option:", options= list(dfExpense['Ident'].unique()), value= "VALUE")
def update_plot(attr, old, new):
dfPlot = dfExpense[dfExpense['Ident'] == select.value]
select.on_change('value', update_plot)
d = []
for x in dfPlot['Account'].unique():
d.append(f's_{x}')
plt = []
for i, x in enumerate(dfPlot['Account'].unique()):
dftemp = dfPlot[dfPlot['Account']==gl]
source1 = ColumnDataSource(dftemp)
d[i] = figure(plot_width = 250, plot_height = 250)
d[i].circle('X', 'Amount', source = source1)
plt.append(d[i])
grid= gridplot([i for i in plt], ncols = 6)
l = row(grid, select)
show(l)
curdoc().add_root(l)
Thanks!
Someone else will probably give you a better answer. I'll just say, I think you might be doing things completely wrong for what you are trying to do (I did the same thing when starting to work with Bokeh).
My understanding after a bit of experience with Bokeh, as it relates to your problem, is as follows:
Using curdoc to make an interactive widget based Bokeh plot means you are using Python to interact with the plot, meaning that you must use a Bokeh server, not just use a .html file. (as a corollary, you won't be using show or output file) https://docs.bokeh.org/en/latest/docs/user_guide/server.html
You can still make a standalone .html file and make it have interactive widgets like sliders, but you will have to write some Javascript. You'll most likely want to do this by utilizing CustomJS within Bokeh, which makes it relatively easy.
https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html
I had a similar problem, wanting interactivity without using a Python Bokeh server. CustomJS ended up serving my needs quite well, and even though I'm a novice at Javascript, they make it pretty easy (well, especially if your problem is similar to the examples, it can get tricky otherwise but still not very hard).
The real (general) question
I am new to Bokeh and I am trying to build a plot which can be dynamically updated based on input provided by a widget. However, usage of Python callbacks is not thoroughly documented for most widgets and therefore I'm stuck.
How can I know which widget method I should use to attach my callback? I can guess the available choices by probing the widgets attributes in an interactive console, but that's not elegant and I'm sure it's written somewhere in the documentation.
Provided that I would know about the method to use (e.g. on_event or on_change), I still have to figure out its signature and arguments. For instance, if I'm using on_change, which widget attributes can I monitor?
Once I know which attribute I can monitor, how can I know the data structure which will be yielded by the event?
Some more context and the (not-as-useful) specific question
Here is an appropriate example. I am using a notebook-embedded server like in this example. As an exercise, I would like to replace the slider with a DataTable with arbitrary values. Here is the code I currently have:
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, DataTable
from bokeh.plotting import figure
from bokeh.io import show, output_notebook
from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature
output_notebook()
def modify_doc(doc):
df = sea_surface_temperature.copy()
source = ColumnDataSource(data=df)
source_table = ColumnDataSource(data={"alpha": [s for s in "abcdefgh"],
"num": list(range(8))})
plot = figure(x_axis_type='datetime', y_range=(0, 25),
y_axis_label='Temperature (Celsius)',
title="Sea Surface Temperature at 43.18, -70.43")
plot.line('time', 'temperature', source=source)
def callback(attr, old, new):
# This is the old callback from the example. What is "new" when I use
# a table widget?
if new == 0:
data = df
else:
data = df.rolling('{0}D'.format(new)).mean()
source.data = ColumnDataSource(data=data).data
table = DataTable(source=source_table,
columns=[TableColumn(field="alpha", title="Alpha"),
TableColumn(field="num", title="Num")])
# How can I attach a callback to table so that the plot gets updated
# with the "num" value when I select a row?
# table.on_change("some_attribute", callback)
doc.add_root(column(table, plot))
show(modify_doc)
This answer was given for Bokeh v1.0.4 and may not be compliant with the latest documentation
JavaScript callbacks and Python callbacks, are very powerful tools in Bokeh and can be attached to any Bokeh model element. Additionally you can extend Bokeh functionality by writing your own extensions with TypeScript (eventually compiled to JS)
JS callbacks can be added using either of both methods:
Model.js_on_event('event', callback)
Model.js_on_change('attr', callback)
Python callbacks are mainly used for widgets:
Widget.on_event('event, onevent_handler)
Widget.on_change('attr', onchange_handler)
Widget.on_click(onclick_handler)
The exact function signature for event handlers very per widget and can be:
onevent_handler(event)
onchange_handler(attr, old, new)
onclick_handler(new)
onclick_handler()
The attr can be any widget class (or it's base class) attribute. Therefore you need always to consult the Bokeh reference pages. Also expanding the JSON Prototype helps to find out which attributes are supported e.g. looking at Div we cannot see directly the id, name, style or text attributes which come from its base classes. However, all of these attributes are present in the Div's JSON Prototype and hence are supported by Div:
{
"css_classes": [],
"disabled": false,
"height": null,
"id": "32025",
"js_event_callbacks": {},
"js_property_callbacks": {},
"name": null,
"render_as_text": false,
"sizing_mode": "fixed",
"style": {},
"subscribed_events": [],
"tags": [],
"text": "",
"width": null
}
Coming back to your question: Many times you can achieve the same result using different approaches.
To my knowledge, there is no nice method that lists all supported events per widget but reading documentation and digging into the base classes helps a lot.
Using methods described above it is possible to check which widget attributes you can use in your callbacks. When it comes to events I advice you to look at and explore the bokeh.events class in your IDE. You can find there extended description for every event. In time it will come naturally when using your programmer's intuition to select the right event that your widget supports (so no button_click for Plot and no pan event for Button but the other way around).
Decision to which widget (model element) attach the callback and which method to choose or to which event bound the callback is yours and depends mainly on: which user action should trigger your callback?
So you can have a JS callback attached to any widget (value change, slider move, etc...), any tool (TapTool, HoverTool, etc...), data_source (clicking on glyph), plot canvas (e.g. for clicks on area outside a glyph) or plot range (zoom or pan events), etc...
Basically you need to know that all Python objects have their equivalents in BokehJS so you can use them the same way in both domains (with some syntax differences, of course).
This documentation shows for example that ColumnDataSource has a "selected" property so for points you can inspect source.selected.indices and see which point on the plot are selected or like in your case: which table rows are selected. You can set a breakpoint in code in Python and also in the browser and inspect the Python or BokehJS data structures. It helps to set the environment variable BOKEH_MINIFIED to no either in you IDE (Run Configuration) or in Terminal (e.g. BOKEH_MINIFIED=no python3 main.py) when running your code. This will make debugging the BokehJS in the browser much easier.
And here is your code (slightly modified for "pure Bokeh" v1.0.4 as I don't have Jupiter Notebook installed)
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, DataTable, TableColumn
from bokeh.plotting import figure, curdoc
from bokeh.io import show, output_notebook
from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature
# output_notebook()
def modify_doc(doc):
df = sea_surface_temperature.copy()
source = ColumnDataSource(data = df)
source_table = ColumnDataSource(data = {"alpha": [s for s in "abcdefgh"],
"num": list(range(8))})
plot = figure(x_axis_type = 'datetime', y_range = (0, 25),
y_axis_label = 'Temperature (Celsius)',
title = "Sea Surface Temperature at 43.18, -70.43")
plot.line('time', 'temperature', source = source)
def callback(attr, old, new): # here new is an array containing selected rows
if new == 0:
data = df
else:
data = df.rolling('{0}D'.format(new[0])).mean() # asuming one row is selected
source.data = ColumnDataSource(data = data).data
table = DataTable(source = source_table,
columns = [TableColumn(field = "alpha", title = "Alpha"),
TableColumn(field = "num", title = "Num")])
source_table.selected.on_change('indices', callback)
doc().add_root(column(table, plot))
modify_doc(curdoc)
# show(modify_doc)
Result:
I've been looking into using the Bokeh library to create animated data visualisations for some small projects I am working on. I feel that the gif format would be the best file format to export these visualisations in. It is widely used and they should be easy to share.
Is it possible (and advisable) to export bokeh animated plots in the gif format?
If so, will I need to make use of any additional tools to do this?
If not, is there a different file format that would be better suited to this?
I found this thread about potential options for creating gifs in Python, but I'm not sure if any of them are relevant to the Bokeh library. Programmatically generate video or animated GIF in Python?
Any help would be much appreciated. Many thanks.
Bokeh plot has a SaveTool which allows you to save the plot canvas manually in PNG format but this would be a lot of work for you to do. Alternatively you could automate this process by implementing Bokeh server app with update() function that updates the data_source property of your plot e.g. each second and saves a screenshot using export_png() function. Then you could use those images to build an animation e.g. using the Python lib you mentioned above.
This is an example script to run with bokeh serve --show app.py:
The content of app.py:
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, curdoc
from datetime import datetime
from bokeh.io import export_png
import random
source = ColumnDataSource(dict(time = [datetime.now()], value = [random.randint(5, 10)]))
plot = figure(plot_width = 1200, x_axis_type = 'datetime', tools = 'pan,box_select,crosshair,reset,save,wheel_zoom')
plot.line(x = 'time', y = 'value', line_color = 'black', source = source)
counter = 0
def update():
global counter
new_source_data = dict(time = [datetime.now()], value = [random.randint(5, 10)])
source.stream(new_source_data)
counter = counter + 1
export_png(plot, filename = "plot_" + str(counter) + ".png")
curdoc().add_root(plot)
curdoc().add_periodic_callback(update, 1000)
To make this script work you need to have phantomjs installed on your system. So first you need to install nodejs and npm, then install phantomjs like this:
sudo npm install -g phantomjs-prebuilt
If you are using Mac then another option is to use QuickTime Player screen recording to make a movie and then convert it into an animated gif using FFMPEG like explained in this post
I'm attempting to connect a datatable with a multiselect widget in bokeh. I've searched around and gathered that I need to develop a function to update the data source for the data table, but I seem to have two problems.
I cannot seem to access the value of the multiselect object after I click it.
I cannot seem to push the change to the notebook after receiving the change.
Here's an example of my code:
import pandas as pd
from bokeh.io import push_notebook
from bokeh.plotting import show, output_notebook
from bokeh.layouts import row
from bokeh.models.widgets import MultiSelect, DataTable, TableColumn
from bokeh.models import ColumnDataSource
output_notebook()
df=pd.DataFrame({'year':[2000,2001,2000,2001,2000,2001,2000,2001],
'color':['red','blue','green','red','blue','green','red','blue'],
'value':[ 0,1,2,3,4,5,6,7]})
columns=[TableColumn(field=x, title=x) for x in df.columns]
source=ColumnDataSource(df)
data_table=DataTable(source=source,columns=columns)
years=[2000,2001,2000,2001,2000,2001,2000,2001]
## MultiSelect won't let me store an integer value, so I convert them to strings
multi=MultiSelect(title="Select a Year", value=['2000','2001'],options=[str(y) for y in set(years)])
def update(attr,old, new):
yr=multi.value
yr_vals=[int(y) for y in yr]
new_data=df[df.year.isin(yr_vals)]
source.data=new_data
push_notebook(handle=t)
multi.on_change('value',update)
t=show(row(multi,data_table),notebook_handle=True)
push_notebook is uni-directional. That is, you can only push changes from the IPython kernel, to the JavaScript front end. No changes from the UI are propagated back to the running Python kernel. In other words, on_change is not useful (without more work, see below) If you want that kind of interaction, there are a few options:
Use ipywidgets with push_notebook. Typically this involved the interact function to automatically generate a simple UI with callbacks that use push_notebook to update the plots, etc. based on the widget values. Just to be clear, this approach uses ipywidgets, which are not Bokeh built-in widgets. You can see a full example notebook here:
https://github.com/bokeh/bokeh/blob/master/examples/howto/notebook_comms/Jupyter%20Interactors.ipynb
Embed a Bokeh server application. The Bokeh server is what makes it possible for on_change callbacks on Bokeh widgets to function. Typically this involves making a function that defines the app (by specifying how a new document is created):
def modify_doc(doc):
df = sea_surface_temperature.copy()
source = ColumnDataSource(data=df)
plot = figure(x_axis_type='datetime', y_range=(0, 25))
plot.line('time', 'temperature', source=source)
def callback(attr, old, new):
if new == 0:
data = df
else:
data = df.rolling('{0}D'.format(new)).mean()
source.data = ColumnDataSource(data=data).data
slider = Slider(start=0, end=30, value=0, step=1, title="Smoothing by N Days")
slider.on_change('value', callback)
doc.add_root(column(slider, plot))
Then calling show on that function:
show(modify_doc)
A full example notebook is here:
https://github.com/bokeh/bokeh/blob/master/examples/howto/server_embed/notebook_embed.ipynb
(Hacky option) some people have combined CustomJS callbacks with Jupyers JS function kernel.execute to propagate values back to the kernel.