Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
- [#3986](https://github.com/plotly/dash/pull/3986) Adjust `_run_before_hooks` in the `fastapi` backend to honor a response returned by a `before_request` function, matching the `flask` backend's behavior.

### Fixed
- [#4077](https://github.com/plotly/dash/pull/4077) Hovering a `parcoords` trace no longer throws in `dcc.Graph`, and `hoverData` now has the same shape as other traces: a `points` list where `curveNumber` is the trace index and `pointNumber` is the hovered line. Fixes [#3128](https://github.com/plotly/dash/issues/3128).
- [#4075](https://github.com/plotly/dash/pull/4075) `fastapi` backend: run `before_request` hooks on every route of the Dash app (assets, hook routes, `api_endpoint`, pages, when mounted) and send a non-None hook result as the response, as on Flask. `fastapi` and `quart` also run them on the WebSocket handshake.
- [#4075](https://github.com/plotly/dash/pull/4075) Background callbacks without `cache_by` no longer share a result between requests with equal inputs (unread Diskcache results now expire after a day, or after `expire` when set, even with `cache_by`), and callbacks with the same source no longer share a cache key. `build_cache_key` takes a new `callback_key` argument.
- [#4075](https://github.com/plotly/dash/pull/4075) `dcc.Location(refresh=True).href`, `html.Area.href` and `html.Base.href` now block `javascript:` URLs like `html.A`.
Expand Down
19 changes: 16 additions & 3 deletions components/dash-core-components/src/fragments/Graph.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,22 @@ const filterEventData = (gd, eventData, event) => {
* into the event object
*/
const data = gd.data;

for (let i = 0; i < eventData.points.length; i++) {
const fullPoint = eventData.points[i];
// Parcoords sends one flat point instead of a points array. Its
// curveNumber is the line index and dataIndex is the trace index, so
// reshape it to match other traces: curveNumber is the trace and
// pointNumber is the line.
const eventPoints = Array.isArray(eventData.points)
? eventData.points
: [
{
...eventData,
curveNumber: eventData.dataIndex,
pointNumber: eventData.curveNumber,
},
];

for (let i = 0; i < eventPoints.length; i++) {
const fullPoint = eventPoints[i];
const pointData = filter(function (o) {
return !includes(type(o), ['Object', 'Array']);
}, fullPoint);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -709,3 +710,124 @@ def rendered_range():
assert rendered_range() == [3, 5]

assert dash_dcc.get_logs() == []


def test_grbs013_parcoords_hover(dash_dcc):
app = Dash(__name__)
app.layout = html.Div(
[
dcc.Graph(
id="graph",
clear_on_unhover=True,
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")

# Hover line 2 of the only trace: curveNumber is the trace index and
# pointNumber the line index, as for other trace types.
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 len(hover_data["points"]) == 1
point = hover_data["points"][0]
assert point["curveNumber"] == 0
assert point["pointNumber"] == 2
# Coordinates depend on layout and pixel ratio, so only check they are sent.
assert {"x", "y", "clientX", "clientY"} <= set(point)

ActionChains(dash_dcc.driver).move_to_element_with_offset(
canvas, -110, -40
).perform()
dash_dcc.wait_for_text_to_equal("#output", "null")
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() == []