Click events in python offline mode?


#1

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


#2

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.


#3

Hi chriddyp,

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.


#4

Any news on this topic? Is it possible now register an “on_click” callback function in python? any view in the roadmap?


#5

Also, would love news on this feature! It would be very valuable


#6

I recommend checking out Dash to do things like this. Here’s the chapter on interactive graphing: https://plot.ly/dash/interactive-graphing


#7

Continuing the discussion from Click events in python offline mode?:

In the absence of a better solution I’ve detailed a kludge for adding events to offline plots: https://stackoverflow.com/a/51182229/2805700

Some limitations:

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