Position the legend outside the plot area with Bokeh - python

I am making a plot following the example found here
Unfortunately, I have 17 curves I need to display, and the legend overlaps them. I know I can create a legend object that can be displayed outside the plot area like here, but I have 17 curves so using a loop is much more convenient.
Do you know how to combine both methods?

Ok, I found the solution. See the code below where I have just modified the interactive legend example:
import pandas as pd
from bokeh.palettes import Spectral4
from bokeh.plotting import figure, output_file, show
from bokeh.sampledata.stocks import AAPL, IBM, MSFT, GOOG
from bokeh.models import Legend
from bokeh.io import output_notebook
output_notebook()
p = figure(plot_width=800, plot_height=250, x_axis_type="datetime", toolbar_location='above')
p.title.text = 'Click on legend entries to mute the corresponding lines'
legend_it = []
for data, name, color in zip([AAPL, IBM, MSFT, GOOG], ["AAPL", "IBM", "MSFT", "GOOG"], Spectral4):
df = pd.DataFrame(data)
df['date'] = pd.to_datetime(df['date'])
c = p.line(df['date'], df['close'], line_width=2, color=color, alpha=0.8,
muted_color=color, muted_alpha=0.2)
legend_it.append((name, [c]))
legend = Legend(items=legend_it)
legend.click_policy="mute"
p.add_layout(legend, 'right')
show(p)

I'd like to expand on joelostbloms answer.
It is also possible to pull out the legend from an existing plot and add it
somewhere else after the plot has been created.
from bokeh.palettes import Category10
from bokeh.plotting import figure, show
from bokeh.sampledata.iris import flowers
# add a column with colors to the data
colors = dict(zip(flowers['species'].unique(), Category10[10]))
flowers["color"] = [colors[species] for species in flowers["species"]]
# make plot
p = figure(height=350, width=500)
p.circle("petal_length", "petal_width", source=flowers, legend_group='species',
color="color")
p.add_layout(p.legend[0], 'right')
show(p)

It is also possible to place legends outside the plot areas for auto-grouped, indirectly created legends. The trick is to create an empty legend and use add_layout to place it outside the plot area before using the glyph legend_group parameter:
from bokeh.models import CategoricalColorMapper, Legend
from bokeh.palettes import Category10
from bokeh.plotting import figure, show
from bokeh.sampledata.iris import flowers
color_mapper = CategoricalColorMapper(
factors=[x for x in flowers['species'].unique()], palette=Category10[10])
p = figure(height=350, width=500)
p.add_layout(Legend(), 'right')
p.circle("petal_length", "petal_width", source=flowers, legend_group='species',
color=dict(field='species', transform=color_mapper))
show(p)

A note on visibility as the above answers, while useful, didn't see me successfully place the legend below the plot and others may come across this too.
Where the plot_height or height are set for the figure as so:
p = figure(height=400)
But the legend is created as in Despee1990's answer and then placed below the plot as so:
legend = Legend(items=legend_it)
p.add_layout(legend, 'below')
Then the legend is not displayed, nor the plot.
If the location is changed to the right:
p.add_layout(legend, 'right')
...then the legend is only displayed where the items fit within the figure plot height. I.e. if you have a plot height of 400 but the legend needs a height of 800 then you won't see the items that don't fit within the plot area.
To resolve this either remove the plot height from the figure entirely or specify a height sufficient to include the height of the legend items box.
i.e. either:
p = figure()
or if Legend required height = 800 and glyph required height is 400:
p = figure(plot_height=800)
p.add_layout(legend, 'below')

Related

Is there a way to add a 3rd, 4th and 5th y axis using Bokeh?

