Skip to content

Commit d33db91

Browse files
authored
Merge pull request #1416 from plotly/callback-graph-robustness
Callback graph robustness
2 parents cea5aba + 53916e4 commit d33db91

File tree

8 files changed

+139
-43
lines changed

8 files changed

+139
-43
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import PropTypes from 'prop-types';
2+
import React, { Fragment } from 'react';
3+
4+
const WidthComponent = props => (<Fragment>
5+
{props.width}
6+
</Fragment>);
7+
8+
WidthComponent.propTypes = {
9+
id: PropTypes.string,
10+
width: PropTypes.number
11+
};
12+
13+
WidthComponent.defaultProps = {
14+
width: 0
15+
};
16+
17+
export default WidthComponent;

@plotly/dash-test-components/src/index.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import AsyncComponent from './components/AsyncComponent';
22
import CollapseComponent from './components/CollapseComponent';
33
import DelayedEventComponent from './components/DelayedEventComponent';
44
import FragmentComponent from './components/FragmentComponent';
5-
import StyledComponent from './components/StyledComponent';
65
import MyPersistedComponent from './components/MyPersistedComponent';
76
import MyPersistedComponentNested from './components/MyPersistedComponentNested';
8-
7+
import StyledComponent from './components/StyledComponent';
8+
import WidthComponent from './components/WidthComponent';
99

