Related
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.
I'm trying to implement a python callback function for a RangeSlider. The Slider Value should tell which Index a IndexFilter should get for display.
For example: If rangeslider.value is (3, 25) my plots should only contain/view data with the Index from 3 to 25.
from bokeh.io import output_file, show
from bokeh.models import ColumnDataSource, GMapOptions, CustomJS, CDSView, IndexFilter
from bokeh.plotting import gmap, ColumnDataSource, figure
from bokeh.layouts import column, row
from bokeh.models.widgets import RangeSlider
import numpy as np
def slider_callback(attr, old, new):
p.view = CDSView(source=source, filters=[IndexFilter(np.arange(new.value[0], new.value[1]))])
v.view = CDSView(source=source, filters=[IndexFilter(np.arange(new.value[0], new.value[1]))])
# data set
lon = [[48.7886, 48.7887, 48.7888, 48.7889, 48.789],
[48.7876, 48.7877, 48.78878, 48.7879, 48.787],
[48.7866, 48.7867, 48.7868, 48.7869, 48.786],
[48.7856, 48.7857, 48.7858, 48.7859, 48.785],
[48.7846, 48.7847, 48.7848, 48.7849, 48.784]]
lat = [[8.92, 8.921, 8.922, 8.923, 8.924],
[8.91, 8.911, 8.912, 8.913, 8.914],
[8.90, 8.901, 8.902, 8.903, 8.904],
[8.89, 8.891, 8.892, 8.893, 8.894],
[8.88, 8.881, 8.882, 8.883, 8.884]]
time = [0, 1, 2, 3, 4, 5]
velocity = [23, 24, 25, 24, 20]
lenght_dataset = len(lon)
# define source and map
source = ColumnDataSource(data = {'x': lon, 'y': lat, 't': time, 'v': velocity})
view = CDSView(source=source, filters=[IndexFilter(np.arange(0, lenght_dataset))])
map_options = GMapOptions(lat=48.7886, lng=8.92, map_type="satellite", zoom=13)
p = gmap("MY_API_KEY", map_options, title="Trajectory Map")
v = figure(plot_width=400, plot_height=400, title="Velocity")
# plot lines on map
p.multi_line('y', 'x', view=view, source=source, line_width=1)
v.line('t', 'v', view=view, source=source, line_width=3)
# slider to limit plotted data
range_slider = RangeSlider(title="Data Range Slider: ", start=0, end=lenght_dataset, value=(0, lenght_dataset), step=1)
range_slider.on_change('value', slider_callback)
# Layout to plot and output
layout = row(column(p, range_slider),
column(v)
)
output_file("diag_plot_bike_data.html")
show(layout)
Some notes:
time is longer than the rest of the columns - you will receive a warning about it. In my code below, I just removed its last element
view with filters in general should not be used for continuous glyphs like lines (v.line in particular - multi_line is fine). You will receive a warning about it. But if the indices in IndexFilter are always continuous, then you should be fine. Either way, you can use the segment glyph to avoid the warning
In your callback, you're trying to set view on the figures - views only exist on glyph renderers
In general, you don't want to recreate views, you want to recreate as few Bokeh models as possible. Ideally, you would have to just change the indices field of the filter. But there's some missing wiring in Bokeh, so you will have to set the filters field of the view, as below
new argument of Python callbacks receives the new value for the attribute passed as the first parameter to the corresponding on_change call. In this case, it will be a tuple, so instead of new.value[0] you should use new[0]
Since you've decided to use Python callbacks, you can no longer use show and have a static HTML file - you will have to use curdoc().add_root and bokeh serve. The UI needs that Python code to run somewhere in runtime
When changing the slider values, you will notice that the separate segments of multi_line will be joined together - it's a bug and I just created https://github.com/bokeh/bokeh/issues/10589 for it
Here's a working example:
from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import GMapOptions, CDSView, IndexFilter
from bokeh.models.widgets import RangeSlider
from bokeh.plotting import gmap, ColumnDataSource, figure
lon = [[48.7886, 48.7887, 48.7888, 48.7889, 48.789],
[48.7876, 48.7877, 48.78878, 48.7879, 48.787],
[48.7866, 48.7867, 48.7868, 48.7869, 48.786],
[48.7856, 48.7857, 48.7858, 48.7859, 48.785],
[48.7846, 48.7847, 48.7848, 48.7849, 48.784]]
lat = [[8.92, 8.921, 8.922, 8.923, 8.924],
[8.91, 8.911, 8.912, 8.913, 8.914],
[8.90, 8.901, 8.902, 8.903, 8.904],
[8.89, 8.891, 8.892, 8.893, 8.894],
[8.88, 8.881, 8.882, 8.883, 8.884]]
time = [0, 1, 2, 3, 4]
velocity = [23, 24, 25, 24, 20]
lenght_dataset = len(lon)
# define source and map
source = ColumnDataSource(data={'x': lon, 'y': lat, 't': time, 'v': velocity})
view = CDSView(source=source, filters=[IndexFilter(list(range(lenght_dataset)))])
map_options = GMapOptions(lat=48.7886, lng=8.92, map_type="satellite", zoom=13)
p = gmap("API_KEY", map_options, title="Trajectory Map")
v = figure(plot_width=400, plot_height=400, title="Velocity")
p.multi_line('y', 'x', view=view, source=source, line_width=1)
v.line('t', 'v', view=view, source=source, line_width=3)
range_slider = RangeSlider(title="Data Range Slider: ", start=0, end=lenght_dataset, value=(0, lenght_dataset), step=1)
def slider_callback(attr, old, new):
view.filters = [IndexFilter(list(range(*new)))]
range_slider.on_change('value', slider_callback)
layout = row(column(p, range_slider), column(v))
curdoc().add_root(layout)
For my project I need to add and remove glpyhs and annotations in bokeh (line, multiline and arrows). I want to make it as interactive as possible. So in order to remove a glyph/annotation in want to select it with a mouse click and then e.g. delete it with a button. The minimal example would look like that:
import numpy as np
import random
from bokeh.plotting import figure, ColumnDataSource
from bokeh.models import Button, TapTool,Arrow,NormalHead
from bokeh.layouts import layout
from bokeh.application import Application
from bokeh.server.server import Server
from bokeh.application.handlers.function import FunctionHandler
plot = figure(plot_height=300, plot_width=600, x_range=(0, 8), y_range=(0, 11),
title="Testplot", tools='save, reset, tap')
Lay = layout(children=[])
#adds the glyphs/annotaions to figure
def Click_action():
x = np.array((random.randrange(1,10),random.randrange(1,10)))
y = np.array((random.randrange(1,10),random.randrange(1,10)))
source = ColumnDataSource(data=dict(x = x,
y = y))
arro = Arrow(end=NormalHead(size=5, fill_color="#C0392B"),
x_start=random.randrange(0,10),
y_start=random.randrange(0,10),
x_end=random.randrange(0,10),
y_end=random.randrange(0,10),
line_width=3,
line_color="#C0392B")
plot.multi_line(xs=[[1,5],[1,1],[3,3],[5,5]],ys=[[5,5],[5,1],[5,1],[5,1]], color='blue', selection_color='red' )
plot.add_layout(arro)
plot.line(x='x',y='y', source = source,selection_color='red')
def Click_delet():
""" Delete the selected Glyphs/Annotations"""
def make_document(doc):
btn1 = Button(label="Click", button_type="success")
btn2 = Button(label="Click_delet", button_type="success")
btn1.on_click(Click_action)
btn2.on_click(Click_delet)
Lay.children.append(plot)
Lay.children.append(btn1)
Lay.children.append(btn2)
doc.add_root(Lay)
if __name__ == '__main__':
bkapp = {'/': Application(FunctionHandler(make_document))}
server = Server(bkapp, port=5004)
server.start()
server.io_loop.add_callback(server.show, "/")
server.io_loop.start()
The problems I currently have are:
How can I select the arrow ?
How do I get all selected glyphs and annotations? (If possible without a CoustomJS callback since I do not know java that well)
Is it possible to select the multiline as one glyph?
I have already solved the issue how to delete lines and arrows from a plot. But I would need the value stored in plot.renders and plot.center in order to delete them and link them to different classes in my project.
Annotations are not interactive in Bokeh
See a minimal example below
No
But I would need the value stored in plot.renders and plot.center in order to delete them and link them to different classes in my project.
Ideally, your workflow should abstain from dynamically creating and removing Bokeh models, especially low-levels ones such as glyphs. If you need to remove a glyph and add a new one with new properties, consider just changing properties of the old glyph. Or maybe just clear the data of the old glyph to hide it.
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import Button
from bokeh.plotting import figure, ColumnDataSource
line_ds = ColumnDataSource(dict(x=[0, 3, 7],
y=[1, 8, 2]))
multi_line_ds = ColumnDataSource(dict(xs=[[1, 5], [1, 1], [3, 3], [5, 5]],
ys=[[5, 5], [5, 1], [5, 1], [5, 1]]))
p = figure(x_range=(0, 8), y_range=(0, 11), tools='save, reset, tap')
p.line('x', 'y', source=line_ds, selection_color='red')
p.multi_line('xs', 'ys', source=multi_line_ds, color='blue', selection_color='red')
b = Button(label="Delete selected", button_type="success")
def delete_rows(ds, indices):
print(indices)
if indices:
print(ds.data)
ds.data = {k: [v for i, v in enumerate(vs) if i not in set(indices)]
for k, vs in ds.data.items()}
print(ds.data)
def delete_selected():
delete_rows(line_ds, line_ds.selected.line_indices)
delete_rows(multi_line_ds, multi_line_ds.selected.indices)
b.on_click(delete_selected)
curdoc().add_root(column(p, b))
Using Bokeh 1.4 and Python 3.7. I have a set of patches that I'd like to vary the color theme for based on two different keys (and labels) from the same ColumnDataSource. I want to stick to using one ColumnDataSource because my real file is quite large and the geometry (i.e. the xs and ys) are common between the two things i'd like to theme by.
See my working example:
from bokeh.io import show
from bokeh.models import ColumnDataSource,CustomJS, widgets, LinearColorMapper
from bokeh.palettes import RdBu6, Spectral11
from bokeh.plotting import figure
from bokeh.layouts import layout, column, row
source = ColumnDataSource(dict(
xs=[[1,2,2], [1,2,2], [3,4,4], [3,4,4]],
ys=[[3,3,4], [1,1,2], [3,3,4], [1,1,2]],
s1=[0, 50, 75, 50],
s2=[0, 25, 50, 75],
label_1=['Blue', 'Orangy', 'Red', 'Orangy'],
label_2=['S', 'P', 'E', 'C']
))
cmap1 = LinearColorMapper(palette='RdBu6', low = 0, high = 75)
cmap2 = LinearColorMapper(palette='Spectral11', low = 0, high = 75)
p = figure(x_range=(0, 7), y_range=(0, 5), plot_height=300)
patches = p.patches( xs='xs', ys='ys', fill_color={'field':'s1','transform':cmap1}
, legend_field='label_1', source=source)
b = widgets.Button(label = 'RdBu')
b.js_on_click(CustomJS(args=dict(b=b,source=source,patches=patches,cmap1=cmap1,cmap2=cmap2,p=p),
code="""if (b.label == 'RdBu')
{b.label='Spectral';
patches.glyph.fill_color = {field: 's2',transform:cmap2};}
else if (b.label == 'Spectral')
{b.label='RdBu';
patches.glyph.fill_color = {field: 's1',transform:cmap1}}"""
))
layout=column(row(p),row(b))
show(layout)
This yields this, and then this when clicking the button. You can see that the fill_color update part of the callback is working correctly as the colors change and even the colors in the legend change, but I have been unable to find a way instruct the CustomJS to properly update the legend entries so that in the second image there would be 4 entries with 'S','P','E' and 'C' as the legend labels.
From what I can tell, when I create the patches object and specify a legend_field argument, it constructs a legend for me with some sort of groupby/aggregate function to generate unique legend entries for me, and then it adds that legend to the figure object?
So that led me down the path of trying to drill down into p.legend:
p.legend.items #returns a list containing one LegendItem object
p.legend.items[0].label #returns a dictionary: {'field': 'label_1'}
I tried putting p.legend.items[0].label['field'] = 'label_2' outside of the callback and it worked as I hoped - the legend now reads S,P,E,C. But when I try putting that into the callback code it doesn't seem to update:
b.js_on_click(CustomJS(args=dict(b=b,source=source,patches=patches,cmap1=cmap1,cmap2=cmap2,p=p),
code="""if (b.label == 'RdBu')
{b.label='Spectral';
patches.glyph.fill_color = {field: 's2',transform:cmap2};
p.legend.items[0].label['field']='label_2'}
else if (b.label == 'Spectral')
{b.label='RdBu';
patches.glyph.fill_color = {field: 's1',transform:cmap1}
p.legend.items[0].label['field']='label_1'}"""
))
I feel like I'm very close but just missing one or two key things.... any advice/help appreciated!
Solution from Carolyn here: https://discourse.bokeh.org/t/is-there-a-way-to-update-legend-patch-labels-using-a-customjs-callback/4504
... I was really close.
I'm lost as to how to set up a Column Data Source so that I can select points from one graph and have the corresponding points highlighted in another graph. I am trying to learn more about how this works.
The sample code I am using is the example called Linked Brushing. I'd like to see if I can get the same effect with my own code, below. That web page explanation also refers to Linked Selection with Filtered Data but I don't understand what the code filters=[BooleanFilter([True if y > 250 or y < 100 else False for y in y1] on that page does, so I'm not sure how to adapt it, or if it's even relevant.
Here is my code:
from bokeh.plotting import figure, output_file, show, Column
from bokeh.models import ColumnDataSource, CDSView, BooleanFilter
from MyFiles import *
class bokehPlot:
def __init__(self, filename, t, a, b, c, d):
self.source = ColumnDataSource(data=dict(x=t, y1=a, y2=b, y3=c, y4=d))
p1 = self.makePlot(filename, 'x', 'y1', 'A')
p2 = self.makePlot(filename, 'x', 'y2', 'B', x_link=p1)
p3 = self.makePlot(filename, 'x', 'y3', 'C', x_link=p1)
p4 = self.makePlot(filename, 'x', 'y4', 'D', x_link=p1)
output_file('scatter_plotting.html', mode='cdn')
p = Column(p1, p2, p3, p4)
show(p)
def makePlot(self,filename,x0,y0,y_label, **optional):
TOOLS = "box_zoom,box_select,reset"
p = figure(tools=TOOLS, plot_width=1800, plot_height=300)
if ('x_link' in optional):
p0 = optional['x_link']
p.x_range = p0.x_range
p.scatter(x=x0, y=y0, marker='square', size=1, fill_color='red', source=self.source)
p.title.text = filename
p.title.text_color = 'orange'
p.xaxis.axis_label = 'T'
p.yaxis.axis_label = y_label
p.xaxis.minor_tick_line_color = 'red'
p.yaxis.minor_tick_line_color = None
return p
And my main looks like this (set to pass along up to 100K data points from the file):web
p = readMyFile(path+filename+extension, 100000)
t = p.time()
a = p.a()
b = p.b()
c = p.c()
d = p.d()
v = bokehPlot(filename, t, a, b, c, d)
The variables t, a, b, c, and d are type numpy ndarray.
I've managed to link the plots so I can pan and zoom them all from one graph. I would like to grab a cluster of data from one plot and see them highlighted, along with the corresponding values (at the same t values) highlighted on the other graphs.
In this code, I can draw a selection box, but it just remains for a moment, then disappears, and I see no effect on any plot. How is the box_select linked to the source and what causes the plots to redraw?
This is just one step in trying to familiarize myself with Bokeh. My next goal will be to use TSNE to cluster my data and show the clusters with synchronized colors in each graph. But first, I want to understand the mechanics of using the column data set here. In the sample code, for example, I don't see any explicit connection between the box_select operation and the source variable and what causes the plot to redraw.
My understanding is that the BooleanFilter, the IndexFilter and the GroupFilter can be used to filter the data in one of your plots before rendering. If you only want the second plot to respond to events in the first plot then you should just use gridplot as suggested in the comment. As long as the plots have the same ColumnDataSource they should be linked.
from bokeh.layouts import gridplot
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show
source = ColumnDataSource(data=dict(x=[1, 2, 3, 4, 5],
y=[1, 2, 3, 4, 5],
z=[3, 5, 1, 6, 7]))
tools = ["box_select", "hover", "reset"]
p_0 = figure(plot_height=300, plot_width=300, tools=tools)
p_0.circle(x="x", y="y", size=10, hover_color="red", source=source)
p_1 = figure(plot_height=300, plot_width=300, tools=tools)
p_1.circle(x="x", y="z", size=10, hover_color="red", source=source)
show(gridplot([[p_0, p_1]]))