interacting with multi_line in bokeh - python

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)

Related

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

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

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()

Bokeh: linking a line plot and a scatter plot

I have a line plot and a scatter plot that are conceptually linked by sample IDs, i.e. each dot on the 2D scatter plot corresponds to a line on the line plot.
While I have done linked plotting before using scatter plots, I have not seen examples of this for the situation above - where I select dots and thus selectively view lines.
Is it possible to link dots on a scatter plot to a line on a line plot? If so, is there an example implementation available online?
Searching the web for bokeh link line and scatter plot yields no examples online, as of 14 August 2018.
I know this is a little late - but maybe this snippet of code will help?
import numpy as np
from bokeh.io import output_file, show
from bokeh.layouts import gridplot
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
from bokeh.models import Circle,MultiLine
def play():
x = np.linspace(0,10,100)
y = np.random.rand(100)
xs = np.random.rand(100,3)
ys = np.random.normal(size=(100,3))
xp = [list(xi) for xi in xs] # Because Multi-List does not like numpy arrays
yp = [list(yi) for yi in ys]
output_file('play.html')
source = ColumnDataSource(data=dict(x=x,y=y,xp=xp,yp=yp))
TOOLS = 'box_select'
left = figure(tools=TOOLS,plot_width=700,plot_height=700)
c1 = left.circle('x','y',source=source)
c1.nonselection_glyph = Circle(fill_color='gray',fill_alpha=0.4,
line_color=None)
c1.selection_glyph = Circle(fill_color='orange',line_color=None)
right = figure(tools=TOOLS,plot_width=700,plot_height=700)
c2 = right.multi_line(xs='xp',ys='yp',source=source)
c2.nonselection_glyph = MultiLine(line_color='gray',line_alpha=0.2)
c2.selection_glyph = MultiLine(line_color='orange')
p = gridplot([[left, right]])
show(p)
As things turn out, I was able to make this happen by using HoloViews rather than Bokeh. The relevant example for making this work comes from the Selection1d tap stream.
http://holoviews.org/reference/streams/bokeh/Selection1D_tap.html#selection1d-tap
I will do an annotated version of the example below.
First, we begin with imports. (Note: all of this assumes work is being done in the Jupyter notebook.)
import numpy as np
import holoviews as hv
from holoviews.streams import Selection1D
from scipy import stats
hv.extension('bokeh')
First off, we set some styling options for the charts. In my experience, I usually build the chart before styling it, though.
%%opts Scatter [color_index=2 tools=['tap', 'hover'] width=600] {+framewise} (marker='triangle' cmap='Set1' size=10)
%%opts Overlay [toolbar='above' legend_position='right'] Curve (line_color='black') {+framewise}
This function below generates data.
def gen_samples(N, corr=0.8):
xx = np.array([-0.51, 51.2])
yy = np.array([0.33, 51.6])
means = [xx.mean(), yy.mean()]
stds = [xx.std() / 3, yy.std() / 3]
covs = [[stds[0]**2 , stds[0]*stds[1]*corr],
[stds[0]*stds[1]*corr, stds[1]**2]]
return np.random.multivariate_normal(means, covs, N)
data = [('Week %d' % (i%10), np.random.rand(), chr(65+np.random.randint(5)), i) for i in range(100)]
sample_data = hv.NdOverlay({i: hv.Points(gen_samples(np.random.randint(1000, 5000), r2))
for _, r2, _, i in data})
The real magic begins here. First off, we set up a scatterplot using the hv.Scatter object.
points = hv.Scatter(data, ['Date', 'r2'], ['block', 'id']).redim.range(r2=(0., 1))
Then, we create a Selection1D stream. It pulls in points from the points object.
stream = Selection1D(source=points)
We then create a function to display the regression plot on the right. There's an empty plot that is the "default", and then there's a callback that hv.DynamicMap calls on.
empty = (hv.Points(np.random.rand(0, 2)) * hv.Curve(np.random.rand(0, 2))).relabel('No selection')
def regression(index):
if not index:
return empty
scatter = sample_data[index[0]]
xs, ys = scatter['x'], scatter['y']
slope, intercep, rval, pval, std = stats.linregress(xs, ys)
xs = np.linspace(*scatter.range(0)+(2,))
reg = slope*xs+intercep
return (scatter * hv.Curve((xs, reg))).relabel('r2: %.3f' % slope)
Now, we create the DynamicMap which dynamically loads the regression curve data.
reg = hv.DynamicMap(regression, kdims=[], streams=[stream])
# Ignoring annotation for average - it is not relevant here.
average = hv.Curve(points, 'Date', 'r2').aggregate(function=np.mean)
Finally, we display the plots.
points * average + reg
The most important thing I learned from building this is that the indices for the points have to be lined up with the indices for the regression curves.
I hope this helps others building awesome viz using HoloViews!

