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
- [#4076](https://github.com/plotly/dash/pull/4076) `dcc.Graph` `clickData`, `hoverData` and `selectedData` now include `customdata` when it was sent as a numpy array (binary-encoded by plotly.py 6), as plain numbers and lists. Fixes [#3196](https://github.com/plotly/dash/issues/3196).
- [#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
26 changes: 20 additions & 6 deletions components/dash-core-components/src/fragments/Graph.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'])) {
Expand Down Expand Up @@ -89,26 +99,30 @@ const filterEventData = (gd, eventData, event) => {
has('curveNumber', fullPoint) &&
has('customdata', data[pointData.curveNumber])
) {
const customdata = data[pointData.curveNumber].customdata;
// Binary (typed array) customdata is only decoded in the full
// trace, so read it from there and fall back to the input.
const customdataArray =
fullPoint.fullData?.customdata ?? 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];
});
}
// Typed arrays would serialize as objects, so send plain arrays.
pointData.customdata = toPlainArray(pointData.customdata);
}

// specific to histogram. see https://github.com/plotly/plotly.js/pull/2113/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -199,12 +198,79 @@ 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)
dash_dcc.wait_for_contains_text("#text-area", '"points"', 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):
Expand Down