I am working a lot with selections that users can modify. I have noticed that bokeh does not distinguish between select all and select nothing. Could this be true?
So for some column data source with data key x the following results in identical selections:
source.selected.indices = []
and
all_indices = list(range(len(source.data['x'])))
source.selected.indices = all_indices
How do I deselect all elements of a data source?
EDIT: A minimal working example.
import bokeh.plotting
from bokeh.io import curdoc
from bokeh.models import ColumnDataSource
data = dict(x=[15,2,21], y=[8,8,6])
source = ColumnDataSource(data)
plot = bokeh.plotting.figure()
plot.circle(x='x', y='y', source=source)
source.selected.indices = [0,1,2]
source.selected.indices = [1]
source.selected.indices = []
curdoc().add_root(plot)
All circles are visualized as "selected" when source.selected.indices = [] and also when source.selected.indices = [0,1,2]. Clearly the selections are different and should not look the same, right?
The default action on selection is to "wash out" the non-selected points (by lowering their alpha and setting color to grey), but leave the selected points looking the way they were (which stands out against the non-selected points). If you want something different, there is a section in the Users Guide that describes how to configure Selected and Unselected Glyphs. E.g:
plot.circle([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], size=50,
# set visual properties for selected glyphs
selection_color="firebrick",
# set visual properties for non-selected glyphs
nonselection_fill_alpha=0.2,
nonselection_fill_color="blue",
nonselection_line_color="firebrick",
nonselection_line_alpha=1.0)
Related
first of all, in case I comment on any mistakes while writing this, sorry, English is not my first language.
I'm a begginer with Data vizualiation with python, I have a dataframe with 115 rows, and I want to do a scatterplot with 4 quadrants and show the values in R1 (image below for reference)
enter image description here
At moment this is my scatterplot. It's a football player dataset so I want to plot the name of the players name in the 'R1'. Is that possible?
enter image description here
You can annotate each point by making a sub-dataframe of just the players in a quadrant that you care about based on their x/y values using plt.annotate. So something like this:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
##### Making a mock dataset #################################################
names = ['one', 'two', 'three', 'four', 'five', 'six']
value_1 = [1, 2, 3, 4, 5, 6]
value_2 = [1, 2, 3, 4, 5, 6]
df = pd.DataFrame(zip(names, value_1, value_2), columns = ['name', 'v_1', 'v_2'])
#############################################################################
plt.rcParams['figure.figsize'] = (10, 5) # sizing parameter to make the graph bigger
ax1 = sns.scatterplot(x = value_1, y = value_2, s = 100) # graph code
# Code to make a subset of data that fits the specific conditions that I want to annotate
quadrant = df[(df.v_1 > 3) & (df.v_2 > 3)].reset_index(drop = True)
# Code to annotate the above "quadrant" on the graph
for x in range(len(quadrant)):
plt.annotate('Player: {}\nValue 1: {}\nValue 2: {}'.format(quadrant.name[x], quadrant.v_1[x], quadrant.v_2[x]),
(quadrant.v_1[x], quadrant.v_2[x])
Output graph:
If you're just working in the notebook and don't need to save the image with all the player's names, then using a "hover" feature might be a better idea. Annotating every player's name might become too busy for the graph, so just hovering over the point might work out better for you.
%matplotlib widget # place this or "%matplotlib notebook" at the top of your notebook
# This allows you to work with matplotlib graphs in line
import mplcursors # this is the module that allows hovering features on your graph
# using the same dataframe from above
ax1 = sns.scatterplot(x = value_1, y = value_2, s = 100)
#mplcursors.cursor(ax1, hover=2).connect("add") # add the plot to the hover feature of mplcursors
def _(sel):
sel.annotation.set_text('Player: {}\nValue 1: {}\nValue 2: {}'.format(df.name[sel.index], sel.target[0], sel.target[0])) # set the text
# you don't need any of the below but I like to customize
sel.annotation.get_bbox_patch().set(fc="lightcoral", alpha=1) # set the box color
sel.annotation.arrow_patch.set(arrowstyle='-|>', connectionstyle='angle3', fc='black', alpha=.5) # set the arrow style
Example outputs from hovering:
You can do two (or more) scatter plots on a single figure.
If I understand correctly what you want to do, you could separate your dataset in two :
Points for which you don't want the name to be plotted
Points for which you want the name to be plotted
You can then plot the second data set and display the name.
Without any other details on your problem, it is difficult to do more. You could edit your question and add a minimal example of your data set.
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 currently working with data from a sports related test. I want to visualize the (multidimensional) test-data of one athlete from several tests (different dates), so I made grouped vbar with the dates at the lowest level of grouping. Now I want to tap on one bar to select it and the corresponding ones from the same date should be selected (and highlighted), too.
Till now I was searching on stackoverflow with "[python][bokeh]taptool" query, I looked up the whole issues section on git/bokeh with the tag "taptool" and did a google with similar queries, but I can't find a matching thread.
To clarify my needs, I modified the grouped_bars_example from the bokeh repository. My goal is to select all bars of one manufacturer by clicking on one bar. (I know it's possible to hold shift-key for multiselections, but it is quiet annoying to select, for example, 6 corresponding bars out of 120 bars. Thatswhy I'm looking for an efficient way to do so with one click)
#### basic example code from ~/latest/docs/user_guide/categorical.html#grouped
from bokeh.io import show, output_file
from bokeh.models import ColumnDataSource, HoverTool, TapTool
from bokeh.plotting import figure
from bokeh.palettes import Spectral5
from bokeh.sampledata.autompg import autompg_clean as df
from bokeh.transform import factor_cmap
# output_file('bars.html')
# preparing data for figure
df.cyl = df.cyl.astype(str)
df.yr = df.yr.astype(str)
group = df.groupby(('cyl', 'mfr'))
source = ColumnDataSource(group)
index_cmap = factor_cmap('cyl_mfr', palette=Spectral5, factors=sorted(df.cyl.unique()), end=1)
# setting up figure
p = figure(plot_width=800, plot_height=300, title="Mean MPG by # Cylinders and Manufacturer",
x_range=group, toolbar_location=None, tools="")
# adding grouped vbar
p.vbar(x='cyl_mfr', top='mpg_mean', width=1, source=source,
line_color="white", fill_color=index_cmap, )
# figurestyling
p.y_range.start = 0
p.x_range.range_padding = 0.05
p.xgrid.grid_line_color = None
p.xaxis.axis_label = "Manufacturer grouped by # Cylinders"
p.xaxis.major_label_orientation = 1.2
p.outline_line_color = None
# adding Tools
p.add_tools(HoverTool(tooltips=[("MPG", "#mpg_mean"), ("Cyl, Mfr", "#cyl_mfr")]))
p.add_tools(TapTool())
#### my additional code
import pandas as pd
import sys
from bokeh.plotting import curdoc, figure
# redirect output in files (just for debugging)
save_stderr = sys.stderr
f_err = open('error.log', 'w')
sys.stderr = f_err
save_stdout = sys.stdout
f_info = open('info.log', 'w')
sys.stdout = f_info
# callbackfunction to obtain selected bars
def callback_tap(attr, old, new):
# write selected indices to file
output = source.selected['1d']['indices'] # indices of selected bars
print(output, type(output))
# make calculations only, if one bar is selected
if len(output) == 1:
# find all corresponding indices to manufacturer based on selected bar
# get manufacturer corresponding to retrieved index
man = source.data['cyl_mfr'][output][1]
# temporary DataFrame
tmp = pd.DataFrame(source.data['cyl_mfr'].tolist(), columns=['cyl', 'mfr'])
# look up all corresponding indices for manufacturer "man"
indices = tmp.index[tmp.mfr == man].values.tolist()
# assing list of indices
source.selected['1d']['indices'] = indices
# assigning callbackfunction
source.on_change('selected', callback_tap)
curdoc().add_root(p)
Please note, that I change the output to run a bokeh server in order to have custom python callback. For debugging reasons, I redirected the outputs to files.
In my callback function, the first part is working fine, and I retrieve the indicees of the selected bar. Additionally, I find the corresponding indicees with an DataFrame, but at the end I'm struggle to assign the new indicees in a way, that the vbar figure is updated.
I'll be very happy, if someone can help me.
Is there any way to interactively change legend label text in Bokeh?
I've read https://github.com/bokeh/bokeh/issues/2274 and How to interactively display and hide lines in a Bokeh plot? but neither are applicable.
I don't need to modify the colors or anything of more complexity than changing the label text but I can't find a way to do it.
I hope this answer can help others with similar issues.
There is a workaround to this problem: starting from version 0.12.3 your legends can be dynamically modified through a ColumnDataSource object used to generate the given elements. For example:
source_points = ColumnDataSource(dict(
x=[1, 2, 3, 4, 5, 6],
y=[2, 1, 2, 1, 2, 1],
color=['blue','red','blue','red','blue','red'],
category=['hi', 'lo', 'hi', 'lo', 'hi', 'lo']
))
self._figure.circle('x',
'y',
color='color',
legend='category',
source=source_points)
Then you should be able to update the legend by setting the category values again, like:
# must have the same length
source_points.data['category'] = ['stack', 'flow', 'stack', 'flow', 'stack', 'flow']
Note the relation between category and color. If you had something like this:
source = ColumnDataSource(dict(
x=[1, 2, 3, 4, 5, 6],
y=[2, 1, 2, 1, 2, 1],
color=['blue','red','blue','red','blue','red'],
category=['hi', 'hi', 'hi', 'lo', 'hi', 'lo']
))
Then the second hi would show up blue as well. It only matches the first occurrence.
As of Bokeh 0.12.1 it does not look like this is currently supported. Legend objects have a legends property that maps the text to a list of glyphs:
{
"foo": [circle1],
"bar": [line2, circle2]
}
Ideally, you could update this legends property to cause it to re-render. But looking at the source code it appears the value is used at initialization, but there is no plumbing to force a re-render if the value changes. A possible workaround could be to change the value of legends then also immediately set some other property that does trigger a re-render.
In any case making this work on update should not be much work, and would be a nice PR for a new contributor. I'd encourage you to submit a feature request issue on the GitHub issue tracker and, if you have the ability a Pull Request to implement it (we are always happy to help new contributors get started and answer questions)
In my case, I made it work with the next code:
from bokeh.plotting import figure, show
# Create and show the plot
plt = figure()
handle = show(plt, notebook_handle=True)
# Update the legends without generating the whole plot once shown
for legend in plt.legend:
for legend_item, new_value in zip(legend.items, new_legend_values):
legend_item.label['value'] = new_value
push_notebook(handle=handle)
In my case, I was plotting some distributions, and updating then interactively (like an animation of the changes in the distributions). In the legend, I have the parameters of the distribution over time, which I need to update at each iteration, as they change.
Note that this code only works in a Jupyter notebook.
I ended up just redrawing the entire graph each times since the number of lines also varied in my case.
A small working Jupyter notebook example:
from bokeh.io import show
from bokeh.plotting import figure
from bokeh.palettes import brewer
from math import sin, pi
output_notebook()
def update(Sine):
p = figure()
r = []
for i in range(sines.index(Sine) + 1):
y = [sin(xi/(10*(i+1))) for xi in x]
r.append(p.line(x, y, legend=labels[i], color=colors[i], line_width = 3))
show(p, notebook_handle=True)
push_notebook()
sines = ["one sine", "two sines", "three sines"]
labels = ["First sine", "second sine", "Third sine"]
colors = brewer['BuPu'][3]
x = [i for i in range(100)]
interact(update, Sine=sines)