Bokeh plot using a function of values in ColumnDataSource - python

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.

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)

How to set size of mouse coordinates in matplotlib toolbar?

I want to increase the size of mouse coordinates (Right in the navigation toolbar) mpl.rcParams['font.size'] = 35 change size of text in the plot but not in the Toolbar.Is there a rcParams for this or some other way to set the size.
I have spent almost one day trying to solve this.
'''
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rcParams['font.size'] = 35
x=np.arange(0,100)
y=np.sin(np.pi*x/10)
plt.plot(x,y)
plt.title('Sin(x)')
plt.show()
'''
I am not sure whether it is possible to change that specific font size. However, as an alternative, it is possible to draw the mouse coordinates using the text method. To this end, I create a figure with two axes. The top axes is the smallest and is used to draw the coordinates of the mouse using the aforementioned text method, while the bottom axes is used for your plot. In addition, I have defined a function which will be called once the mouse moves. This function is responsible for updating the mouse coordinates. See the code below:
import numpy as np
import matplotlib.pyplot as plt
# Set option(s)
plt.rcParams['font.size'] = 15
# Generate data to plot
x = np.arange(0, 100)
y = np.sin(np.pi * x / 10)
# Create a figure with two axes:
# Top axes : Used to draw text containing the coordinates of the mouse
# Bottom axes: Used to plot the data
fig, ax = plt.subplots(nrows=2,
ncols=1,
# Note: default figsize is [6.4, 4.8] in inches
figsize=(6.4, 5.6),
# Using height_ratios, we make the top axes small
# and the bottom axes as big as usual
gridspec_kw={'height_ratios': [0.8 / 5.6, 4.8 / 5.6]}
)
# ax[0] is where the coordinates will be shown, so remove the x- and y-axis
ax[0].axis('off')
# Set default text on the top axes
default_text = "Cursor location will be shown here"
ax[0].text(0.5, 0.5, default_text, va='center', ha='center', color='grey')
# Plot the data on the bottom axes (ax[1])
ax[1].plot(x, y)
ax[1].set_title("Sin(x)")
# Define a function which is called when the location of the mouse changes
def update_mouse_coordinates(ax, text, event):
# Don't print coordinates if not on bottom axes
if event.inaxes == ax or event.xdata==None or event.ydata==None:
ax.texts[0].set_text(text)
plt.draw()
return
# Show mouse coordinates to user
ax.texts[0].set_text(f"x={event.xdata:.4f}; y={event.ydata:.4f}")
plt.draw()
plt.connect('motion_notify_event',
# Using a lambda expression, we supply the top axes and the default
# text to this function (our resulting function may only have one
# argument!)
lambda event: update_mouse_coordinates(ax[0], default_text, event)
)
# Show the plot
plt.show()
Whenever the mouse is outside of the bottom axes (where your plot is), it will look as follows:
As soon as your mouse is somewhere inside the bottom axes, you will get the following behaviour:
This is not a complete answer, but I see no way to put a few lines of code in a comment.
I was able to solve this problem in a Qt GUI application like this:
self.toolbar = NavigationToolbar2QT(self.view, self)
self.toolbar.setStyleSheet("font-size: 12px;")

Create Legend Label for Quad glyph - Bokeh

I have a Quad plot displaying 2 data-sets. I would like to add a legend to the plot, however I am not sure how to do this with the Quad glyph.
Previous examples have used 'legend' however this is now deprecated, and I've tried using
'legend_label' however this is does not work.
My ultimate goal is to use the legend to interactively display both datasets
# Convert dataframe to column data source
src1 = ColumnDataSource(Merged_Bins)
src2 = ColumnDataSource(Merged_Bins)
#------------------------------------------------------------------------------------------------
# Plot Histogram using Bokeh plotting library
#------------------------------------------------------------------------------------------------
plot = figure(y_range=Range1d(start=0, end=Max_Histogram_Value),sizing_mode="scale_width",width=3000,height= 600,
title= "Histogram Plot",
x_axis_label="Time (ms)",
y_axis_label="Count",toolbar_location = "below")
plot.yaxis.ticker = FixedTicker(ticks=list(tick_vals))
glyph1=Quad(bottom=0, top='Delay1', left='left1',
right='right1', fill_color='#FF7F00',
line_color='black', fill_alpha=0.7,line_alpha=0.5,name="Option 2")
glyph1_plot=plot.add_glyph(src1, glyph1)
glyph2=Quad(bottom=0, top='Delay2', left='left2',
right='right2', fill_color='#616261',
line_color='#616261',line_alpha=0.1, fill_alpha=0.1,name="Original Design")
plot.add_glyph(src2, glyph2)
# Add hover tool for when mouse is over data
hover1 = HoverTool(tooltips=[('Delay Envelope', '#Bin_interval'),('Count', '#Delay1'),('Count Original', '#Delay2')],mode='vline',renderers=[glyph1_plot])
plot.add_tools(hover1)
plot.legend.location = "top_left"
plot.legend.click_policy="hide"
# Set autohide to true to only show the toolbar when mouse is over plot
plot.toolbar.autohide = True
script, div = components(plot)
show(plot)
It works just fine if you use the Figure.quad method instead of manually calling Figure.add_glyph with an explicitly created instance of Quad. All legen_* arguments are parsed by glyph methods of the Figure class - the glyph classes themselves do not use them at all.
from bokeh.io import show
from bokeh.plotting import figure
p = figure()
p.quad(-1, 1, 1, -1, legend_label='Hello')
p.quad(1, 3, 3, 1, color='green', legend_label='there')
show(p)
Alternatively, if you really need the manual approach for some reason, you can also create a legend manually by creating an instance of the Legend class and by adding it to the figure with Figure.add_layout.
Also, on an unrelated note - your plot looks like it was created with vbar instead of quad because all bars seem to have the same width. If so, perhaps using vbar would be simpler in your case.

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