Skip to content

Commit e8f677f

Browse files
authored
Merge pull request #3384 from plotly/master-3.2.0
Master 3.2.0
2 parents b465f06 + 6cfac83 commit e8f677f

File tree

72 files changed

+15189
-56094
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+15189
-56094
lines changed

.circleci/config.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,17 @@ jobs:
163163
- run:
164164
name: npm prereqs
165165
command: |
166+
nvm use 18
166167
npm ci
167168
cd dash/dash-renderer && npm i && cd ../../
168169
cd components/dash-html-components && npm i && npm run extract && cd ../../
169170
- run:
170171
name: ️️🏗️ build dash
171172
command: |
173+
nvm use 18
172174
. venv/Scripts/activate
173-
npm run private::build.jupyterlab && npm run private::build.renderer && python dash/development/update_components.py 'dash-html-components'
175+
npm run private::build.jupyterlab && npm run private::build.renderer
176+
cd components/dash-html-components && npm run build
174177
no_output_timeout: 30m
175178

176179
test-312: &test

CHANGELOG.md

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

5+
## [3.2.0] - 2025-07-31
6+
7+
## Added
8+
- [#3369](https://github.com/plotly/dash/pull/3369) Expose `dash.NoUpdate` type
9+
- [#3371](https://github.com/plotly/dash/pull/3371) Add devtool hook to add components to the devtool bar ui.
10+
11+
## Fixed
12+
- [#3353](https://github.com/plotly/dash/pull/3353) Support pattern-matching/dict ids in `dcc.Loading` `target_components`
13+
- [#3371](https://github.com/plotly/dash/pull/3371) Fix allow_optional triggering a warning for not found input.
14+
- [#3379](https://github.com/plotly/dash/pull/3379) Fix dcc.Graph backward compatibility with dash 2.0 for ddk.Graph
15+
- [#3373](https://github.com/plotly/dash/pull/3373) Fix layout as list and persistence.
16+
17+
# Changed
18+
- [#3365](https://github.com/plotly/dash/pull/3365) Warn if dcc.Slider has more than 500 marks and use default instead.
19+
520
# [3.1.1] - 2025-06-29
621

722
## Fixed

components/dash-core-components/package-lock.json

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

components/dash-core-components/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dash-core-components",
3-
"version": "3.1.0",
3+
"version": "3.2.0",
44
"description": "Core component suite for Dash",
55
"repository": {
66
"type": "git",
@@ -63,11 +63,11 @@
6363
"uniqid": "^5.4.0"
6464
},
6565
"devDependencies": {
66-
"@babel/cli": "^7.27.2",
67-
"@babel/core": "^7.27.4",
68-
"@babel/eslint-parser": "^7.27.5",
66+
"@babel/cli": "^7.28.0",
67+
"@babel/core": "^7.28.0",
68+
"@babel/eslint-parser": "^7.28.0",
6969
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
70-
"@babel/preset-env": "^7.27.2",
70+
"@babel/preset-env": "^7.28.0",
7171
"@babel/preset-react": "^7.27.1",
7272
"@plotly/dash-component-plugins": "^1.2.3",
7373
"@plotly/webpack-dash-dynamic-import": "^1.3.0",
@@ -85,8 +85,8 @@
8585
"react-jsx-parser": "1.21.0",
8686
"rimraf": "^5.0.5",
8787
"style-loader": "^3.3.3",
88-
"styled-jsx": "^3.4.4",
89-
"webpack": "^5.99.9",
88+
"styled-jsx": "^5.1.7",
89+
"webpack": "^5.101.0",
9090
"webpack-cli": "^5.1.4"
9191
},
9292
"optionalDependencies": {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ function Loading({
8787
custom_spinner,
8888
}) {
8989
const ctx = window.dash_component_api.useDashContext();
90+
9091
const loading = ctx.useSelector(
9192
loadingSelector(ctx.componentPath, target_components),
9293
equals

components/dash-core-components/src/fragments/Graph.react.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ import {
1313
import PropTypes from 'prop-types';
1414
import {graphPropTypes, graphDefaultProps} from '../components/Graph.react';
1515

16-
import LoadingElement from '../utils/LoadingElement';
17-
1816
/* global Plotly:true */
1917

2018
import ResizeDetector from '../utils/ResizeDetector';
19+
import LoadingElement from '../utils/LoadingElement';
2120

2221
/**
2322
* `autosize: true` causes Plotly.js to conform to the parent element size.
@@ -537,23 +536,46 @@ class PlotlyGraph extends Component {
537536
}
538537

539538
render() {
540-
const {className, id} = this.props;
539+
const {className, id, loading_state} = this.props;
541540
const style = this.getStyle();
542541

542+
if (window.dash_component_api) {
543+
return (
544+
<LoadingElement
545+
id={id}
546+
key={id}
547+
className={className}
548+
style={style}
549+
ref={this.parentElement}
550+
>
551+
<ResizeDetector
552+
onResize={this.graphResize}
553+
targets={[this.parentElement, this.gd]}
554+
/>
555+
<div
556+
ref={this.gd}
557+
style={{height: '100%', width: '100%'}}
558+
/>
559+
</LoadingElement>
560+
);
561+
}
543562
return (
544-
<LoadingElement
563+
<div
545564
id={id}
546565
key={id}
547566
className={className}
548567
style={style}
549568
ref={this.parentElement}
569+
data-dash-is-loading={
570+
(loading_state && loading_state.is_loading) || undefined
571+
}
550572
>
551573
<ResizeDetector
552574
onResize={this.graphResize}
553575
targets={[this.parentElement, this.gd]}
554576
/>
555577
<div ref={this.gd} style={{height: '100%', width: '100%'}} />
556-
</LoadingElement>
578+
</div>
557579
);
558580
}
559581
}

components/dash-core-components/src/fragments/Slider.react.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
} from '../utils/formatSliderTooltip';
1818
import LoadingElement from '../utils/LoadingElement';
1919

20+
const MAX_MARKS = 500;
21+
2022
const sliderProps = [
2123
'min',
2224
'max',
@@ -76,6 +78,21 @@ export default class Slider extends Component {
7678
} = this.props;
7779
const value = this.state.value;
7880

81+
// Check if marks exceed 500 limit for performance
82+
let processedMarks = marks;
83+
if (marks && typeof marks === 'object' && marks !== null) {
84+
const marksCount = Object.keys(marks).length;
85+
if (marksCount > MAX_MARKS) {
86+
/* eslint-disable no-console */
87+
console.error(
88+
`dcc.Slider: Too many marks (${marksCount}) provided. ` +
89+
`For performance reasons, marks are limited to 500. ` +
90+
`Using auto-generated marks instead.`
91+
);
92+
processedMarks = undefined;
93+
}
94+
}
95+
7996
let tipProps, tipFormatter;
8097
if (tooltip) {
8198
/**
@@ -136,11 +153,16 @@ export default class Slider extends Component {
136153
tipFormatter={tipFormatter}
137154
style={{position: 'relative'}}
138155
value={value}
139-
marks={sanitizeMarks({min, max, marks, step})}
140-
max={setUndefined(min, max, marks).max_mark}
141-
min={setUndefined(min, max, marks).min_mark}
156+
marks={sanitizeMarks({
157+
min,
158+
max,
159+
marks: processedMarks,
160+
step,
161+
})}
162+
max={setUndefined(min, max, processedMarks).max_mark}
163+
min={setUndefined(min, max, processedMarks).min_mark}
142164
step={
143-
step === null && !isNil(marks)
165+
step === null && !isNil(processedMarks)
144166
? null
145167
: calcStep(min, max, step)
146168
}

components/dash-core-components/tests/integration/loading/test_loading_component.py

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from multiprocessing import Lock
22
from dash import Dash, Input, Output, dcc, html
3+
from dash.dependencies import stringify_id
34
from dash.testing import wait
45
import time
56

@@ -414,9 +415,9 @@ def updateDiv(n_clicks):
414415
assert dash_dcc.get_logs() == []
415416

416417

417-
# multiple components, only one triggers the spinner
418-
def test_ldcp010_loading_component_target_components(dash_dcc):
419-
418+
# update multiple props of same component, only targeted id/prop triggers spinner
419+
# test that target_components id can be a dict id
420+
def test_ldcp011_loading_component_target_components(dash_dcc):
420421
lock = Lock()
421422

422423
app = Dash(__name__)
@@ -425,53 +426,61 @@ def test_ldcp010_loading_component_target_components(dash_dcc):
425426
[
426427
dcc.Loading(
427428
[
428-
html.Button(id="btn-1"),
429+
html.Button(id={"type": "button", "index": "one"}),
429430
html.Button(id="btn-2"),
431+
html.Button(id="btn-3"),
430432
],
431433
className="loading-1",
432-
target_components={"btn-2": "children"},
434+
target_components={
435+
stringify_id({"type": "button", "index": "one"}): "className"
436+
},
433437
)
434438
],
435439
id="root",
436440
)
437441

438-
@app.callback(Output("btn-1", "children"), [Input("btn-2", "n_clicks")])
442+
@app.callback(
443+
Output({"type": "button", "index": "one"}, "children"),
444+
[Input("btn-2", "n_clicks")],
445+
)
439446
def updateDiv1(n_clicks):
440447
if n_clicks:
441448
with lock:
442449
return "changed 1"
443-
444450
return "content 1"
445451

446-
@app.callback(Output("btn-2", "children"), [Input("btn-1", "n_clicks")])
452+
@app.callback(
453+
Output({"type": "button", "index": "one"}, "className"),
454+
[Input("btn-3", "n_clicks")],
455+
)
447456
def updateDiv2(n_clicks):
448457
if n_clicks:
449458
with lock:
450-
return "changed 2"
451-
452-
return "content 2"
459+
return "new-class"
460+
return ""
453461

454462
dash_dcc.start_server(app)
455463

456-
dash_dcc.wait_for_text_to_equal("#btn-1", "content 1")
457-
dash_dcc.wait_for_text_to_equal("#btn-2", "content 2")
458-
459-
with lock:
460-
dash_dcc.find_element("#btn-1").click()
464+
btn1id = "#" + stringify_id({"type": "button", "index": "one"})
461465

462-
dash_dcc.find_element(".loading-1 .dash-spinner")
463-
dash_dcc.wait_for_text_to_equal("#btn-2", "")
464-
465-
dash_dcc.wait_for_text_to_equal("#btn-2", "changed 2")
466+
dash_dcc.wait_for_text_to_equal(btn1id, "content 1")
466467

467468
with lock:
468469
dash_dcc.find_element("#btn-2").click()
469-
spinners = dash_dcc.find_elements(".loading-1 .dash-spinner")
470-
dash_dcc.wait_for_text_to_equal("#btn-1", "")
471470

472-
dash_dcc.wait_for_text_to_equal("#btn-1", "changed 1")
471+
spinners = dash_dcc.find_elements(".loading-1 .dash-spinner")
472+
dash_dcc.wait_for_text_to_equal(btn1id, "")
473+
dash_dcc.wait_for_text_to_equal(btn1id, "changed 1")
473474
assert spinners == []
474475

476+
with lock:
477+
dash_dcc.find_element("#btn-3").click()
478+
479+
dash_dcc.find_element(".loading-1 .dash-spinner")
480+
dash_dcc.wait_for_text_to_equal(btn1id, "")
481+
482+
dash_dcc.wait_for_class_to_equal(btn1id, "new-class")
483+
475484
assert dash_dcc.get_logs() == []
476485

477486

components/dash-core-components/tests/integration/sliders/test_sliders.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,3 +616,50 @@ def test_sls016_sliders_format_tooltips(dash_dcc):
616616
dash_dcc.percy_snapshot("sliders-format-tooltips")
617617

618618
assert dash_dcc.get_logs() == []
619+
620+
621+
def test_slsl017_marks_limit_500(dash_dcc):
622+
"""Test that slider works with exactly 500 marks"""
623+
app = Dash(__name__)
624+
marks_500 = {str(i): f"Mark {i}" for i in range(500)}
625+
app.layout = html.Div(
626+
[
627+
dcc.Slider(id="slider", min=0, max=499, marks=marks_500, value=250),
628+
html.Div(id="output"),
629+
]
630+
)
631+
632+
@app.callback(Output("output", "children"), [Input("slider", "value")])
633+
def update_output(value):
634+
return f"Selected: {value}"
635+
636+
dash_dcc.start_server(app)
637+
dash_dcc.wait_for_text_to_equal("#output", "Selected: 250")
638+
639+
# No warnings should be logged for 500 marks
640+
assert dash_dcc.get_logs() == []
641+
642+
643+
def test_slsl018_marks_limit_exceeded(dash_dcc):
644+
"""Test behavior when marks exceed 500 limit"""
645+
app = Dash(__name__)
646+
marks_501 = {str(i): f"Mark {i}" for i in range(501)}
647+
app.layout = html.Div(
648+
[
649+
dcc.Slider(id="slider", min=0, max=500, marks=marks_501, value=250),
650+
html.Div(id="output"),
651+
]
652+
)
653+
654+
@app.callback(Output("output", "children"), [Input("slider", "value")])
655+
def update_output(value):
656+
return f"Selected: {value}"
657+
658+
dash_dcc.start_server(app)
659+
dash_dcc.wait_for_text_to_equal("#output", "Selected: 250")
660+
661+
# Check that warning is logged
662+
logs = dash_dcc.get_logs()
663+
assert len(logs) > 0
664+
warning_found = any("Too many marks" in log["message"] for log in logs)
665+
assert warning_found, "Expected warning about too many marks not found in logs"

0 commit comments

Comments
 (0)