dash icon indicating copy to clipboard operation
dash copied to clipboard

customdata is missing in clickData for Scattergl objects

Open rra88 opened this issue 10 months ago • 5 comments

Describe your context I've made a dash application that presents some data from an SQLite database both in a graph and an AG Grid table. I'm using scattergl to create a figure with scattergl traces. Clicking a point on the scattergl traces fires a dash callback that should highlight the relevant data in the table. The mapping of the data between the figure and the table relies on the customdata variable of the scattergl class, and that the clickData passed by the click event carries with it the customdata data. This has been working until I recently upgraded my environment.

  • replace the result of pip list | grep dash below
  - Installing dash-core-components (2.0.0)
  - Installing dash-html-components (2.0.0)
  - Installing dash-table (5.0.0)
  - Installing dash (2.18.2)
  - Installing dash-bootstrap-components (1.7.1)
  - Installing dash-ag-grid (31.3.0)
  - Installing dash-bootstrap-templates (2.1.0)
  • if frontend related, tell us your Browser, Version and OS

    • Windows 10
    • Edge
    • 133.0.3065.69

Describe the bug

The bug is the same as that described in https://github.com/plotly/dash/issues/2493, except that I'm experiencing this with Scattergl objects (added to a plotly.graph_objs._figure object with add_traces). Note that this worked just fine before I recreated my environment (and updated many of my packages). Unfortunately I don't have the package list of the old environment.

Expected behavior

I expected the data in the customdata variable of the Scattergl trace to be present in the clickData object I receive in my figure callback, just like before I upgraded my environment.

Screenshots of actual behaviour Firstly showing created Scattergl objects containing data in its customdata variable. Image Secondly showing the click_data dict received in my dash callback after clicking a point on the scatter graph, which does not contain 'customdata'. Image

rra88 avatar Feb 28 '25 09:02 rra88

Thanks for labelling this issue, @gvwilson. What does the P2 ("considered for next cycle") label entail? Is it possible to give a guesstimate of when a fix could be implemented (I depend on this feature in a project I'm working on). Maybe I should look into using Scatter instead of Scattergl, but I'll wait to see your response first. Cheers!

rra88 avatar Apr 03 '25 09:04 rra88

hi @rra88 - realistically, it will be several months before anyone here can look at this. I'd be happy to prioritize review of a community PR if you or someone else can put one together. Thanks - @gvwilson

gvwilson avatar Apr 03 '25 13:04 gvwilson

Alright, thanks for the info!

rra88 avatar Apr 03 '25 13:04 rra88

Hi @rra88, There is a workaround for this specific issue. If you add a State to your callback that contains the figure props of your dcc.Graph, you can still use the curveNumber property of your clickData to retrieve the customdata you're expecting:

from typing import Any

@callback(Input("<YOUR-GRAPH-ID>", "clickData"), State("<YOUR-GRAPH-ID>", "figure"))
def your_callback_function(click_data: dict[str, Any], figure: dict[str, Any]) -> ...:
    # Case 1: your clickData contains a "customdata".
    if "customdata" in click_data:
        do_something(click_data)

    # Case 2: your clickData should have contained a "customdata".
    trace: dict[str, Any] = figure["data"][click_data["curveNumber"]]
    if "customdata" in trace:
        do_something(trace["customdata"])

    # Case 3: your clickData definitely contains no "customdata"
    do_something_else(...)

With this solution and design, your code will work perfectly, and once this issue is fixed, you'll just need to delete the whole # Case 2 block since it will be absorbed in the # Case 1.

quentinemusee avatar Apr 09 '25 06:04 quentinemusee

Hello @quentinemusee!

Inspired by your comment my callback now works again and looks like this:

@callback(
    Output('custom-pagination', 'active_page', allow_duplicate=True),
    Output('ag-grid-id', 'getRowStyle'),
    Input('graph-id', 'clickData'),
    State('graph-id', 'figure'),  prevent_initial_call=True)
def update_aggrid_on_click(click_data, fig):
    point = click_data['points'][0]
    if 'customdata' in point:
        msg_id = int(point['customdata'])  # gets the id of the dataframe row
    else:
        point_nr = point["pointNumber"]
        trace = fig["data"][point["curveNumber"]]
        if "customdata" in trace:
            msg_id = trace["customdata"]["_inputArray"][str(point_nr)]  # gets the id of the dataframe row
    page = math.floor(msg_id / DATATABLE_PAGE_SIZE) + 1  #  plus 1 as active_page dash.Pagination counts from 1
    return page, highlight_row(msg_id)

Note that I previously (before running into the issue described in this thread) looked up my customdata data in click_data["points"][0]["customdata"]. Adding the else block as per your workaround, I find the same data in fig["data"][point["curveNumber"]]["customdata"]["_inputArray"][str(point_nr)], where curveNumber and pointNumber are gotten from the click_data object.

Thanks heaps for the workaround!

rra88 avatar Apr 09 '25 08:04 rra88