1010
export {
1111
AsyncComponent,
@@ -14,5 +14,6 @@ export {
1414
FragmentComponent,
1515
MyPersistedComponent,
1616
MyPersistedComponentNested,
17-
StyledComponent
17+
StyledComponent,
18+
WidthComponent
1819
};

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
55
## Unreleased
66
### Fixed
77
- [#1415](https://github.com/plotly/dash/pull/1415) Fix a regression with some layouts callbacks involving dcc.Tabs, not yet loaded dash_table.DataTable and dcc.Graph to not be called
8+
- [#1416](https://github.com/plotly/dash/pull/1416) Make callback graph more robust for complex apps and some specific props (`width` in particular) that previously caused errors.
89

910
## [1.16.1] - 2020-09-16
1011
### Changed

dash-renderer/package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dash-renderer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"cookie": "^0.4.0",
3030
"cytoscape": "^3.14.1",
3131
"cytoscape-dagre": "^2.2.2",
32+
"cytoscape-fcose": "^1.2.3",
3233
"dependency-graph": "^0.9.0",
3334
"fast-isnumeric": "^1.1.3",
3435
"prop-types": "15.7.2",

dash-renderer/src/components/error/CallbackGraph/CallbackGraphContainer.react.js

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import PropTypes from 'prop-types';
33
import {connect, useSelector} from 'react-redux';
44
import Cytoscape from 'cytoscape';
55
import CytoscapeComponent from 'react-cytoscapejs';
6-
import Dagre from 'cytoscape-dagre';
6+
import dagre from 'cytoscape-dagre';
7+
import fcose from 'cytoscape-fcose';
78
import JSONTree from 'react-json-tree';
8-
import {keys, mergeRight, omit, path} from 'ramda';
9+
import {keys, mergeRight, omit, path, values} from 'ramda';
910

1011
import {getPath} from '../../../actions/paths';
1112
import {stringifyId} from '../../../actions/dependencies';
@@ -19,46 +20,51 @@ import {
1920
updateCallback
2021
} from './CallbackGraphEffects';
2122

22-
Cytoscape.use(Dagre);
23+
Cytoscape.use(dagre);
24+
Cytoscape.use(fcose);
2325

2426
/*
2527
* Generates all the elements (nodes, edeges) for the dependency graph.
2628
*/
27-
function generateElements(graphs, profile) {
29+
function generateElements(graphs, profile, extraLinks) {
2830
const consumed = [];
2931
const elements = [];
32+
const structure = {};
3033

3134
function recordNode(id, property) {
3235
const idStr = stringifyId(id);
3336
const idType = typeof id === 'object' ? 'wildcard' : 'component';
3437

35-
const parent = idStr;
36-
const child = `${idStr}.${property}`;
38+
// dagre layout has problems with eg `width` property - so prepend an X
39+
const parentId = idStr;
40+
const childId = `${parentId}.X${property}`;
3741

38-
if (!consumed.includes(parent)) {
39-
consumed.push(parent);
42+
if (!consumed.includes(parentId)) {
43+
consumed.push(parentId);
4044
elements.push({
4145
data: {
42-
id: idStr,
46+
id: parentId,
4347
label: idStr,
4448
type: idType
4549
}
4650
});
51+
structure[parentId] = [];
4752
}
4853

49-
if (!consumed.includes(child)) {
50-
consumed.push(child);
54+
if (!consumed.includes(childId)) {
55+
consumed.push(childId);
5156
elements.push({
5257
data: {
53-
id: child,
58+
id: childId,
5459
label: property,
55-
parent: parent,
60+
parent: parentId,
5661
type: 'property'
5762
}
5863
});
64+
structure[parentId].push(childId);
5965
}
6066

61-
return child;
67+
return childId;
6268
}
6369

6470
function recordEdge(source, target, type) {
@@ -91,21 +97,34 @@ function generateElements(graphs, profile) {
9197
});
9298

9399
callback.outputs.map(({id, property}) => {
94-
const node = recordNode(id, property);
95-
recordEdge(cb, node, 'output');
100+
const nodeId = recordNode(id, property);
101+
recordEdge(cb, nodeId, 'output');
96102
});
97103

98104
callback.inputs.map(({id, property}) => {
99-
const node = recordNode(id, property);
100-
recordEdge(node, cb, 'input');
105+
const nodeId = recordNode(id, property);
106+
recordEdge(nodeId, cb, 'input');
101107
});
102108

103109
callback.state.map(({id, property}) => {
104-
const node = recordNode(id, property);
105-
recordEdge(node, cb, 'state');
110+
const nodeId = recordNode(id, property);
111+
recordEdge(nodeId, cb, 'state');
106112
});
107113
});
108114

115+
// pull together props in the same component
116+
if (extraLinks) {
117+
values(structure).forEach(childIds => {
118+
childIds.forEach(childFrom => {
119+
childIds.forEach(childTo => {
120+
if (childFrom !== childTo) {
121+
recordEdge(childFrom, childTo, 'hidden');
122+
}
123+
});
124+
});
125+
});
126+
}
127+
109128
return elements;
110129
}
111130

@@ -141,24 +160,19 @@ function flattenInputs(inArray, final) {
141160
// len('__dash_callback__.')
142161
const cbPrefixLen = 18;
143162

163+
const dagreLayout = {
164+
name: 'dagre',
165+
padding: 10,
166+
ranker: 'tight-tree'
167+
};
168+
169+
const forceLayout = {name: 'fcose', padding: 10, animate: false};
170+
144171
const layouts = {
145-
'top-down': {
146-
name: 'dagre',
147-
padding: 10,
148-
spacingFactor: 0.8
149-
},
150-
'left-right': {
151-
name: 'dagre',
152-
padding: 10,
153-
nodeSep: 0,
154-
rankSep: 80,
155-
rankDir: 'LR'
156-
},
157-
force: {
158-
name: 'cose',
159-
padding: 10,
160-
animate: false
161-
}
172+
'top-down': {...dagreLayout, spacingFactor: 0.8},
173+
'left-right': {...dagreLayout, nodeSep: 0, rankSep: 80, rankDir: 'LR'},
174+
force: forceLayout,
175+
'force-loose': forceLayout
162176
};
163177

164178
function CallbackGraph() {
@@ -180,7 +194,10 @@ function CallbackGraph() {
180194
const [layoutType, setLayoutType] = useState(chosenType || 'top-down');
181195

182196
// Generate and memoize the elements.
183-
const elements = useMemo(() => generateElements(graphs, profile), [graphs]);
197+
const elements = useMemo(
198+
() => generateElements(graphs, profile, layoutType === 'force'),
199+
[graphs, layoutType]
200+
);
184201

185202
// Custom hook to make sure cytoscape is loaded.
186203
const useCytoscapeEffect = (effect, condition) => {

dash-renderer/src/components/error/CallbackGraph/CallbackGraphContainerStylesheet.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ const stylesheet = [
2424
}
2525
},
2626

27+
{
28+
selector: 'edge[type="hidden"]',
29+
style: {
30+
display: 'none'
31+
}
32+
},
33+
2734
{
2835
selector: 'edge[type="output"]',
2936
style: {

tests/integration/devtools/test_devtools_ui.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import dash_core_components as dcc
44
import dash_html_components as html
55
import dash
6+
from dash.dependencies import Input, Output
67
import dash.testing.wait as wait
78

9+
from dash_test_components import WidthComponent
810
from ...assets.todo_app import todo_app
911

1012

@@ -102,7 +104,7 @@ def test_dvui003_callback_graph(dash_duo):
102104

103105
pos = dash_duo.driver.execute_script(
104106
"""
105-
const pos = store.getState().profile.graphLayout.positions['new-item.value'];
107+
const pos = store.getState().profile.graphLayout.positions['new-item.Xvalue'];
106108
pos.y -= 100;
107109
return pos.y;
108110
"""
@@ -119,7 +121,36 @@ def test_dvui003_callback_graph(dash_duo):
119121
# the manually moved node is still in its new position
120122
assert pos == dash_duo.driver.execute_script(
121123
"""
122-
const pos = store.getState().profile.graphLayout.positions['new-item.value'];
124+
const pos = store.getState().profile.graphLayout.positions['new-item.Xvalue'];
123125
return pos.y;
124126
"""
125127
)
128+
129+
130+
def test_dvui004_width_props(dash_duo):
131+
app = dash.Dash(__name__)
132+
133+
app.layout = html.Div(
134+
[html.Button(["Click me!"], id="btn"), WidthComponent(id="width")]
135+
)
136+
137+
@app.callback(Output("width", "width"), Input("btn", "n_clicks"))
138+
def get_width(n_clicks):
139+
n_clicks = n_clicks if n_clicks is not None else 0
140+
141+
return (n_clicks + 1) * 10
142+
143+
dash_duo.start_server(
144+
app,
145+
debug=True,
146+
use_reloader=False,
147+
use_debugger=True,
148+
dev_tools_hot_reload=False,
149+
)
150+
151+
dash_duo.find_element(".dash-debug-menu").click()
152+
sleep(1) # wait for debug menu opening animation
153+
dash_duo.find_element(".dash-debug-menu__button--callbacks").click()
154+
sleep(3) # wait for callback graph to draw
155+
156+
assert dash_duo.get_logs() == []

0 commit comments

Comments
 (0)