-
-
Notifications
You must be signed in to change notification settings - Fork 145
dcc.Graph: Improve responsiveness, expose resize status, allow wrapper extension #706
Changes from 11 commits
6a4a2b3
d721038
6361f5b
a02a986
2f957bf
33afc15
260c90b
ed02f5e
c2fcd2c
3badeb9
327a9e3
46aed75
c3f6fd9
0b78f42
b36919d
b784df8
dc6b4ae
de29b48
9ba52e6
41527bf
f0ba158
79d3bfa
0d81609
860d133
4555c68
c8a5679
c01eb32
28e9dd6
c6a2d4a
577cc8b
571db5a
a30abcb
71852a3
3e4de1c
c60b59c
ed1b823
cd41f75
68b9c8e
a59682e
8beae49
99b9a08
74b08fc
fb218a5
8addc80
d1a1dd5
701809b
530ef65
cb77c3b
b39406e
56d883b
6867be2
c744ae9
de59930
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -108,6 +108,20 @@ PlotlyGraph.propTypes = { | |
*/ | ||
id: PropTypes.string, | ||
|
||
/** | ||
* If True, the Plotly.js plot will be fully responsive to window resize | ||
* and parent element resize event. This is achieved by overriding | ||
* `config.responsive` to True, `figure.layout.autosize` to True and unsetting | ||
* `figure.layout.height` and `figure.layout.width`. | ||
* If False (default), the Plotly.js plot may exhibit certain responsive | ||
* behaviors on its own but nothing is done in Dash to help this behavior. | ||
* This is the legacy behavior of the Graph component. | ||
* If 'auto', the Graph will determine if the Plotly.js plot can be made fully | ||
* responsive (True) or not (False) based on the values in `config.responsive`, | ||
* `figure.layout.autosize`, `figure.layout.height`, `figure.layout.width`. | ||
*/ | ||
responsive: PropTypes.oneOf([true, false, 'auto']), | ||
|
||
/** | ||
* Data from latest click event. Read-only. | ||
*/ | ||
|
@@ -491,6 +505,7 @@ PlotlyGraph.defaultProps = { | |
layout: {}, | ||
frames: [], | ||
}, | ||
responsive: false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defaults to responsive |
||
animate: false, | ||
animation_options: { | ||
frame: { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,42 @@ | ||
import React, {Component} from 'react'; | ||
import {clone, equals, filter, has, includes, isNil, omit, type} from 'ramda'; | ||
import ResizeDetector from 'react-resize-detector'; | ||
import { | ||
clone, | ||
equals, | ||
filter, | ||
has, | ||
includes, | ||
isNil, | ||
mergeDeepRight, | ||
omit, | ||
type, | ||
} from 'ramda'; | ||
import PropTypes from 'prop-types'; | ||
import {graphPropTypes, graphDefaultProps} from '../components/Graph.react'; | ||
/* global Plotly:true */ | ||
|
||
/** | ||
* `autosize: true` causes Plotly.js to conform to the parent element size. | ||
* This is necessary for `dcc.Graph` call to `Plotly.Plots.resize(target)` to do something. | ||
* | ||
* Users can override this value for specific use-cases by explicitly passing `autoresize: true` | ||
* if `responsive` is not set to True. | ||
*/ | ||
const DEFAULT_LAYOUT = { | ||
autosize: true, | ||
}; | ||
|
||
/** | ||
* `responsive: true` causes Plotly.js to resize the graph on `window.resize`. | ||
* This is necessary for `dcc.Graph` call to `Plotly.Plots.resize(target)` to do something. | ||
* | ||
* Users can override this value for specific use-cases by explicitly passing `responsive: false` | ||
* if `responsive` is not set to True. | ||
*/ | ||
const DEFAULT_CONFIG = { | ||
responsive: true, | ||
}; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When responsive is true, whether through true or auto, will apply these values to config/figure to get the desired behavior. |
||
const filterEventData = (gd, eventData, event) => { | ||
let filteredEventData; | ||
if (includes(event, ['click', 'hover', 'selected'])) { | ||
|
@@ -79,10 +112,13 @@ class PlotlyGraph extends Component { | |
this._hasPlotted = false; | ||
this._prevGd = null; | ||
this.graphResize = this.graphResize.bind(this); | ||
this.isResponsive = this.isResponsive.bind(this); | ||
} | ||
|
||
plot(props) { | ||
const {figure, animate, animation_options, config} = props; | ||
const responsive = this.isResponsive(props); | ||
|
||
const gd = this.gd.current; | ||
|
||
if ( | ||
|
@@ -92,11 +128,27 @@ class PlotlyGraph extends Component { | |
) { | ||
return Plotly.animate(gd, figure, animation_options); | ||
} | ||
|
||
let layoutClone = figure.layout; | ||
if (layoutClone) { | ||
if (responsive) { | ||
layoutClone = mergeDeepRight(figure.layout, DEFAULT_LAYOUT); | ||
delete layoutClone.height; | ||
delete layoutClone.width; | ||
} else { | ||
layoutClone = clone(figure.layout); | ||
} | ||
} | ||
|
||
const configClone = responsive | ||
? mergeDeepRight(config, DEFAULT_CONFIG) | ||
: clone(config); | ||
|
||
return Plotly.react(gd, { | ||
data: figure.data, | ||
layout: clone(figure.layout), | ||
layout: layoutClone, | ||
frames: figure.frames, | ||
config: config, | ||
config: configClone, | ||
}).then(() => { | ||
const gd = this.gd.current; | ||
|
||
|
@@ -154,7 +206,28 @@ class PlotlyGraph extends Component { | |
clearExtendData(); | ||
} | ||
|
||
isResponsive(props) { | ||
const {config, figure, responsive} = props; | ||
|
||
if (type(responsive) === 'Boolean') { | ||
return responsive; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If responsive is a boolean, use as-is. Otherwise, determined auto responsiveness by looking at config/figure for the graph. |
||
|
||
return ( | ||
(config.responsive || isNil(config.responsive)) && | ||
(!figure.layout || | ||
((figure.layout.autosize || isNil(figure.layout.autosize)) && | ||
(isNil(figure.layout.height) || | ||
isNil(figure.layout.width)))) | ||
); | ||
} | ||
|
||
graphResize() { | ||
const responsive = this.isResponsive(this.props); | ||
if (!responsive) { | ||
return; | ||
} | ||
|
||
const gd = this.gd.current; | ||
if (gd) { | ||
Plotly.Plots.resize(gd); | ||
|
@@ -221,10 +294,7 @@ class PlotlyGraph extends Component { | |
} | ||
|
||
componentDidMount() { | ||
this.plot(this.props).then(() => { | ||
window.addEventListener('resize', this.graphResize); | ||
}); | ||
|
||
this.plot(this.props); | ||
if (this.props.extendData) { | ||
this.extend(this.props); | ||
} | ||
|
@@ -238,7 +308,6 @@ class PlotlyGraph extends Component { | |
Plotly.purge(gd); | ||
} | ||
} | ||
window.removeEventListener('resize', this.graphResize); | ||
} | ||
|
||
shouldComponentUpdate(nextProps) { | ||
|
@@ -277,15 +346,27 @@ class PlotlyGraph extends Component { | |
|
||
return ( | ||
<div | ||
key={id} | ||
id={id} | ||
ref={this.gd} | ||
key={id} | ||
data-dash-is-loading={ | ||
(loading_state && loading_state.is_loading) || undefined | ||
} | ||
style={style} | ||
className={className} | ||
/> | ||
> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The graph is now wrapped in a
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still concerned about adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Understandable. My goal here is for old versions of DDK to continue having the same behavior. Given the way DDK styling works and the stated goal of minimizing custom CSS, this should be fine. To break this behavior would require users to assign For the DCC world, this is probably a non-issue, the general idea is that we support classes & attributes with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we don't add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As pointed out by @alexcjohnson, in the end the legacy Discussing this with @wbrgss, it seems like the only scenario left is a new version of DDK pointing to an old version of Dash, but that can be handled through CSS with |
||
<ResizeDetector | ||
handleHeight={true} | ||
handleWidth={true} | ||
refreshMode="debounce" | ||
refreshOptions={{trailing: true}} | ||
refreshRate={50} | ||
onResize={this.graphResize} | ||
/> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. element resize observer - either uses ResizeObserver in newer browsers, or polyfills with a DOM/css method if it doesn't. Debounce / trail / wait to prevent making useless calls. |
||
<div | ||
ref={this.gd} | ||
style={{height: '100%', width: '100%'}} | ||
className={className} | ||
/> | ||
</div> | ||
); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allow users to define the behavior they want for their graph, allowing them to override the normal behavior for their
config
/figure
values.