Can I (quickly) highlight a data point that has just been clicked on in a line plot?


I have Plotly line plots where users can click a data point to pre-select it, mess arounds with widgets, and validate their selection with a button. The problem is that when they click a data point, there’s no visual feedback that it has been pre-selected. Can I (for instance) represent this one point with a marker instead of a line once it has been clicked?

Or do I need to do something much more cumbersome, like adding an entire trace for “highlights” that will only ever contain one data point represented with a marker, and update it whenever there’s a click? It seems like there should be a simpler way.

This is the best I could come up with. When I create the plots (actually a set of sub_plots), I add a trace of “highlights” drawn with markers, whose y values are all initialized to None. Then, whenever the user clicks a (valid) trace, I replace the associated highlights trace with one that has just one point at (x_click, y_click), the coordinates of the point that was clicked.

That’s pretty simple (though it seems like a lot of code for something so basic), the slightly annoying bit is that I have to keep track of the last click in order to remove it whenever there is a new one.

def delete_clicks(sub_plots, last_highlight, nb_points):
    clicks_trace_index = last_highlight["curve_number"] + 1
    clicks_trace = sub_plots["data"][clicks_trace_index]
    clean_y = nb_points * [None]
    clicks_trace["y"] = tuple(clean_y)

def update_click_highlight(click_data, sub_plots, current_highlights, last_highlight):
    clean_click_data = click_data["points"][0]
    curve_number = clean_click_data["curveNumber"]
    # I don't want the click highlights to be clickable, nor the validated clicks, which also have their own trace
    if curve_number % 3 != 0:
        print("update_click_highlight: This is not a line but a trace of markers (either registered events or clicks)")

    clicks_trace_index = curve_number + 1 # the clicks trace is inserted just after the main trace
    clicks_trace = sub_plots["data"][clicks_trace_index]
    nb_points = len(clicks_trace["x"])
    click_index = clean_click_data["x"]
    click_y_value = clean_click_data["y"]

    if(len(current_highlights) == 0):
        for i in range(nb_points):
            current_highlights.append(None) # We need to append and cannot assign nb_points * [None], or the list won't persist
        current_highlights[last_highlight["x"]] = None # removing previous click
        if curve_number != last_highlight["curve_number"]:
            delete_clicks(sub_plots, last_highlight, nb_points)

    current_highlights[click_index] = click_y_value
    last_highlight["curve_number"] = curve_number
    last_highlight["x"] = click_index
    clicks_trace["y"] = tuple(current_highlights)

def main():
    current_highlights = []
    last_highlight = {"curve_number": -1, "x": -1}
    @pn.depends(plot_pane.param.click_data, watch=True)
    def get_global_plots_click_data(click_data):
        update_click_highlight(click_data, sub_plots, current_highlights, last_highlight)

My real code is actually a bit more complicated because I have to do this for two sets of sub_plots rather than just one here, but this should communicate enough information for someone meaning to replicate this approach.

Still, I’m not convinced I should mark it as a solution, because it feels like there should be a simpler way, and also because it’s not super fast. On a set of sub_plots with just 3 sub_plots, it’s basically instantaneous, but on another one with a few dozen sub_plots, it takes maybe a second or so.