I would like to add multiple y axes to a bokeh plot (similar to the one achieved using matplotlib in the attached image).
Would this also be possible using bokeh? The resources I found demonstrate a second y axis.
Thanks in advance!
Best Regards,
Pranit Iyengar
Yes, this is possible. To add a new axis to the figure p use p.extra_y_ranges["my_new_axis_name"] = Range1d(...). Do not write p.extra_y_ranges = {"my_new_axis_name": Range1d(...)} if you want to add multiple axis, because this will overwrite and not extend the dictionary. Other range objects are also valid, too.
Minimal example
from bokeh.plotting import figure, show, output_notebook
from bokeh.models import LinearAxis, Range1d
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')
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), 'left')
p.line(data_x, data_y, color=c, y_range_name=name)
show(p)
Output
Official example
See also the twin axis example (axis) on the official webpage. This example uses the same syntax with only two axis. Another example is the twin axis example for models.

trying to plot mouse hovering interactive graph with bokeh in python

I am new to bokeh and trying to plot a graph.I have three lists say,
from bokeh.plotting import figure, show
x=[1,2,3,4,5,6,7,8,9,10,11]
y=[1,2,1,1,1,1,3,4,5,5,5]
c=[50,40,30,20,10,60,50,40,30,20,10]
p = figure(x_axis_type="datetime", title="Range", plot_height=350, plot_width=800)
p.xgrid.grid_line_color=None
p.ygrid.grid_line_alpha=0.5
p.xaxis.axis_label = 'Time'
p.yaxis.axis_label = 'Value'
p.line(x,y)
show(p)
I want to have a sort of time series like step function graph, where the x-axis is a continuous series of time (the list x) and the y-axis is the event (the list y) i.e. y-axis should have markings only till 5 (like 1,2,3,4,5) and the plotted points when hovered over by mouse pointer should show the corresponding value stored in c.
so for example for when time is x=1, then y=1, and c=50.
so that I know by looking at the x-axis at what time where the person was (out of 5 places 1,2,3,4,5 on the y-axis) and by placing my mouse what was the value at that time (the list c).
If you want to show tooltips only at specific points I wold add circles and set them as the only hover renderers like this:
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool
x=[1,2,3,4,5,6,7,8,9,10,11]
y=[1,2,1,1,1,1,3,4,5,5,5]
c=[50,40,30,20,10,60,50,40,30,20,10]
source = ColumnDataSource({'x': x, 'y': y, 'c': c})
p = figure(x_axis_type="datetime", title="Range", plot_height=350, plot_width=800, tooltips = [('time', '#x'), ('place', '#y'), ('value','#c')])
p.xgrid.grid_line_color=None
p.ygrid.grid_line_alpha=0.5
p.xaxis.axis_label = 'Time'
p.yaxis.axis_label = 'Value'
lines = p.line('x','y', source=source)
circles = p.circle('x','y', source=source)
p.select_one(HoverTool).renderers = [circles]
show(p)

Make the colour AND marker of bokeh plot scatter points dependent on dataframe values

I've been playing around with bokeh in order to get an interactive scatter plot, with tooltips and interactive legends etc.
Currently I am able to set the colour of the points using the values of a column in the pandas dataframe behind the plot. However I'm wondering if it's possible to set the marker type (diamond, circle, square etc.) as well, using another column in the dataframe?
I appreciate this would mean you'd need a double legend, but hopefully this wouldn't be too much of a problem.
This can be accomplished with marker_map and CDS filters:
from bokeh.plotting import figure, show, output_file
from bokeh.sampledata.iris import flowers
from bokeh.transform import factor_cmap, factor_mark
SPECIES = ['setosa', 'versicolor', 'virginica']
MARKERS = ['hex', 'circle_x', 'triangle']
p = figure(title = "Iris Morphology", background_fill_color="#fafafa")
p.xaxis.axis_label = 'Petal Length'
p.yaxis.axis_label = 'Sepal Width'
p.scatter("petal_length", "sepal_width", source=flowers, legend="species",
fill_alpha=0.4, size=12,
marker=factor_mark('species', MARKERS, SPECIES),
color=factor_cmap('species', 'Category10_3', SPECIES))
show(p)

Coloring lines in a Bokeh Plot