Bokeh HoverTool Shows "???"

I'm getting into the Bokeh library with Python, but I'm having some trouble. I have the following code from the Bokeh tutorial website:
from bokeh.plotting import figure
from bokeh.io import output_notebook, show
output_notebook()
from bokeh.sampledata.autompg import autompg
from bokeh.models import HoverTool
from bokeh.plotting import ColumnDataSource
grouped = autompg.groupby("yr")
mpg2 = grouped["mpg"]
avg = mpg2.mean()
std = mpg2.std()
years = list(grouped.groups.keys())
american = autompg[autompg["origin"]==1]
japanese = autompg[autompg["origin"]==3]
p = figure(title="MPG by Year (Japan and US)")
p.vbar(x=years, bottom=avg-std, top=avg+std, width=0.8,
fill_alpha=0.2, line_color=None, legend="MPG 1 stddev")
p.circle(x=japanese["yr"], y=japanese["mpg"], size=10, alpha=0.5,
color="red", legend="Japanese")
p.triangle(x=american["yr"], y=american["mpg"], size=10, alpha=0.3,
color="blue", legend="American")
p.legend.location = "top_left"
show(p)
It works, but I'd like to add the functionality that when you hover over a point, it displays the horsepower. What I tried is
grouped = autompg.groupby("yr")
mpg = grouped["mpg"]
avg = mpg.mean()
std = mpg.std()
years = list(grouped.groups.keys())
american = autompg[autompg["origin"]==1]
japanese = autompg[autompg["origin"]==3]
source = ColumnDataSource(data=
dict(autompg)
)
hover1 = HoverTool(tooltips=[("hp", "#hp")])
p = figure(title="MPG by Year (Japan and US)",tools=[hover1])
p.vbar(x=years, bottom=avg-std, top=avg+std, width=0.8,
fill_alpha=0.2, line_color=None, legend="MPG 1 stddev")
p.circle(x=japanese["yr"], y=japanese["mpg"], size=10, alpha=0.5,
color="red", legend="Japanese")
p.triangle(x=american["yr"], y=american["mpg"], size=10, alpha=0.3,
color="blue", legend="American")
p.legend.location = "top_left"
show(p)
So defining a HoverTool that I hoped would do just that. Unfortunately, it only displays "hp: ???" for every entry. I think it's a problem with the data source, but I don't have much experience here and can't figure it out myself. I've tried the source without dict(), and I've tried to set it as american or japanese, but none of this made a difference.
Thanks!
You need to pass the source to the glyph functions, and refer to the column names for the coordinates. If you pass literal lists/arrays (as you are doing above) to circle, etc. then Bokeh will create a CDS for that data under the covers, but only with the just the data you pass to the glyph function (i.e. without extra columns like "hp"). Since you are trying to plot different glyphs for different subsets of the data, the simplest thing will be to use a CDSView to group them on the client. Something like this instead:
from bokeh.plotting import figure
from bokeh.io import show
from bokeh.sampledata.autompg import autompg
from bokeh.models import ColumnDataSource, CDSView, GroupFilter, HoverTool
p = figure()
# Bokeh factors must be strings
autompg.origin = [str(x) for x in autompg.origin]
source = ColumnDataSource(autompg)
# view for just japanese origin
japanese = CDSView(source=source, filters=[GroupFilter(column_name='origin', group="1")])
# draw circles for just the japanese view
p.circle(x="yr", y="mpg", size=10, alpha=0.5, color="red", legend="Japanese",
source=source, view=japanese)
# view for just japanese origin
american = CDSView(source=source, filters=[GroupFilter(column_name='origin', group="3")])
# draw triangles for just the american view
p.triangle(x="yr", y="mpg", size=10, alpha=0.5, color="blue", legend="american",
source=source, view=american)
p.add_tools(HoverTool(tooltips=[("hp", "#hp")]))
show(p)

Categories