trying to plot mouse hovering interactive graph with bokeh in python - 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)

Related

Bokeh plot using a function of values in ColumnDataSource

I'd like to plot a glyph using a function of the values in a ColumnDataSource instead of the raw values.
As a minimum example, suppose I want to plot a point that the user can drag, which moves another point around. The position of the second point is some arbitrary function of the position of the first point.
I can draw the second point right on top of the first point like so:
from bokeh.plotting import figure, show
from bokeh.models import PointDrawTool, ColumnDataSource, Circle
p = figure(x_range=(0, 10), y_range=(0, 10), tools=[],
title='Point Draw Tool')
source = ColumnDataSource({
'x': [1], 'y': [1], 'color': ['red']
})
# plot a point at x,y
renderer = p.scatter(x='x', y='y', source=source, color='color', size=10)
# create a circle at the same position as the point
glyph = Circle(x='x',y='y', size=30)
p.add_glyph(source, glyph)
# allow the user to move the point at x,y
draw_tool = PointDrawTool(renderers=[renderer], empty_value='black')
p.add_tools(draw_tool)
p.toolbar.active_tap = draw_tool
show(p)
This plot draws the second point with exactly the same (x,y) coordinates as the first point.
Now, suppose I'd like to draw the second point at some arbitrary function of the first point (f(x,y), g(x,y)) for a pair of arbitrary functions f() and g(). To make it simple, let's say the coordinates of the new point are (2x, 2y) .
Can someone tell me how to do this?
I'd like to do something like
glyph = Circle(x=2*'x',y=2*'y', size=30)
p.add_glyph(source, glyph)
Clearly this is not how to do it, but hopefully this makes it clear what I'm trying to do. In general, I'd like to do something like:
glyph = Circle(x=f('x','y'), y=g('x','y'), size=30)
p.add_glyph(source, glyph)
I have tried something silly like the following:
def get_circle(x,y):
new_x = 2*x
new_y = 2*y
return Circle(x=new_x, y=new_y, size=30)
You can create a ColumnDataSource and use a CustomJS to apply changes to your data.
In the example below I create a default source, add two renderers to a figure, define a JavaScript callback and excecute the callback every time the source changes.
Because I link the only one renderer to the PointDrawTool, the second renderer is updated, after I moved the frist renderer.
Minimal Example
from bokeh.plotting import figure, show, output_notebook
from bokeh.models import CustomJS, ColumnDataSource, PointDrawTool
output_notebook()
source = ColumnDataSource(dict(
x1=[1,3],
y1=[2,3],
x2=[2,6],
y2=[6,9]
))
p = figure(width=300, height=300)
r1 = p.circle(x='x1', y='y1', color='blue', source=source, size=5)
r2 = p.triangle(x='x2', y='y2', color='green', source=source, size=5)
draw_tool = PointDrawTool(renderers=[r1], empty_value='black')
p.add_tools(draw_tool)
callback = CustomJS(args=dict(source=source),
code="""
function f(x) {
return x*2;
};
function g(y) {
return y*3;
};
let data = source.data
data['x2'] = data['x1'].map(f)
data['y2'] = data['y1'].map(g)
source.change.emit()
"""
)
source.js_on_change('data', callback)
show(p)
Result
Comment
The example can have even more cirlces and triangles but at the moment only one can be moved and updated at a time and the order matters.
Really hope this helps you.

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.

using bokeh to create a bar graph

there is an example on the bokeh website:
https://docs.bokeh.org/en/latest/docs/gallery/bar_nested.html
but it does not work on my Jupiter notebook.
I have the following data frame:
precision recall f1
Random Forest 0.493759 1.0 0.661096
XGBoost 0.493759 1.0 0.661096
I want to build a graph that compares the two models on these 3 metrics.
But to start, I just wanted to compare one metric. this is my non-working code:
from bokeh.io import show, output_file
from bokeh.models import ColumnDataSource, FactorRange
from bokeh.plotting import figure
data = pd.DataFrame({'precision':[percision_rf,percision_xgb],'recall':[recall_rf,recall_xgb],'f1':[f1_rf,f1_xgb]})
data.rename({0:'Random Forest',1:'XGBoost'}, inplace=True)
source = ColumnDataSource(data=data)
p = figure()
p.vbar(x='Random Forest', top=0.9, width=0.9, source=source)
p.y_range.start = 0
p.x_range.range_padding = 0.1
p.xaxis.major_label_orientation = 1
p.xgrid.grid_line_color = None
show(p)
There is an example of a simple bar graph on the bokeh website, but it is not using a ColumnDataSource.
When you pass a DataFrame to a ColumnDataSource, Bokeh makes CDS columns out of the columns of the DataFrame. Those are what you can refer to in the glyph methods, and then the glyph will draw glyphs for all values of that column. For example, in the example above, you could do
# plot bars for every precision value along the x axis
p.vbar(x='precision', top=0.9, width=0.9, source=source)
All Bokeh glyphs are inherently "vectorized" in this way.
In the above code, x='Random Forest' is not meaningful to pass to vbar, because there is no column in the DataFrame (and hence no column in the CDS) called "Random Forest".

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)

Position the legend outside the plot area with Bokeh

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')

Categories

Resources