Repository navigation
Fix parcoords hover event handling in dcc.Graph #4077
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| import plotly.express as px | ||
| import plotly.graph_objects as go | ||
| from dash import Dash, Input, Output, dcc, html | ||
| from selenium.webdriver.common.action_chains import ActionChains | ||
|
|
||
| import dash.testing.wait as wait | ||
|
|
||
|
|
@@ -709,3 +710,133 @@ def rendered_range(): | |
| assert rendered_range() == [3, 5] | ||
|
|
||
| assert dash_dcc.get_logs() == [] | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("clear_on_unhover", [True, False]) | ||
| def test_grbs013_parcoords_hover(dash_dcc, clear_on_unhover): | ||
| app = Dash(__name__) | ||
| app.layout = html.Div( | ||
| [ | ||
| dcc.Graph( | ||
| id="graph", | ||
| clear_on_unhover=clear_on_unhover, | ||
| config={"plotGlPixelRatio": 1}, | ||
| figure={ | ||
| "data": [ | ||
| { | ||
| "type": "parcoords", | ||
| "dimensions": [ | ||
| { | ||
| "label": label, | ||
| "range": [0, 4], | ||
| "values": [0.5, 3.5, 2], | ||
| } | ||
| for label in ["A", "B", "C"] | ||
| ], | ||
| } | ||
| ], | ||
| "layout": { | ||
| "width": 600, | ||
| "height": 400, | ||
| "margin": {"l": 80, "r": 80, "t": 60, "b": 60}, | ||
| }, | ||
| }, | ||
| ), | ||
| html.Pre(id="output"), | ||
| ] | ||
| ) | ||
|
|
||
| @app.callback(Output("output", "children"), Input("graph", "hoverData")) | ||
| def show_hover(data): | ||
| return json.dumps(data) | ||
|
|
||
| dash_dcc.driver.set_window_size(1000, 800) | ||
| dash_dcc.start_server(app) | ||
| dash_dcc.wait_for_element("#graph .js-plotly-plot:not(.dash-graph--pending)") | ||
| dash_dcc.wait_for_text_to_equal("#output", "null") | ||
| canvas = dash_dcc.find_element("#graph .gl-canvas-pick") | ||
|
|
||
| # Line 2 exceeds the trace count and must not be used as a trace index. | ||
| ActionChains(dash_dcc.driver).move_to_element_with_offset(canvas, -110, 0).perform() | ||
| dash_dcc.wait_for_contains_text("#output", '"points"') | ||
| hover_data = json.loads(dash_dcc.find_element("#output").text) | ||
| assert hover_data == { | ||
| "points": [ | ||
| { | ||
| "x": 190, | ||
| "y": 200, | ||
| "clientX": canvas.rect["x"] + 190, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Asserting exact |
||
| "clientY": canvas.rect["y"] + 200, | ||
| "dataIndex": 0, | ||
| "curveNumber": 2, | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| ActionChains(dash_dcc.driver).move_to_element_with_offset( | ||
| canvas, -110, -40 | ||
| ).perform() | ||
| if clear_on_unhover: | ||
| dash_dcc.wait_for_text_to_equal("#output", "null") | ||
| else: | ||
| assert json.loads(dash_dcc.find_element("#output").text) == hover_data | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the |
||
| assert dash_dcc.get_logs() == [] | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("trace_type", ["scatter", "sankey"]) | ||
| def test_grbs014_graph_hover_points(dash_dcc, trace_type): | ||
| app = Dash(__name__) | ||
| if trace_type == "scatter": | ||
| trace = { | ||
| "type": "scatter", | ||
| "x": [1, 2, 3], | ||
| "y": [1, 2, 3], | ||
| "mode": "markers", | ||
| "customdata": ["a", "b", "c"], | ||
| } | ||
| selector = ".scatterlayer .point" | ||
| expected = { | ||
| "curveNumber": 0, | ||
| "pointNumber": 0, | ||
| "x": 1, | ||
| "y": 1, | ||
| "customdata": "a", | ||
| } | ||
| else: | ||
| trace = { | ||
| "type": "sankey", | ||
| "node": {"label": ["A", "B"]}, | ||
| "link": {"source": [0], "target": [1], "value": [2]}, | ||
| } | ||
| selector = ".sankey-node" | ||
| expected = {"curveNumber": 0, "pointNumber": 0, "label": "A", "value": 2} | ||
|
|
||
| app.layout = html.Div( | ||
| [ | ||
| dcc.Graph( | ||
| id="graph", | ||
| figure={"data": [trace]}, | ||
| style={"width": 600, "height": 400}, | ||
| clear_on_unhover=True, | ||
| ), | ||
| html.Pre(id="output"), | ||
| ] | ||
| ) | ||
|
|
||
| @app.callback(Output("output", "children"), Input("graph", "hoverData")) | ||
| def show_hover(data): | ||
| return json.dumps(data) | ||
|
|
||
| dash_dcc.driver.set_window_size(1000, 800) | ||
| dash_dcc.start_server(app) | ||
| dash_dcc.wait_for_element("#graph .js-plotly-plot:not(.dash-graph--pending)") | ||
| ActionChains(dash_dcc.driver).move_to_element( | ||
| dash_dcc.find_element(f"#graph {selector}") | ||
| ).perform() | ||
| dash_dcc.wait_for_contains_text("#output", '"points"') | ||
| hover_data = json.loads(dash_dcc.find_element("#output").text) | ||
| assert len(hover_data["points"]) == 1 | ||
| point = hover_data["points"][0] | ||
| assert {key: point[key] for key in expected} == expected | ||
| assert not {"data", "fullData", "xaxis", "yaxis"}.intersection(point) | ||
| assert dash_dcc.get_logs() == [] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this change,
hoverData.points[0].curveNumberis the parcoords line index. Everywhere else in Dash it's the trace index. A callback doingfig["data"][pt["curveNumber"]]breaks as soon as you hover line 2 of a one-trace figure (that's the exact case your test covers). Can you normalize it here instead:curveNumber: dataIndex,pointNumber: <line index>? Then it matches the other traces, and the customdata lookup below works without thehasPointsArrayguard.