I have a html file generated by bokeh
gridplot
containing multiple figures.
My use case is:
Could you please show me how to do it?
Here is an example code to quickly generate a gridplot with 2 figures.
import numpy as np
from bokeh.io import save
from bokeh.layouts import gridplot
from bokeh.plotting import figure
x = np.linspace(0, 4 * np.pi, 100)
y = np.sin(x)
TOOLS = "pan,wheel_zoom,box_zoom,reset,save,box_select"
p1 = figure(title="Legend Example", tools=TOOLS)
p1.circle(x, y, legend_label="sin(x)")
p1.circle(x, 2 * y, legend_label="2*sin(x)", color="orange")
p1.circle(x, 3 * y, legend_label="3*sin(x)", color="green")
p1.legend.title = "Markers"
p2 = figure(title="Another Legend Example", tools=TOOLS)
p2.circle(x, y, legend_label="sin(x)")
p2.line(x, y, legend_label="sin(x)")
p2.line(
x,
2 * y,
legend_label="2*sin(x)",
line_dash=(4, 4),
line_color="orange",
line_width=2,
)
p2.square(x, 3 * y, legend_label="3*sin(x)", fill_color=None, line_color="green")
p2.line(x, 3 * y, legend_label="3*sin(x)", line_color="green")
p2.legend.title = "Lines"
p2.x_range = p1.x_range
save(gridplot([p1, p2], ncols=2, width=400, height=400))
Thanks.
Ok, first of all, I don't know if it's intentional or not, but I guess not, but when you move one graphic you move the other one too, so if you already implement the Tap (zoom), even if you zoom only on one, the other one will move.
So with this in mind we have to change this. The easiest way is to create separate range objects for p1
and p2
.
p1_xrange = figure().x_range
p1_yrange = figure().y_range
p2_xrange = figure().x_range
p2_yrange = figure().y_range
I think you can also use the deepcopy library but there is no need to use it here.
After that you just apply it to your figure:
p1 = figure(title="Legend Example", tools=TOOLS, x_range=p1_xrange, y_range=p1_yrange)
And the same for p2
p2 = figure(title="Another Legend Example", tools=TOOLS, x_range=p2_xrange, y_range=p2_yrange)
With that fixed, now let's move to the first point.
For this we are going to use the CustomJS
module of bokeh.models
. Basically we need to create an event for each figure when it is touched.
So we create a function that does this zoom:
def tap_callback(p):
custom_tap_code = CustomJS(args=dict(p=p), code="""
p.x_range.start = cb_obj.x - (p.x_range.end - p.x_range.start)/4;
p.x_range.end = cb_obj.x + (p.x_range.end - p.x_range.start)/4;
p.y_range.start = cb_obj.y - (p.y_range.end - p.y_range.start)/4;
p.y_range.end = cb_obj.y + (p.y_range.end - p.y_range.start)/4;
""")
return custom_tap_code
And then we create the event for both figures:
p1.js_on_event('tap', tap_callback(p1))
p2.js_on_event('tap', tap_callback(p2))
With that you can now zoom as you wish.
First of all we have to create a button, for this we are using the Button
method of bokeh.models
. And we'll do the same as we did for the zoom, we have to create a CustomJS that from JavaScript downloads the images individually (of course we can't use the export_png
function of bokeh, because the code is saved in HTML using JS).
So to download the images a bit more complicated, because bokeh introduce some shadow-root
elements and we cannot get the elements as usual, using getElementsByTagName
or getElementsByClassName
. Instead, we have to use shadowRoot.querySelector
(at least that's the easiest way I've found that works for me).
So here is the code for that:
# create the button and attach the callback function to its 'click' event
button = Button(label="Save all figures")
callback = CustomJS(args=dict(p1=p1), code="""
// Because there are some shadow-root (open), we can't use the normal way to get the canvas element
let shadow_root1 = document.querySelector( '.bk-GridPlot' );
let shadow_root2 = shadow_root1.shadowRoot.querySelector('.bk-GridBox');
let shadow_root3 = shadow_root2.shadowRoot.querySelectorAll('.bk-Figure');
shadow_root3.forEach((figure) => {
let shadow_root4 = figure.shadowRoot.querySelector('.bk-Canvas');
let canvas = shadow_root4.shadowRoot.querySelector('canvas');
let url = canvas.toDataURL('image/png');
let downloadLink = document.createElement('a');
downloadLink.setAttribute('download', 'CanvasAsImage.png');
downloadLink.setAttribute('href', url);
downloadLink.click();
});
""")
button.js_on_click(callback)
And finally if we put it all together this would be the final code:
import numpy as np
from bokeh.io import save
from bokeh.layouts import gridplot
from bokeh.plotting import figure
from bokeh.models import CustomJS, Button
def tap_callback(p):
custom_tap_code = CustomJS(args=dict(p=p), code="""
p.x_range.start = cb_obj.x - (p.x_range.end - p.x_range.start)/4;
p.x_range.end = cb_obj.x + (p.x_range.end - p.x_range.start)/4;
p.y_range.start = cb_obj.y - (p.y_range.end - p.y_range.start)/4;
p.y_range.end = cb_obj.y + (p.y_range.end - p.y_range.start)/4;
""")
return custom_tap_code
x = np.linspace(0, 4 * np.pi, 100)
y = np.sin(x)
TOOLS = "pan,wheel_zoom,box_zoom,reset,save,box_select"
# create separate range objects for p1 and p2
p1_xrange = figure().x_range
p1_yrange = figure().y_range
p2_xrange = figure().x_range
p2_yrange = figure().y_range
p1 = figure(title="Legend Example", tools=TOOLS, x_range=p1_xrange, y_range=p1_yrange)
p1.name = "p1"
p1.circle(x, y, legend_label="sin(x)")
p1.circle(x, 2 * y, legend_label="2*sin(x)", color="orange")
p1.circle(x, 3 * y, legend_label="3*sin(x)", color="green")
p1.legend.title = "Markers"
p1.js_on_event('tap', tap_callback(p1))
p2 = figure(title="Another Legend Example", tools=TOOLS, x_range=p2_xrange, y_range=p2_yrange)
p2.name = "p2"
p2.circle(x, y, legend_label="sin(x)")
p2.line(x, y, legend_label="sin(x)")
p2.line(
x,
2 * y,
legend_label="2*sin(x)",
line_dash=(4, 4),
line_color="orange",
line_width=2,
)
p2.square(x, 3 * y, legend_label="3*sin(x)", fill_color=None, line_color="green")
p2.line(x, 3 * y, legend_label="3*sin(x)", line_color="green")
p2.legend.title = "Lines"
p2.js_on_event('tap', tap_callback(p2))
# create the button and attach the callback function to its 'click' event
button = Button(label="Save all figures")
callback = CustomJS(args=dict(p1=p1), code="""
// Because there are some shadow-root (open), we can't use the normal way to get the canvas element
let shadow_root1 = document.querySelector( '.bk-GridPlot' );
let shadow_root2 = shadow_root1.shadowRoot.querySelector('.bk-GridBox');
let shadow_root3 = shadow_root2.shadowRoot.querySelectorAll('.bk-Figure');
shadow_root3.forEach((figure) => {
let shadow_root4 = figure.shadowRoot.querySelector('.bk-Canvas');
let canvas = shadow_root4.shadowRoot.querySelector('canvas');
let url = canvas.toDataURL('image/png');
let downloadLink = document.createElement('a');
downloadLink.setAttribute('download', 'CanvasAsImage.png');
downloadLink.setAttribute('href', url);
downloadLink.click();
});
""")
button.js_on_click(callback)
save(gridplot([p1, p2, button], ncols=3, width=400, height=400))
I know this is probably not the best way to do this, but at least it's the one that worked for me. I hope it works for you too.