Skip to content

Commit 6b75843

Browse files
committed
Merge branch 'pyright-typing-fixes' of github.com:plotly/dash into pyright-typing-fixes
2 parents c763880 + 68a8c4f commit 6b75843

File tree

11 files changed

+346
-18
lines changed

11 files changed

+346
-18
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## [unreleased]
6+
7+
## Fixed
8+
- [#3264](https://github.com/plotly/dash/pull/3264) Fixed an issue where moving components inside of children would not update the `setProps` path, leading to hashes being incorrect
9+
- [#3265](https://github.com/plotly/dash/pull/3265) Fixed issue where the resize of graphs was cancelling others
10+
11+
## Added
12+
- [#3268](https://github.com/plotly/dash/pull/3268) Added the ability for component devs to subscribe to descendent updates by setting `dashChildrenUpdate = true` on the component, eg: `Tabs.dashChildrenUpdate = true`
13+
514
## [3.0.2] - 2025-04-01
615

716
## Changed

components/dash-core-components/src/components/Tabs.react.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,6 @@ const EnhancedTab = ({
121121
);
122122
};
123123

124-
EnhancedTab.defaultProps = {
125-
loading_state: {
126-
is_loading: false,
127-
component_name: '',
128-
prop_name: '',
129-
},
130-
};
131-
132124
/**
133125
* A Dash component that lets you render pages with tabs - the Tabs component's children
134126
* can be dcc.Tab components, which can hold a label that will be displayed as a tab, and can in turn hold
@@ -439,3 +431,5 @@ Tabs.propTypes = {
439431
*/
440432
persistence_type: PropTypes.oneOf(['local', 'session', 'memory']),
441433
};
434+
435+
Tabs.dashChildrenUpdate = true;

components/dash-core-components/src/utils/ResizeDetector.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ import PropTypes from 'prop-types';
44
// Debounce 50 ms
55
const DELAY = 50;
66

7-
let resizeTimeout;
8-
97
const ResizeDetector = props => {
108
const {onResize, children, targets} = props;
119
const ref = createRef();
10+
let resizeTimeout;
1211

1312
const debouncedResizeHandler = useCallback(() => {
1413
if (resizeTimeout) {

components/dash-core-components/tests/integration/graph/test_graph_responsive.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

33
from dash import Dash, Input, Output, State, dcc, html
4+
import plotly.graph_objects as go
45

56
from dash.exceptions import PreventUpdate
67
from dash.testing import wait
@@ -134,3 +135,77 @@ def resize(n_clicks, style):
134135
)
135136

136137
assert dash_dcc.get_logs() == []
138+
139+
140+
def test_grrs002_graph(dash_dcc):
141+
app = Dash(__name__)
142+
143+
app.layout = html.Div(
144+
[
145+
html.Button("Generate Figures", id="generate-btn", n_clicks=0),
146+
html.Button("Get Bounding Box", id="bounding-btn"),
147+
html.Div(
148+
id="graph-container",
149+
children=[
150+
html.Div(id="bounding-output"),
151+
dcc.Graph(
152+
id="prec-climate-daily",
153+
style={"height": "45vh"},
154+
config={"responsive": True},
155+
),
156+
dcc.Graph(
157+
id="temp-climate-daily",
158+
style={"height": "45vh"},
159+
config={"responsive": True},
160+
),
161+
],
162+
style={"display": "none"},
163+
),
164+
]
165+
)
166+
167+
app.clientside_callback(
168+
"""() => {
169+
pcd_container = document.querySelector("#prec-climate-daily")
170+
pcd_container_bbox = pcd_container.getBoundingClientRect()
171+
pcd_graph = pcd_container.querySelector('.main-svg')
172+
pcd_graph_bbox = pcd_graph.getBoundingClientRect()
173+
tcd_container = document.querySelector("#temp-climate-daily")
174+
tcd_container_bbox = tcd_container.getBoundingClientRect()
175+
tcd_graph = tcd_container.querySelector('.main-svg')
176+
tcd_graph_bbox = tcd_graph.getBoundingClientRect()
177+
return JSON.stringify(
178+
pcd_container_bbox.height == pcd_graph_bbox.height &&
179+
pcd_container_bbox.width == pcd_graph_bbox.width &&
180+
tcd_container_bbox.height == tcd_graph_bbox.height &&
181+
tcd_container_bbox.width == tcd_graph_bbox.width
182+
)
183+
}""",
184+
Output("bounding-output", "children"),
185+
Input("bounding-btn", "n_clicks"),
186+
prevent_initial_call=True,
187+
)
188+
189+
@app.callback(
190+
[
191+
Output("prec-climate-daily", "figure"),
192+
Output("temp-climate-daily", "figure"),
193+
Output("graph-container", "style"),
194+
Output("bounding-output", "children", allow_duplicate=True),
195+
],
196+
[Input("generate-btn", "n_clicks")],
197+
prevent_initial_call=True,
198+
)
199+
def update_figures(n_clicks):
200+
fig_acc = go.Figure(data=[go.Scatter(x=[0, 1, 2], y=[0, 1, 0], mode="lines")])
201+
fig_daily = go.Figure(data=[go.Scatter(x=[0, 1, 2], y=[1, 0, 1], mode="lines")])
202+
return fig_acc, fig_daily, {"display": "block"}, "loaded"
203+
204+
dash_dcc.start_server(app)
205+
dash_dcc.wait_for_text_to_equal("#generate-btn", "Generate Figures")
206+
dash_dcc.find_element("#generate-btn").click()
207+
dash_dcc.wait_for_text_to_equal("#bounding-output", "loaded")
208+
dash_dcc.find_element(".dash-graph .js-plotly-plot.dash-graph--pending")
209+
dash_dcc.find_element(".dash-graph .js-plotly-plot:not(.dash-graph--pending)")
210+
dash_dcc.find_element("#bounding-btn").click()
211+
dash_dcc.wait_for_text_to_equal("#bounding-output", "true")

dash/_callback.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def callback(
124124
while a callback is running, the callback is canceled.
125125
Note that the value of the property is not significant, any change in
126126
value will result in the cancellation of the running job (if any).
127+
This parameter only applies to background callbacks (`background=True`).
127128
:param progress:
128129
An `Output` dependency grouping that references properties of
129130
components in the app's layout. When provided, the decorated function
@@ -132,21 +133,25 @@ def callback(
132133
function should call in order to provide updates to the app on its
133134
current progress. This function accepts a single argument, which
134135
correspond to the grouping of properties specified in the provided
135-
`Output` dependency grouping
136+
`Output` dependency grouping. This parameter only applies to background
137+
callbacks (`background=True`).
136138
:param progress_default:
137139
A grouping of values that should be assigned to the components
138140
specified by the `progress` argument when the callback is not in
139141
progress. If `progress_default` is not provided, all the dependency
140142
properties specified in `progress` will be set to `None` when the
141-
callback is not running.
143+
callback is not running. This parameter only applies to background
144+
callbacks (`background=True`).
142145
:param cache_args_to_ignore:
143146
Arguments to ignore when caching is enabled. If callback is configured
144147
with keyword arguments (Input/State provided in a dict),
145148
this should be a list of argument names as strings. Otherwise,
146149
this should be a list of argument indices as integers.
150+
This parameter only applies to background callbacks (`background=True`).
147151
:param cache_ignore_triggered:
148152
Whether to ignore which inputs triggered the callback when creating
149-
the cache.
153+
the cache. This parameter only applies to background callbacks
154+
(`background=True`).
150155
:param interval:
151156
Time to wait between the background callback update requests.
152157
:param on_error:

dash/dash-renderer/src/wrapper/DashWrapper.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ function DashWrapper({
6565
const dispatch = useDispatch();
6666
const memoizedKeys: MutableRefObject<MemoizedKeysType> = useRef({});
6767
const newRender = useRef(false);
68+
const renderedPath = useRef<DashLayoutPath>(componentPath);
6869
let renderComponent: any = null;
6970
let renderComponentProps: any = null;
7071
let renderH: any = null;
@@ -90,6 +91,7 @@ function DashWrapper({
9091
} else {
9192
newRender.current = false;
9293
}
94+
renderedPath.current = componentPath;
9395
}, [_newRender]);
9496

9597
const setProps = (newProps: UpdatePropsPayload) => {
@@ -101,7 +103,10 @@ function DashWrapper({
101103
dispatch((dispatch, getState) => {
102104
const currentState = getState();
103105
const {graphs} = currentState;
104-
const oldLayout = getComponentLayout(componentPath, currentState);
106+
const oldLayout = getComponentLayout(
107+
renderedPath.current,
108+
currentState
109+
);
105110
if (!oldLayout) return;
106111
const {props: oldProps} = oldLayout;
107112
if (!oldProps) return;
@@ -144,7 +149,7 @@ function DashWrapper({
144149
dispatch(
145150
updateProps({
146151
props: changedProps,
147-
itempath: componentPath,
152+
itempath: renderedPath.current,
148153
renderType: 'internal'
149154
})
150155
);

dash/dash-renderer/src/wrapper/selectors.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,85 @@
11
import {DashLayoutPath, DashComponent, BaseDashProps} from '../types/component';
2-
import {getComponentLayout, stringifyPath} from './wrapping';
2+
import {
3+
getComponentLayout,
4+
stringifyPath,
5+
checkDashChildrenUpdate
6+
} from './wrapping';
7+
import {pathOr} from 'ramda';
38

49
type SelectDashProps = [DashComponent, BaseDashProps, number, object, string];
510

11+
interface ChangedPropsRecord {
12+
hash: number;
13+
changedProps: Record<string, any>;
14+
renderType: string;
15+
}
16+
17+
interface Hashes {
18+
[key: string]: any; // Index signature for string keys with number values
19+
}
20+
21+
const previousHashes: Hashes = {};
22+
23+
const isFirstLevelPropsChild = (
24+
updatedPath: string,
25+
strPath: string
26+
): [boolean, string[]] => {
27+
const updatedSegments = updatedPath.split(',');
28+
const fullSegments = strPath.split(',');
29+
30+
// Check that strPath actually starts with updatedPath
31+
const startsWithPath = fullSegments.every(
32+
(seg, i) => updatedSegments[i] === seg
33+
);
34+
35+
if (!startsWithPath) return [false, []];
36+
37+
// Get the remaining path after the prefix
38+
const remainingSegments = updatedSegments.slice(fullSegments.length);
39+
40+
const propsCount = remainingSegments.filter(s => s === 'props').length;
41+
42+
return [propsCount < 2, remainingSegments];
43+
};
44+
45+
function determineChangedProps(
46+
state: any,
47+
strPath: string
48+
): ChangedPropsRecord {
49+
let combinedHash = 0;
50+
let renderType: any; // Default render type, adjust as needed
51+
const changedProps: Record<string, any> = {};
52+
Object.entries(state.layoutHashes).forEach(([updatedPath, pathHash]) => {
53+
const [descendant, remainingSegments] = isFirstLevelPropsChild(
54+
updatedPath,
55+
strPath
56+
);
57+
if (descendant) {
58+
const previousHash: any = pathOr({}, [updatedPath], previousHashes);
59+
combinedHash += pathOr(0, ['hash'], pathHash);
60+
if (previousHash !== pathHash) {
61+
if (updatedPath !== strPath) {
62+
Object.assign(changedProps, {[remainingSegments[1]]: true});
63+
renderType = 'components';
64+
} else {
65+
Object.assign(
66+
changedProps,
67+
pathOr({}, ['changedProps'], pathHash)
68+
);
69+
renderType = pathOr({}, ['renderType'], pathHash);
70+
}
71+
previousHashes[updatedPath] = pathHash;
72+
}
73+
}
74+
});
75+
76+
return {
77+
hash: combinedHash,
78+
changedProps,
79+
renderType
80+
};
81+
}
82+
683
export const selectDashProps =
784
(componentPath: DashLayoutPath) =>
885
(state: any): SelectDashProps => {
@@ -12,7 +89,12 @@ export const selectDashProps =
1289
// Then it can be easily compared without having to compare the props.
1390
const strPath = stringifyPath(componentPath);
1491

15-
const hash = state.layoutHashes[strPath];
92+
let hash;
93+
if (checkDashChildrenUpdate(c)) {
94+
hash = determineChangedProps(state, strPath);
95+
} else {
96+
hash = state.layoutHashes[strPath];
97+
}
1698
let h = 0;
1799
let changedProps: object = {};
18100
let renderType = '';

dash/dash-renderer/src/wrapper/wrapping.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,14 @@ export function checkRenderTypeProp(componentDefinition: any) {
7272
)
7373
);
7474
}
75+
76+
export function checkDashChildrenUpdate(componentDefinition: any) {
77+
return (
78+
'dashChildrenUpdate' in
79+
pathOr(
80+
{},
81+
[componentDefinition?.namespace, componentDefinition?.type],
82+
window as any
83+
)
84+
);
85+
}

tests/integration/callbacks/test_prevent_update.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ def callback1(value):
3636
raise PreventUpdate("testing callback does not update")
3737
return value
3838

39-
@app.callback(Output("output2", "children"), [Input("output1", "children")])
39+
@app.callback(
40+
Output("output2", "children"),
41+
[Input("output1", "children")],
42+
prevent_initial_call=True,
43+
)
4044
def callback2(value):
4145
callback2_count.value += 1
4246
return value

0 commit comments

Comments
 (0)