I have a Bokeh App that contains a Line Plot. This plot has 10 lines in it. How do I color the lines with different shades of the same color?
Is there any way I can generate a list of different shades of the same color without hardcoding it?
You could use a palette from bokeh.palettes:
from bokeh.plotting import figure, output_file, show
from bokeh.palettes import Blues9
output_file('palette.html')
p = figure()
p.scatter(x=[0,1,2,3,4,5,6,7,8], y=[0,1,2,3,4,5,6,7,8], radius=1, fill_color=Blues9)
show(p)
https://docs.bokeh.org/en/latest/docs/reference/palettes.html

How to put the legend on first subplot of seaborn.FacetGrid?

I have a pandas DataFrame df which I visualize with subplots of a seaborn.barplot. My problem is that I want to move my legend inside one of the subplots.
To create subplots based on a condition (in my case Area), I use seaborn.FacetGrid. This is the code I use:
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns
# .. load data
grid = sns.FacetGrid(df, col="Area", col_order=['F1','F2','F3'])
bp = grid.map(sns.barplot,'Param','Time','Method')
bp.add_legend()
bp.set_titles("{col_name}")
bp.set_ylabels("Time (s)")
bp.set_xlabels("Number")
sns.plt.show()
Which generates this plot:
You see that the legend here is totally at the right, but I would like to have it inside one of the plots (for example the left one) since my original data labels are quite long and the legend occupies too much space. This is the example for only 1 plot where the legend is inside the plot:
and the code:
mask = df['Area']=='F3'
ax=sns.barplot(x='Param',y='Time',hue='Method',data=df[mask])
sns.plt.show()
Test 1:
I tried the example of an answer where they have the legend in one of the subplots:
grid = sns.FacetGrid(df, col="Area", col_order=['F1','F2','F3'])
bp = grid.map(sns.barplot,'Param','Time','Method')
Ax = bp.axes[0]
Boxes = [item for item in Ax.get_children()
if isinstance(item, matplotlib.patches.Rectangle)][:-1]
legend_labels = ['So1', 'So2', 'So3', 'So4', 'So5']
# Create the legend patches
legend_patches = [matplotlib.patches.Patch(color=C, label=L) for
C, L in zip([item.get_facecolor() for item in Boxes],
legend_labels)]
# Plot the legend
plt.legend(legend_patches)
sns.plt.show()
Note that I changed plt.legend(handles=legend_patches) did not work for me therefore I use plt.legend(legend_patches) as commented in this answer. The result however is:
As you see the legend is in the third subplot and neither the colors nor labels match.
Test 2:
Finally I tried to create a subplot with a column wrap of 2 (col_wrap=2) with the idea of having the legend in the right-bottom square:
grid = sns.FacetGrid(df, col="MapPubName", col_order=['F1','F2','F3'],col_wrap=2)
but this also results in the legend being at the right:
Question: How can I get the legend inside the first subplot? Or how can I move the legend to anywhere in the grid?
You can set the legend on the specific axes you want, by using grid.axes[i][j].legend()
For your case of a 1 row, 3 column grid, you want to set grid.axes[0][0].legend() to plot on the left hand side.
Here's a simple example derived from your code, but changed to account for the sample dataset.
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns
df = sns.load_dataset("tips")
grid = sns.FacetGrid(df, col="day")
bp = grid.map(sns.barplot,"time",'total_bill','sex')
grid.axes[0][0].legend()
bp.set_titles("{col_name}")
bp.set_ylabels("Time (s)")
bp.set_xlabels("Number")
sns.plt.show()
Use the legend_out=False option.
If you are making a faceted bar plot, you should use factorplot with kind=bar. Otherwise, if you don't explicitly specify the order for each facet, it is possible that your plot will end up being wrong.
import seaborn as sns
tips = sns.load_dataset("tips")
sns.factorplot(x="sex", y="total_bill", hue="smoker", col="day",
data=tips, kind="bar", aspect=.7, legend_out=False)

Categories

Resources