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)
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.
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 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)
From the bokeh examples
from bokeh.charts import HeatMap, output_file, show
data = {'fruit': ['apples']*3 + ['bananas']*3 + ['pears']*3,
'fruit_count': [4, 5, 8, 1, 2, 4, 6, 5, 4],
'sample': [1, 2, 3]*3}
hm = HeatMap(data, x='fruit', y='sample', values='fruit_count',
title='Fruits', stat=None)
show(hm)
is there a workaround for changing the order in which the labels are displayed? For example, if I wanted to show pears first?
First, you should not use bokeh.charts. It was deprecated, and has been removed from core Bokeh to a separate bkcharts repo. It is completely unsupported and unmaintained. It will not see any new features, bugfixes, improvements, or documentation. It is a dead end.
There are two good options to create this chart:
1) Use the stable and well-supported bokeh.plotting API. This is slightly more verbose, but gives you explicit control over everything, e.g. the order if the categories. In the code below these are specified as x_range and y_range values to figure:
from bokeh.io import output_file, show
from bokeh.models import ColumnDataSource, LinearColorMapper
from bokeh.palettes import Spectral9
from bokeh.plotting import figure
from bokeh.transform import transform
source = ColumnDataSource(data={
'fruit': ['apples']*3 + ['bananas']*3 + ['pears']*3,
'fruit_count': [4, 5, 8, 1, 2, 4, 6, 5, 4],
'sample': ['1', '2', '3']*3,
})
mapper = LinearColorMapper(palette=Spectral9, low=0, high=8)
p = figure(x_range=['apples', 'bananas', 'pears'], y_range=['1', '2', '3'],
title='Fruits')
p.rect(x='fruit', y='sample', width=1, height=1, line_color=None,
fill_color=transform('fruit_count', mapper), source=source)
show(p)
This yields the output below:
You can find much more information (as well as live examples) about categorical data with Bokeh in the Handling Categorical Data sections of the User's Guide.
2) Look into HoloViews, which is a very high level API on top of Bokeh that is actively maintained by a team, and endorsed by the Bokeh team as well. A simple HeatMap in HoloViews is typically a one-liner as with bokeh.charts.
How can you disable scientific output of numbers on an axis in bokeh?
For example, I want 400000 and not 4.00e+5
In mpl: ax.get_xaxis().get_major_formatter().set_scientific(False)
You can disable scientific notation with this:
fig = plt.figure(title='xxx', x_axis_type='datetime')
fig.left[0].formatter.use_scientific = False
Note that as of Bokeh v0.9.1, Marek's answer will no longer work due to changes in the interface for Charts. The following code (from GitHub) is a fully-functional example of how to turn off scientific notation in a high level chart.
from bokeh.embed import components
from bokeh.models import Axis
from bokeh.charts import Bar
data = {"y": [6, 7, 2, 4, 5], "z": [1, 5, 12, 4, 2]}
bar = Bar(data)
yaxis = bar.select(dict(type=Axis, layout="left"))[0]
yaxis.formatter.use_scientific = False
script, div = components(bar)
print(script)
print(div)
The key lines are:
yaxis = bar.select(dict(type=Axis, layout="left"))[0]
yaxis.formatter.use_scientific = False
I was trying to turn off scientific notation from a logarithmic axis, and the above answers did not work for me.
I found this: python bokeh plot how to format axis display
In that spirit, this worked for me:
from bokeh.models import BasicTickFormatter
fig = plt.figure(title='xxx', x_axis_type='datetime',y_axis_type='log')
fig.yaxis.formatter = BasicTickFormatter(use_scientific=False)
To disable the scientific output in Bokeh, use use_scientific attribute of the formatter you use.
You can find more information regarding use_scientific attribute here:
a description of the attribute in the code of bokeh: BasicTickFormatter
class (line 28)
documentation of use_scientific attr
Example (originaly comes from Bokeh issues discussion):
from bokeh.models import Axis
yaxis = bar.chart.plot.select(dict(type=Axis, layout="left"))[0]
yaxis.formatter.use_scientific = False
bar.chart.show()