diff --git a/components/dash-core-components/src/fragments/Graph.react.js b/components/dash-core-components/src/fragments/Graph.react.js index c122dde0c9..7f9167ed94 100644 --- a/components/dash-core-components/src/fragments/Graph.react.js +++ b/components/dash-core-components/src/fragments/Graph.react.js @@ -55,6 +55,16 @@ const UNRESPONSIVE_CONFIG = { responsive: false, }; +const toPlainArray = value => { + if (ArrayBuffer.isView(value)) { + return Array.from(value); + } + if (Array.isArray(value)) { + return value.map(toPlainArray); + } + return value; +}; + const filterEventData = (gd, eventData, event) => { let filteredEventData; if (includes(event, ['click', 'hover', 'selected'])) { @@ -89,26 +99,29 @@ const filterEventData = (gd, eventData, event) => { has('curveNumber', fullPoint) && has('customdata', data[pointData.curveNumber]) ) { + const customdata = data[pointData.curveNumber].customdata; + // Plotly.js caches decoded binary arrays on the input object. + const customdataArray = customdata?._inputArray || customdata; if (has('pointNumber', fullPoint)) { if (typeof fullPoint.pointNumber === 'number') { pointData.customdata = - data[pointData.curveNumber].customdata[ - fullPoint.pointNumber - ]; + customdataArray[fullPoint.pointNumber]; } else if ( !fullPoint.pointNumber && fullPoint.data.mode.includes('lines') ) { - pointData.customdata = - data[pointData.curveNumber].customdata; + pointData.customdata = customdataArray; } } else if (has('pointNumbers', fullPoint)) { pointData.customdata = fullPoint.pointNumbers.map(function ( point ) { - return data[pointData.curveNumber].customdata[point]; + return customdataArray[point]; }); } + if (customdata?._inputArray) { + pointData.customdata = toPlainArray(pointData.customdata); + } } // specific to histogram. see https://github.com/plotly/plotly.js/pull/2113/ diff --git a/components/dash-core-components/tests/integration/graph/test_graph_basics.py b/components/dash-core-components/tests/integration/graph/test_graph_basics.py index f268c1edd5..2d0e444f5f 100644 --- a/components/dash-core-components/tests/integration/graph/test_graph_basics.py +++ b/components/dash-core-components/tests/integration/graph/test_graph_basics.py @@ -167,7 +167,6 @@ def update_graph(n_clicks): assert dash_dcc.get_logs() == [] -@pytest.mark.skip(reason="customdata has broken with plotly.py 6") def test_grbs005_graph_customdata(dash_dcc): app = Dash(__name__) @@ -199,12 +198,83 @@ def handleClick(clickData): dash_dcc.find_elements("g .slice")[0].click() - data = dash_dcc.wait_for_element("#text-area").get_attribute("value") - assert data != "", "graph clickData must contain data" - - data = json.loads(data) + wait.until( + lambda: '"points"' + in (dash_dcc.find_element("#text-area").get_attribute("value") or ""), + timeout=3, + ) + data = json.loads(dash_dcc.find_element("#text-area").get_attribute("value")) assert "customdata" in data["points"][0], "graph clickData must contain customdata" - assert data["points"][0]["customdata"][0] == data["points"][0]["pointNumbers"][0] + assert data["points"][0]["customdata"] == data["points"][0]["pointNumbers"] + assert dash_dcc.get_logs() == [] + + +@pytest.mark.parametrize("trace_type", [go.Scatter, go.Scattergl]) +@pytest.mark.parametrize("as_array", [False, True], ids=["list", "numpy"]) +@pytest.mark.parametrize("row_data", [False, True], ids=["scalar", "row"]) +def test_grbs013_graph_numeric_customdata(dash_dcc, trace_type, as_array, row_data): + customdata = [[11, 110], [22, 220], [33, 330]] if row_data else [11, 22, 33] + expected = customdata[1] + if as_array: + customdata = np.array(customdata, dtype=np.int32) + + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.Graph( + id="graph", + figure=go.Figure( + data=[ + go.Scatter(x=[0, 2], y=[2, 0], mode="markers"), + trace_type( + x=[0, 1, 2], + y=[0, 1, 2], + mode="markers", + customdata=customdata, + ), + ], + layout={ + "xaxis": {"range": [-0.5, 2.5]}, + "yaxis": {"range": [-0.5, 2.5]}, + "showlegend": False, + "clickmode": "event+select", + }, + ), + style={"width": 600, "height": 400}, + ), + html.Div(id="click-data"), + html.Div(id="hover-data"), + html.Div(id="selected-data"), + ] + ) + + @app.callback( + Output("click-data", "children"), + Output("hover-data", "children"), + Output("selected-data", "children"), + Input("graph", "clickData"), + Input("graph", "hoverData"), + Input("graph", "selectedData"), + prevent_initial_call=True, + ) + def show_event_data(click_data, hover_data, selected_data): + return ( + json.dumps(click_data), + json.dumps(hover_data), + json.dumps(selected_data), + ) + + dash_dcc.start_server(app) + dash_dcc.wait_for_element("#graph .nsewdrag").click() + + for output in ("click-data", "hover-data", "selected-data"): + dash_dcc.wait_for_contains_text(f"#{output}", '"points"') + point = json.loads(dash_dcc.find_element(f"#{output}").text)["points"][0] + assert point["curveNumber"] == 1 + assert point["pointNumber"] == 1 + assert point.get("customdata") == expected + + assert dash_dcc.get_logs() == [] def test_grbs006_graph_update_frames(dash_dcc):