I'm trying to generate a bokeh application which allows the user to change the glyphs of a plot. Unfortunately, the glyphs don't change after calling on_change() method of the dropdown button although I'm able to change the axis label in a similar way. However, changing plot.glyph outside of the called function works fine.
from bokeh.layouts import layout
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import Dropdown
from bokeh.plotting import figure
from bokeh.io import curdoc
from bokeh.models.markers import Cross, Circle
source=ColumnDataSource(dict(x=[1,2,3],y=[4,5,6]))
fig=figure()
plot=fig.circle(x='x', y='y',source=source)
fig.xaxis.axis_label='This is a label'
#this changes the glyphs from circle to cross
plot.glyph=Cross(x='x', y='y', size=20, line_color='firebrick',
fill_color='firebrick', line_alpha=0.8, fill_alpha=0.3)
def update_plot(attr,old,new):
if dropdown.value=='cross':
#this changes the axis label but not the glyphs
fig.xaxis.axis_label='Label changed'
plot.glyph=Cross(x='x', y='y', size=20, line_color='firebrick',
fill_color='firebrick', line_alpha=0.8, fill_alpha=0.3)
elif dropdown.value=='circle':
#this also only changes the axis label but not the glyphs
fig.xaxis.axis_label='Label changed again'
plot.glyph=Circle(x='x', y='y', size=20, line_color='firebrick',
fill_color='firebrick', line_alpha=0.8, fill_alpha=0.3)
menu=[('Circle','circle'),('Cross', 'cross')]
dropdown=Dropdown(label="Select marker", menu=menu, value='circle')
dropdown.on_change('value', update_plot)
lay_out=layout([fig, dropdown])
curdoc().add_root(lay_out)
I'm not sure about the exact reasons why your approach doesn't work, but this one works:
from bokeh.io import curdoc
from bokeh.layouts import layout
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import Dropdown
from bokeh.plotting import figure
source = ColumnDataSource(dict(x=[1, 2, 3], y=[4, 5, 6]))
plot = figure()
renderers = {rn: getattr(plot, rn)(x='x', y='y', source=source,
**extra, visible=False)
for rn, extra in [('circle', dict(size=10)),
('line', dict()),
('cross', dict(size=10)),
('triangle', dict(size=15))]}
def label_fn(item):
return 'Select marker ({})'.format(item)
menu = [('No renderer', None)]
menu.extend((rn.capitalize(), rn) for rn in renderers)
dropdown = Dropdown(label=label_fn(None), menu=menu, value=None)
def update_plot(attr, old, new):
dropdown.label = label_fn(new)
for renderer_name, renderer in renderers.items():
renderer.visible = (renderer_name == new)
dropdown.on_change('value', update_plot)
lay_out = layout([plot, dropdown])
curdoc().add_root(lay_out)
Basically, I create all necessary renderers beforehand, and then just switch visible flag of each one.
Also, note the correct terminology. What you're calling a plot, is not actually a plot but a glyph renderer. And plot and figure are basically the same thing.
Related
I'm trying to highlight last value of a time series plot by plot its value on yaxis, as shown in this question. I prefer using LabelSet over Legend because you can precisely control the text positions and also using a data source to update it. But unfortunately, I can not find out how to draw label text outside the plot box.
Here is some code to plot LabelSet and notice how the text is only shown inside the box (66.1x is partially blocked by yaxis):
import pandas as pd
from bokeh.io import output_notebook
output_notebook()
from bokeh.plotting import figure, show
from bokeh.models import LabelSet, ColumnDataSource
#import bokeh.sampledata
#bokeh.sampledata.download()
from bokeh.sampledata.stocks import MSFT
df = pd.DataFrame(MSFT)[:50]
df["date"] = pd.to_datetime(df["date"])
p = figure(
x_axis_type="datetime", width=1000, toolbar_location='left',
title = "MSFT Candlestick", y_axis_location="right")
p.line(df.date, df.close)
ds = ColumnDataSource({'x': [df.date.iloc[-1]], 'y': [df.close.iloc[-1]], 'text': [' ' + str(df.close.iloc[-1])]})
ls = LabelSet(x='x', y='y', text='text', source=ds)
p.add_layout(ls)
show(p)
Please let me know how to show LabelSet outside the box, Thanks
I am plotting a heatmap representation of a correlation matrix with Rect glyphs and a Labelset on a categorical axis. I'm trying to use a MultiSelect widget to dynamically update which elements are currently displayed.
Expected behavior - when I click on the button, the heatmap updates and displays only the elements that were picked in the widget (X and Y axis labels are identical). This works fine most of the time, but
Observed behavior: if I uncheck the first label in the list, the Rect glyphs shift right by 1 unit, while the LabelSet glyphs are displayed correctly.
Here's a minimal example:
from bokeh.io import show, output_notebook
from bokeh.models.callbacks import CustomJS
from bokeh.models.glyphs import Rect
from bokeh.events import ButtonClick
from bokeh.models.widgets import MultiSelect, Button
from bokeh.models import ColumnDataSource, LabelSet
from bokeh.layouts import row, column
from bokeh.plotting import figure
output_notebook()
data = pd.DataFrame({
'A': [0,1,2],
'B': [3,4,5],
'C': [6,7,8]
}, index=['A', 'B', 'C'])
data_transformed = data.stack().rename('value').reset_index()
source = ColumnDataSource(data_transformed)
p = figure(
width=200,
height=200,
x_range=data_transformed.level_0.unique().tolist(),
y_range=data_transformed.level_0.unique().tolist()[::-1],
x_axis_location="above"
)
rect = Rect(
x='level_0',
y='level_1',
width=1,
height=1,
fill_color='red',
line_color='black'
)
labels = LabelSet(
x='level_0',
y='level_1',
text='value',
level='annotation',
source=source
)
p.add_glyph(source, rect)
p.add_layout(labels)
multiselect = MultiSelect(
title='Pick the symbols',
value=data_transformed.level_0.unique().tolist(),
options=data_transformed.level_0.unique().tolist(),
height=200
)
button_callback = CustomJS(args=dict(plot=p, multiselect=multiselect), code="""
var x_range = multiselect.value.concat().sort()
var y_range = x_range.concat().reverse()
plot.x_range.factors = x_range
plot.y_range.factors = y_range
""")
button = Button(label="Calculate", button_type="primary")
button.js_on_event(ButtonClick, button_callback)
widgets = column(children=[multiselect, button])
layout = row(children=[widgets, p])
show(layout)
Initial display:
Image 1
After unchecking the last element and clicking the button (all good here):
Image 2
If the first element is unchecked:
Image 3
After switching to Circle glyphs it works as expected :
Image 4
Is this a bug or am I doing something wrong? Thanks.
I am using Bokeh 2.0.2
I'm pretty sure it's a bug. I've opened https://github.com/bokeh/bokeh/issues/10219.
I'm using the Bokeh package to plot a line chart.
I want a given line to bolden (alpha to increase) when I hover over it.
I added a hover tool and then added "hover_line_alpha = 0.6" in my line chart.
However when I hover over points on a given line, the line disappears altogether!
Can you help me fix this?
Code below so you can see my logic.
Thanks,
Ross
# Code in Question
from bokeh.io import output_notebook, show, output_file
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, HoverTool
output_notebook()
# set out axes
x = 'time_rnd'
y = 'count'
# set colour palette
col_brew = ['#8dd3c7','#ffffb3','#bebada','#fb8072','#80b1d3','#fdb462','#b3de69','#fccde5','#d9d9d9','#bc80bd','#ccebc5','#ffed6f']
# map out figure
plot = figure(tools='box_select, lasso_select, save' ,x_axis_type='datetime')
# add HoverTool
hover_info = [('time', '#hover_time'),
('word', '#word'),
('count', '#count')]
hover = HoverTool(names=['use'],tooltips=hover_info,
mode='mouse',
show_arrow=True
)
plot.add_tools(hover)
### FOR LOOP OF PLOT [THIS IS WHERE THE ISSUE MANIFESTS]
for i in top_wds_test:
df_eng_word = df_eng_timeline[df_eng_timeline['word']==i]
source = ColumnDataSource(df_eng_word)
plot.line(x, y, line_width = 3,
line_alpha = 0.1, line_color=col_brew[top_wds.index(i)],
hover_line_alpha = 0.6,
#hover_line_color = 'black',
#hover_line_color = col_brew[top_wds.index(i)],
source = source, legend=i, name = 'use'
)
plot.circle(x, y, fill_color='white', size=5,
selection_color='green',
nonselection_fill_color='grey',nonselection_fill_alpha=0.4,
hover_color='red',
source = source, name = 'use')
# add legend
plot.legend.location = "top_left"
plot.legend.label_text_font_style = 'bold'
# materialize the plot
show(plot)
There seems to be an issue when the renderers share a data source. However, this works (with Bokeh >= 0.13.0) if you let Bokeh create a new separate source for each glyph:
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show
p = figure(tools="hover", tooltips="$name: #$name")
data=dict(x=[1,2,3], y1=[2,6,5], y2=[6,2,3])
p.line('x', 'y1', color="navy", line_width=3, source=data,
alpha=0.1, hover_color="navy", hover_alpha=0.6, name="y1")
p.line('x', 'y2',color="firebrick", line_width=3, source=data,
alpha=0.1, hover_color="firebrick", hover_alpha=0.6, name="y2")
show(p)
So I'm trying to create a Bokeh plot for which I would like to be able to manually adapt the range of the X-Axis with a slider.
Being a newbie I've only managed to get this far and despite researching this subject I haven't been able to solve this problem.
Here my code:
from bokeh.layouts import column
from bokeh.models import CustomJS, ColumnDataSource, Slider
from bokeh.plotting import Figure, output_file, show
output_file("test.html")
x = [x*0.5 for x in range(0, 200)]
y = x
source = ColumnDataSource(data=dict(x=x, y=y))
plot = Figure(plot_width=600, plot_height=400, x_range=(0, 100))
plot.line('x', 'y', source=source, line_width=2, line_alpha=0.75)
callback = CustomJS(args=dict(x_range=plot.x_range), code="""
var start = cb_obj.value
x_range.set({"start": start, "end": start+10})
""")
slider = Slider (start=0, end=90, value=20, step=10)
slider.js_on_change('value', callback)
layout = column(slider, plot)
show(layout)
My biggest problem is to understand how I connect the CustomJS to my plot.
I would gladly appreciate your help.
It has been answered at the Bokeh Google group
To reiterate, the only change needed is to replace x_range.set with x_range.setv.
I'm new to bokeh and I just jumped right into using hovertool as that's why I wanted to use bokeh in the first place.
Now I'm plotting genes and what I want to achieve is multiple lines with the same y-coordinate and when you hover over a line you get the name and position of this gene.
I have tried to mimic this example, but for some reason the I can't even get it to show coordinates.
I'm sure that if someone who actually knows their way around bokeh looks at this code, the mistake will be apparent and I'd be very thankful if they showed it to me.
from bokeh.plotting import figure, HBox, output_file, show, VBox, ColumnDataSource
from bokeh.models import Range1d, HoverTool
from collections import OrderedDict
import random
ys = [10 for x in range(len(levelsdf2[(name, 'Start')]))]
xscale = zip(levelsdf2[('Log', 'Start')], levelsdf2[('Log', 'Stop')])
yscale = zip(ys,ys)
TOOLS="pan,wheel_zoom,box_zoom,reset,hover"
output_file("scatter.html")
hover_tips = levelsdf2.index.values
colors = ["#%06x" % random.randint(0,0xFFFFFF) for c in range(len(xscale))]
source = ColumnDataSource(
data=dict(
x=xscale,
y=yscale,
gene=hover_tips,
colors=colors,
)
)
p1 = figure(plot_width=1750, plot_height=950,y_range=[0, 15],tools=TOOLS)
p1.multi_line(xscale[1:10],yscale[1:10], alpha=1, source=source,line_width=10, line_color=colors[1:10])
hover = p1.select(dict(type=HoverTool))
hover.tooltips = [
("index", "$index"),
("(x,y)", "($x, $y)"),
]
show(p1)
the levelsdf2 is a pandas.DataFrame, if it matters.
I figured it out on my own. It turns out that version 0.8.2 of Bokeh doesn't allow hovertool for lines so I did the same thing using quads.
from bokeh.plotting import figure, HBox, output_file, show, VBox, ColumnDataSource
from bokeh.models import Range1d, HoverTool
from collections import OrderedDict
import random
xscale = zip(levelsdf2[('series1', 'Start')], levelsdf2[('series1', 'Stop')])
xscale2 = zip(levelsdf2[('series2', 'Start')], levelsdf2[('series2', 'Stop')])
yscale2 = zip([9.2 for x in range(len(levelsdf2[(name, 'Start')]))],[9.2 for x in range(len(levelsdf2[(name, 'Start')]))])
TOOLS="pan,wheel_zoom,box_zoom,reset,hover"
output_file("linesandquads.html")
hover_tips = levelsdf2.index.values
colors = ["#%06x" % random.randint(0,0xFFFFFF) for c in range(len(xscale))]
proc1 = 'Log'
proc2 = 'MazF2h'
expression1 = levelsdf2[(proc1, 'Level')]
expression2 = levelsdf2[(proc2, 'Level')]
source = ColumnDataSource(
data=dict(
start=[min(xscale[x]) for x in range(len(xscale))],
stop=[max(xscale[x]) for x in range(len(xscale))],
start2=[min(xscale2[x]) for x in range(len(xscale2))],
stop2=[max(xscale2[x]) for x in range(len(xscale2))],
gene=hover_tips,
colors=colors,
expression1=expression1,
expression2=expression2,
)
)
p1 = figure(plot_width=900, plot_height=500,y_range=[8,10.5],tools=TOOLS)
p1.quad(left="start", right="stop", top=[9.211 for x in range(len(xscale))],
bottom = [9.209 for x in range(len(xscale))], source=source, color="colors")
p1.multi_line(xscale2,yscale2, source=source, color="colors", line_width=20)
hover = p1.select(dict(type=HoverTool))
hover.tooltips = OrderedDict([
(proc1+" (start,stop, expression)", "(#start| #stop| #expression1)"),
("Gene","#gene"),
])
show(p1)
Works like a charm.
EDIT: Added a picture of the result, as requested and edited code to match the screenshot posted.
It's not the best solution as it turns out it's not all that easy to plot several series of quads on one plot. It's probably possible but as it didn't matter much in my use case I didn't investigate too vigorously.
As all genes are represented on all series at the same place I just added tooltips for all series to the quads and plotted the other series as multi_line plots on the same figure.
This means that if you hovered on the top line at 9.21 you'd get tooltips for the line at 9.2 as well, but If you hovered on the 9.2 line you wouldn't get a tooltip at all.