4

Is it possible to add click events to a Plotly scatter plot (offline mode in Python)?

As an example, I want to change the shape of a set of scatter points upon being clicked.

What I tried so far

My understanding from reading other questions from the site (with no clear answer) is that I may have to produce the html and then edit it after the fact by putting in javascript code? So I could write a javascript function, save it off to my_js.js and then link to it from the html?

Jesper - jtk.eth
  • 7,026
  • 11
  • 36
  • 63

2 Answers2

3

I've been doing some work with offline plots in plotly and had the same challenge.

Here's a kludge I've come up with which my prove as inspiration for others.

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))
Ryan Collingwood
  • 392
  • 5
  • 11
  • How do you use this function? If you could show a (basic) example, that would be great! – Kristada673 May 17 '19 at 07:21
  • As part of report generation process I had some plotly scatter plots (aggregations of state ,and below state aggregations of areas) that were generated. I needed them to be offline plots due to firewall fun times. I used the onclick to move between the State level aggregation to drill down into the area level aggregation. – Ryan Collingwood May 20 '19 at 07:03
1

According to Click events in python offline mode? on Plotly's community site this is not supported, at least as of December 2015.

That post does contain some hints as to how to implement this functionality yourself, if you're feeling adventurous.

Jason Sundram
  • 12,225
  • 19
  • 71
  • 86