How to make a Bokeh scatter plot interactive (with slider) - python

I'm trying to make a scatter plot in Bokeh based on the simple example code posted here.
The following code produces a working demo for a line plot:
from bokeh.layouts import column
from bokeh.models import CustomJS, ColumnDataSource, Slider
from bokeh.plotting import Figure, show
# fetch and clear the document
from bokeh.io import curdoc
curdoc().clear()
x = [x*0.005 for x in range(0, 100)]
y = x
source = ColumnDataSource(data=dict(x=x, y=y))
plot = Figure(plot_width=400, plot_height=400)
plot.line(x='x', y='y', source=source)
def callback(source=source, window=None):
data = source.data
f = cb_obj.value
x, y = data['x'], data['y']
for i in range(len(x)):
y[i] = window.Math.pow(x[i], f)
source.trigger('change')
slider = Slider(start=0.1, end=4, value=1, step=.1, title="Start week",
callback=CustomJS.from_py_func(callback))
layout = column(slider, plot)
show(layout)
It looks like this:
In this demo, when you adjust the slider and press the 'reset' icon, the plot re-draws itself based on the updated formula for y=f(x).
However, I want to make a scatter plot that changes, not a line plot.
Problem:
When I simply change plot.line in above code to plot.circle, the plot renders okay but it is static - it does not change when you shift the slider and press 'reset'. No error messages that I can see.

I found the answer in the documentation.
The final line in callback should be source.change.emit() not source.trigger('change'). I do not know the difference between these two but the latter works with circle plots.
I.e.
def callback(source=source, window=None):
data = source.data
f = cb_obj.value
x, y = data['x'], data['y']
for i in range(len(x)):
y[i] = window.Math.pow(x[i], f)
source.change.emit()

Related

Separate range sliders for multiple y axes in bokeh

