I’m trying to register an “on_click” callback function in python when running in offline notebook mode and can’t seem to find any information on how to do this. I’ve only found examples for doing this using javascript and online/non-notebook mode. How are click/hover callbacks exposed in offline mode in python? Basically I need to call a function do display another graph when the user clicks on a point in a scatter3D.
(I really hope the answer is not that they aren’t or I’m going to have to switch to a different plotting package which I would prefer not to have to do.)
Hey @jsharpe –
Great question. Click events are not exposed in offline mode in IPython notebook. It wouldn’t be too hard to add them though, the code would closely replicate the code that exposed these events in online mode. Instead of passing these events through an IFrame, you would bind to plotly.js’s native click and hover events.
This feature isn’t under high demand, so it’s not on our immediate roadmap unless a company would like to sponsor the work. Of course, if the community would like to contribute this feature, I’m glad to guide the work and review a PR.
Thanks much for the super quick reply. At least now I know I’m not going crazy or missed something obvious. Your suggestions for it to be added seem reasonable, however given our current time constraints it’s quicker for us to just to switch to using Bokeh for this project.
Assumes that you have the offline output in a single html file, for a single plot.
Assumes that your on events are named the same as the event handlers.
Requires Beautiful Soup 4.
Assumes you’ve got lxml installed.
Developed with Plotly 2.2.2
Code Snippet:
import bs4
def add_custom_plotly_events(
filename,
events = {
"plotly_click": "function plotly_click(data) { console.log(data); }",
"plotly_hover": "function plotly_hover(data) { console.log(data); }"
},
prettify_html = True
):
# what the value we're looking for the javascript
find_string = "Plotly.newPlot"
# stop if we find this value
stop_string = "then(function(myPlot)"
def locate_newplot_script_tag(soup):
scripts = soup.find_all('script')
script_tag = soup.find_all(string=re.compile(find_string))
if len(script_tag) == 0:
raise ValueError("Couldn't locate the newPlot javascript in {}".format(filename))
elif len(script_tag) > 1:
raise ValueError("Located multiple newPlot javascript in {}".format(filename))
if script_tag[0].find(stop_string) > -1:
raise ValueError("Already updated javascript, it contains:", stop_string)
return script_tag[0]
def split_javascript_lines(new_plot_script_tag):
return new_plot_script_tag.string.split(";")
def find_newplot_creation_line(javascript_lines):
for index, line in enumerate(javascript_lines):
if line.find(find_string) > -1:
return index, line
raise ValueError("Missing new plot creation in javascript, couldn't find:", find_string)
def join_javascript_lines(javascript_lines):
# join the lines with javascript line terminator ;
return ";".join(javascript_lines)
def register_on_events(events):
on_events_registration = []
for function_name in events:
on_events_registration.append("myPlot.on('{}', {})".format(
function_name, function_name
))
return on_events_registration
# load the file
with open(filename) as inf:
txt = inf.read()
soup = bs4.BeautifulSoup(txt, "lxml")
new_plot_script_tag = locate_newplot_script_tag(soup)
javascript_lines = split_javascript_lines(new_plot_script_tag)
line_index, line_text = find_newplot_creation_line(javascript_lines)
on_events_registration = register_on_events(events)
# replace whitespace characters with actual whitespace
# using + to concat the strings as {} in format
# causes fun times with {} as the brackets in js
# could possibly overcome this with in ES6 arrows and such
line_text = line_text + ".then(function(myPlot) { " + join_javascript_lines(on_events_registration) +" })".replace('\n', ' ').replace('\r', '')
# now add the function bodies we've register in the on handles
for function_name in events:
javascript_lines.append(events[function_name])
# update the specific line
javascript_lines[line_index] = line_text
# update the text of the script tag
new_plot_script_tag.string.replace_with(join_javascript_lines(javascript_lines))
# save the file again
with open(filename, "w") as outf:
# tbh the pretty out is still ugly af
if prettify_html:
for line in soup.prettify(formatter = None):
outf.write(str(line))
else:
outf.write(str(soup))
Hi I am new to scripting JS. With the above code snippet, I am unable to figure out how to use it my code where I have a code snippet to generate the html file created through offline Plotly. Some more explanation or sharing your rest of the code would help. Thanks !