I have multiple line plots to draw on a single figure and I am doing this using bokeh.plotting. Using
p0.line(),
p0.extra_y_ranges(),
and
p0.add_layout(LinearAxes())
p0 being 1 bokeh figure.
I would like to have range_sliders for each y axis separately on the right. Would this be possible using bokeh?
For the primary y axis, the range slider works fine using
# set up RangeSlider y_counts
range_slider_c = RangeSlider(
title="c",
start=-10,
end=400,
step=1,
value=(-1, 300),
height = 250,
orientation='vertical',
show_value= False,
direction = 'rtl'
)
range_slider_c.js_link("value", p0.y_range, "start", attr_selector=0)
range_slider_c.js_link("value", p0.y_range, "end", attr_selector=1)
But I am unclear about how to call the additional y axes's ranges like I did for p0.y_range in js.link.
I hope I have been able to explain my requirements properly.
This answers is based on the answer on your previous question.
As it is mentioned there, the extra y-ranges are saved in a dictionary with the keyword extra_y_ranges. Therefor you only have to change the js_link using extra_y_ranges with a valid name. For example range_slider_c.js_link("value", p0.extra_y_ranges["c", "start", attr_selector=0).
Complete minimal example
from bokeh.layouts import column
from bokeh.models import LinearAxis, Range1d, CustomJS, RangeSlider
from bokeh.plotting import figure, show, output_notebook
output_notebook()
data_x = [1,2,3,4,5]
data_y = [1,2,3,4,5]
color = ['red', 'green', 'magenta', 'black']
p = figure(plot_width=500, plot_height=300)
p.line(data_x, data_y, color='blue')
range_sliders = []
for i, c in enumerate(color, start=1):
name = f'extra_range_{i}'
lable = f'extra range {i}'
p.extra_y_ranges[name] = Range1d(start=0, end=10*i)
p.add_layout(LinearAxis(axis_label=lable, y_range_name=name), 'right')
p.line(data_x, data_y, color=c, y_range_name=name)
range_slider = RangeSlider(start=0, end=10*i, value=(1,9*i), step=1, title=f"Slider {lable}")
range_slider.js_link("value", p.extra_y_ranges[name] , "start", attr_selector=0)
range_slider.js_link("value", p.extra_y_ranges[name] , "end", attr_selector=1)
range_sliders.append(range_slider)
show(column(range_sliders+[p]))
Output
Comment
To stack the sliders and the figure i use the column layout, which takes a list of bokeh objects. Other layouts are available.

how to add secondary x-axis with my String labels

I have plot
with secondary axis added like this:
plot.extra_x_ranges['sec_x_axis'] = Range1d(0, 100)
ax2 = LinearAxis(x_range_name="sec_x_axis", axis_label="secondary x-axis")
plot.add_layout(ax2, 'above')
x_axis is x_axis_type='datetime', so bokeh show ms on second x-axis too. This is not good.
Is there a way I can put my labels on this axis? I have a list of str labels like:
my_labels = ['21.5; 315.1', '21.6; 315.0', '21.7; 315.0', '21.7; 314.9',.....]
I found FuncTickFormatter but it takes JS code inside, so I can't handle it.
Maybe there is another way to do this?
To override the values of the labels use major_label_overrides on the appropriate axis. You can pass a dictionary like {1:'A', ...}, where 1 is the place to overwrite and A is the new label.
To avoid "wrong" labels while zooming, you can set the ticker direcetlly as list unsing ticker.
In your case the axis is p.above[0].
Comment
If you add a LinearAxis to a figure with an already existing DatetimeAxis, the new axis shoudn't be effected and therefor shouldn't be formatted as datetime. I used the latest version 2.4.3 and it works as expected. Use the minimal example to try it on your own.
Minimal Example
This code is based on the twin_axis.py example published by the authors of bokeh.
from numpy import arange, linspace, pi, sin
from bokeh.models import LinearAxis, Range1d
from bokeh.plotting import figure, show, output_notebook
output_notebook()
x = arange(-2*pi, 2*pi, 0.2)
x2 = arange(-pi, pi, 0.1)
y = sin(x)
y2 = sin(x2)
p = figure(
width=400,
height=400,
x_range=(-6.5, 6.5),
y_range=(-1.1, 1.1),
min_border=80,
x_axis_type="datetime"
)
p.circle(x, y, color="crimson", size=8)
p.yaxis.axis_label = "red circles"
p.yaxis.axis_label_text_color ="crimson"
p.extra_x_ranges['foo'] = Range1d(-pi, pi)
p.circle(x2, y2, color="navy", size=8, x_range_name="foo")
ax2 = LinearAxis(x_range_name="foo", axis_label="blue circles")
ax2.axis_label_text_color ="navy"
p.add_layout(ax2, 'above')
# set ticker to avoid wrong formatted labels while zooming
p.above[0].ticker = list(range(-3,4))
# overwrite labels
p.above[0].major_label_overrides = {key: item for key, item in zip(range(-3,4), list('ABCDEFG'))}
show(p)
default
overwritten labels

python bokeh: update scatter plot colors on callback

I only started to use Bokeh recently. I have a scatter plot in which I would like to color each marker according to a certain third property (say a quantity, while the x-axis is a date and the y-axis is a given value at that point in time).
Assuming my data is in a data frame, I managed to do this using a linear color map as follows:
min_q = df.quantity.min()
max_q = df.quantity.max()
mapper = linear_cmap(field_name='quantity', palette=palettes.Spectral6, low=min_q, high=max_q)
source = ColumnDataSource(data=get_data(df))
p = figure(x_axis_type="datetime")
p.scatter(x="date_column", y="value", marker="triangle", fill_color=mapper, line_color=None, source=source)
color_bar = ColorBar(color_mapper=mapper['transform'], width=8, location=(0,0))
p.add_layout(color_bar, 'right')
This seems to work as expected. Below is the plot I get upon starting the bokeh server.
Then I have a callback function update() triggered upon changing value in some widget (a select or a time picker).
def update():
# get new df (according to new date/select)
df = get_df()
# update min/max for colormap
min_q = df.quantity.min()
max_q = df.quantity.max()
# I think I should not create a new mapper but doing so I get closer
mapper = linear_cmap(field_name='quantity', palette=palettes.Spectral6 ,low=min_q, high=max_q)
color_bar.color_mapper=mapper['transform']
source.data = get_data(df)
# etc
This is the closest I could get. The color map is updated with new values, but it seems that the colors of the marker still follow the original pattern. See picture below (given that quantity I would expect green, but it is blue as it still seen as < 4000 as in the map of the first plot before the callback).
Should I just add a "color" column to the data frame? I feel there is an easier/more convenient way to do that.
EDIT: Here is a minimal working example using the answer by bigreddot:
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.plotting import figure
from bokeh.models import Button, ColumnDataSource, ColorBar, HoverTool
from bokeh.palettes import Spectral6
from bokeh.transform import linear_cmap
import numpy as np
x = [1,2,3,4,5,7,8,9,10]
y = [1,2,3,4,5,7,8,9,10]
z = [1,2,3,4,5,7,8,9,10]
source = ColumnDataSource(dict(x=x, y=y, z=z))
#Use the field name of the column source
mapper = linear_cmap(field_name='z', palette=Spectral6 ,low=min(y) ,high=max(y))
p = figure(plot_width=300, plot_height=300, title="Linear Color Map Based on Y")
p.circle(x='x', y='y', line_color=mapper,color=mapper, fill_alpha=1, size=12, source=source)
color_bar = ColorBar(color_mapper=mapper['transform'], width=8, location=(0,0))
p.add_tools(HoverTool(tooltips="#z", show_arrow=False, point_policy='follow_mouse'))
p.add_layout(color_bar, 'right')
b = Button()
def update():
new_z = np.exp2(z)
mapper = linear_cmap(field_name='z', palette=Spectral6 ,low=min(new_z), high=max(new_z))
color_bar.color_mapper=mapper['transform']
source.data = dict(x=x, y=y, z=new_z)
b.on_click(update)
curdoc().add_root(column(b, p))
Upon update, the circles will be colored according to the original scale: everything bigger than 10 will be red. Instead, I would expect everything blue until the last 3 circle on tops that should be colored green yellow and red respectively.
It's possible that is a bug, feel free to open a GitHub issue.
That said, the above code does not represent best practices for Bokeh usage, which is: always make the smallest update possible. In this case, this means setting new property values on the existing color transform, rather than replacing the existing color transform.
Here is a complete working example (made with Bokeh 1.0.2) that demonstrates the glyph's colormapped colors updating in response to the data column changing:
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.plotting import figure
from bokeh.models import Button, ColumnDataSource, ColorBar
from bokeh.palettes import Spectral6
from bokeh.transform import linear_cmap
x = [1,2,3,4,5,7,8,9,10]
y = [1,2,3,4,5,7,8,9,10]
z = [1,2,3,4,5,7,8,9,10]
#Use the field name of the column source
mapper = linear_cmap(field_name='z', palette=Spectral6 ,low=min(y) ,high=max(y))
source = ColumnDataSource(dict(x=x, y=y, z=z))
p = figure(plot_width=300, plot_height=300, title="Linear Color Map Based on Y")
p.circle(x='x', y='y', line_color=mapper,color=mapper, fill_alpha=1, size=12, source=source)
color_bar = ColorBar(color_mapper=mapper['transform'], width=8, location=(0,0))
p.add_layout(color_bar, 'right')
b = Button()
def update():
new_z = np.exp2(z)
# update the existing transform
mapper['transform'].low=min(new_z)
mapper['transform'].high=max(new_z)
source.data = dict(x=x, y=y, z=new_z)
b.on_click(update)
curdoc().add_root(column(b, p))
Here is the original plot:
And here is the update plot after clicking the button

Animate GMapPlot w/ Python/Bokeh

I'm a very nooby programmer and this is my first Stack Overflow question. :)
So I'm trying to animate a car's trip on google maps using Python. I used matplotlib at first and could get a dot animated over the path line... then I tried using bokeh and successfully got the path to overlay on google maps...
My problem is that I haven't found a good way to do both (animate plot over google maps).
My data is in the form of Lat/Long coordinates.
Any advice? Thanks in advance!
EDIT: Here's my code that does the gmapplot... I'd rather have this and no animation than animation with no GMAP. My goal is to animate that "car" dot.
import numpy as np
from bokeh.io import output_file, show, vform
from bokeh.models.widgets import Dropdown
from bokeh.models import (GMapPlot, GMapOptions, ColumnDataSource, Line, Circle,
DataRange1d, PanTool, WheelZoomTool, BoxSelectTool, HoverTool)
data = np.genfromtxt('Desktop\Temp Data for Python\test data 3.csv', delimiter=',',
names=True)
map_options = GMapOptions(lat=np.average(data['Latitude']),
lng=np.average(data['Longitude']), map_type="roadmap", zoom=13)
plot = GMapPlot(x_range=DataRange1d(), y_range=DataRange1d(), map_options=map_options,
title="My Drive")
source = ColumnDataSource(data=dict(lat=data['Latitude'], lon=data['Longitude'],
speed=data['GpsSpeed'],))
path = Line(x="lon", y="lat", line_width = 2, line_color='blue')
car = Circle(x=data['Longitude'][0], y=data['Latitude'][0], size=5, fill_color='red')
plot.add_glyph(source, path)
plot.add_glyph(source, car)
plot.add_tools(PanTool(), WheelZoomTool(), BoxSelectTool(),
HoverTool(tooltips=[("Speed", "#speed"),]))
output_file("gmap_plot.html")
show(plot)
This may not be exactly what you are looking for, but you could have a slider widget that controls the position of your car dot. The slider example found in the bokeh docs (or github repository, I can't remember) helped me when I started using sliders.
Just so you are aware, I was having problems with latlng points showing up in the correct locations. There is about a 10px offset. This is an open issue (github issue 2964).
The following code currently is just producing a generic bokeh Figure, but in theory, if you change it from a Figure to a GMapPlot it should work. I wasn't able to get this working with GMapPlots directly. I think this may be because of github issue 3737. I can't even run the Austin example from the bokeh docs.
Hopefully this is what you had in mind
from bokeh.plotting import Figure, ColumnDataSource, show, vplot
from bokeh.io import output_file
from bokeh.models import (Slider, CustomJS, GMapPlot,
GMapOptions, DataRange1d, Circle, Line)
import numpy as np
output_file("path.html")
# Create path around roundabout
r = 0.000192
x1 = np.linspace(-1,1,100)*r
x2 = np.linspace(1,-1,100)*r
x = np.hstack((x1,x2))
f = lambda x : np.sqrt(r**2 - x**2)
y1 = f(x1)
y2 = -f(x2)
y = np.hstack((y1,y2))
init_x = 40.233688
init_y = -111.646784
lon = init_x + x
lat = init_y + y
# Initialize data sources.
location = ColumnDataSource(data=dict(x=[lon[0]], y=[lat[0]]))
path = ColumnDataSource(data=dict(x=lon, y=lat))
# Initialize figure, path, and point
"""I haven't been able to verify that the GMapPlot code below works, but
this should be the right thing to do. The zoom may be totally wrong,
but my latlng points should be a path around a roundabout.
"""
##options = GMapOptions(lat=40.233681, lon=-111.646595, map_type="roadmap", zoom=15)
##fig = GMapPlot(x_range=DataRange1d(), y_range=DataRange1d(), map_options=options)
fig = Figure(plot_height=600, plot_width=600)
c = Circle(x='x', y='y', size=10)
p = Line(x='x', y='y')
fig.add_glyph(location, c)
fig.add_glyph(path, p)
# Slider callback
callback = CustomJS(args=dict(location=location, path=path), code="""
var loc = location.get('data');
var p = path.get('data');
t = cb_obj.get('value');
/* set the point location to the path location that
corresponds to the slider position */
loc['x'][0] = p['x'][t];
loc['y'][0] = p['y'][t];
location.trigger('change');
""")
# The way I have written this, 'start' has to be 0 and
# 'end' has to be the length of the array of path points.
slider = Slider(start=0, end=200, step=1, callback=callback)
show(vplot(fig, slider))

interacting with multi_line in bokeh

Trying to plot 2 lines in Bokeh and update them simultaneously based on inputs from 2 sliders. When I use multi_line, the lines do not update.
Using ipython notebook:
%matplotlib inline
import numpy as np
from ipywidgets import interact
from bokeh.models import Line, ColumnDataSource
from bokeh.plotting import figure, show
from bokeh.io import output_notebook
Define 2 simple functions that give me my 2 lines over the same x-values:
def mu(q,tau,c):
p = np.maximum((tau-2*q-c),0)
return p*q
def mu_d(q,tau,c):
mask = (tau-q-c)<0
payoff = .25*(tau-q-c)**2
payoff[mask]=0
return payoff
Now make the Bokeh plot:
x = np.linspace(0, .6, 200)
y = mu(x,1,.1)
y1 = mu_d(x,1,.1)
output_notebook()
source = ColumnDataSource(data=dict(x=x, y=y, y1=y1))
p = figure(title="simple line example", plot_height=300, plot_width=600, y_range=(0, .25))
# p.line(x, y, source=source, alpha=.5, color="red", line_width=2) #this case works
# p.line(x, y1, source=source, alpha=.5, color="red", line_width=2) #this case does not
p.multi_line([x,x], [y,y1], source=source, alpha=.5, color=["red","blue"], line_width=2) # neither does this
def update(tau=1, c=.1):
source.data['y'] = mu(x,tau,c)
source.data['y1'] = mu_d(x,tau,c)
source.push_notebook()
show(p)
And the slider:
interact(update, tau=(0,1, 0.1), c=(0,.5, 0.1))
To debug things, I've been playing with plotting single lines. Things work fine if I plot only (x,y) as in the first case (commented out). When I try the second case, the line (x,y1) turns into (x,y) when the slider updates - even when I've restarted the kernel and commented out the unused source.data lines (bug?). I've printed the mu_d() output within the update command and things look nice there, but they don't update in the figure.
Anyone else encountered this? All tools (ipython, bokeh, etc.) are the latest version as in the Anaconda dist.
This isn't multi-line, but works. Would be nice if this could be vectorized, so that any number of lines could be manipulated at once without writing a ton of code.
source1 = ColumnDataSource(data=dict(x=qgrid, y=mugrid))
source2 = ColumnDataSource(data=dict(x=qgrid, y=mudgrid))
p.line(qgrid, mugrid, source=source1, legend='mu(q)',line_color="blue",alpha=.6)
p.line(qgrid, mudgrid,source=source2,legend='mu_d(q)',line_color="green",alpha=.5)
p.legend.label_text_font = "times"
def update(tau=1, c=.1):
source1.data['y'] = mu(x,tau,c)
source2.data['y'] = mu_d(x,tau,c)
source1.push_notebook()
source2.push_notebook()
show(p)

Categories