1
+ import PropTypes from 'prop-types' ;
2
+ import React , { Component } from 'react' ;
3
+ import { isPlotlyBug } from './PlotUtil' ;
4
+
5
+ // The naming convention is:
6
+ // - events are attached as `'plotly_' + eventName.toLowerCase()`
7
+ // - react props are `'on' + eventName`
8
+ const eventNames = [
9
+ 'AfterExport' ,
10
+ 'AfterPlot' ,
11
+ 'Animated' ,
12
+ 'AnimatingFrame' ,
13
+ 'AnimationInterrupted' ,
14
+ 'AutoSize' ,
15
+ 'BeforeExport' ,
16
+ 'ButtonClicked' ,
17
+ 'Click' ,
18
+ 'ClickAnnotation' ,
19
+ 'Deselect' ,
20
+ 'DoubleClick' ,
21
+ 'Framework' ,
22
+ 'Hover' ,
23
+ 'LegendClick' ,
24
+ 'LegendDoubleClick' ,
25
+ 'Relayout' ,
26
+ 'Restyle' ,
27
+ 'Redraw' ,
28
+ 'Selected' ,
29
+ 'Selecting' ,
30
+ 'SliderChange' ,
31
+ 'SliderEnd' ,
32
+ 'SliderStart' ,
33
+ 'Transitioning' ,
34
+ 'TransitionInterrupted' ,
35
+ 'Unhover' ,
36
+ ] ;
37
+
38
+ const updateEvents = [
39
+ 'plotly_restyle' ,
40
+ 'plotly_redraw' ,
41
+ 'plotly_relayout' ,
42
+ 'plotly_doubleclick' ,
43
+ 'plotly_animated' ,
44
+ ] ;
45
+
46
+ // Check if a window is available since SSR (server-side rendering)
47
+ // breaks unnecessarily if you try to use it server-side.
48
+ const isBrowser = typeof window !== 'undefined' ;
49
+
50
+ export default function plotComponentFactory ( Plotly ) {
51
+ class PlotlyComponent extends Component {
52
+ constructor ( props ) {
53
+ super ( props ) ;
54
+
55
+ this . p = Promise . resolve ( ) ;
56
+ this . resizeHandler = null ;
57
+ this . handlers = { } ;
58
+
59
+ this . syncWindowResize = this . syncWindowResize . bind ( this ) ;
60
+ this . syncEventHandlers = this . syncEventHandlers . bind ( this ) ;
61
+ this . attachUpdateEvents = this . attachUpdateEvents . bind ( this ) ;
62
+ this . getRef = this . getRef . bind ( this ) ;
63
+ this . handleUpdate = this . handleUpdate . bind ( this ) ;
64
+ this . figureCallback = this . figureCallback . bind ( this ) ;
65
+ this . updatePlotly = this . updatePlotly . bind ( this ) ;
66
+ }
67
+
68
+ updatePlotly ( shouldInvokeResizeHandler , figureCallbackFunction , shouldAttachUpdateEvents ) {
69
+ this . p = this . p
70
+ . then ( ( ) => {
71
+ if ( ! this . el ) {
72
+ let error ;
73
+ if ( this . unmounting ) {
74
+ error = new Error ( 'Component is unmounting' ) ;
75
+ error . reason = 'unmounting' ;
76
+ } else {
77
+ error = new Error ( 'Missing element reference' ) ;
78
+ }
79
+ throw error ;
80
+ }
81
+ if ( isPlotlyBug ( this . el , this . props . data ) ) {
82
+ return Plotly . newPlot ( this . el , {
83
+ data : this . props . data ,
84
+ layout : this . props . layout ,
85
+ config : this . props . config ,
86
+ frames : this . props . frames ,
87
+ } ) ;
88
+ } else {
89
+ return Plotly . react ( this . el , {
90
+ data : this . props . data ,
91
+ layout : this . props . layout ,
92
+ config : this . props . config ,
93
+ frames : this . props . frames ,
94
+ } ) ;
95
+ }
96
+ } )
97
+ . then ( ( ) => this . syncWindowResize ( shouldInvokeResizeHandler ) )
98
+ . then ( this . syncEventHandlers )
99
+ . then ( ( ) => this . figureCallback ( figureCallbackFunction ) )
100
+ . then ( shouldAttachUpdateEvents ? this . attachUpdateEvents : ( ) => {
101
+ } )
102
+ . catch ( err => {
103
+ if ( err . reason === 'unmounting' ) {
104
+ return ;
105
+ }
106
+ console . error ( 'Error while plotting:' , err ) ; // eslint-disable-line no-console
107
+ if ( this . props . onError ) {
108
+ this . props . onError ( err ) ;
109
+ }
110
+ } ) ;
111
+ }
112
+
113
+ componentDidMount ( ) {
114
+ this . unmounting = false ;
115
+
116
+ this . updatePlotly ( true , this . props . onInitialized , true ) ;
117
+ }
118
+
119
+ componentDidUpdate ( prevProps ) {
120
+ this . unmounting = false ;
121
+
122
+ // frames *always* changes identity so fall back to check length only :(
123
+ const numPrevFrames =
124
+ prevProps . frames && prevProps . frames . length ? prevProps . frames . length : 0 ;
125
+ const numNextFrames =
126
+ this . props . frames && this . props . frames . length ? this . props . frames . length : 0 ;
127
+
128
+ const figureChanged = ! (
129
+ prevProps . layout === this . props . layout &&
130
+ prevProps . data === this . props . data &&
131
+ prevProps . config === this . props . config &&
132
+ numNextFrames === numPrevFrames
133
+ ) ;
134
+ const revisionDefined = prevProps . revision !== void 0 ;
135
+ const revisionChanged = prevProps . revision !== this . props . revision ;
136
+
137
+ if ( ! figureChanged && ( ! revisionDefined || ( revisionDefined && ! revisionChanged ) ) ) {
138
+ return ;
139
+ }
140
+
141
+ this . updatePlotly ( false , this . props . onUpdate , false ) ;
142
+ }
143
+
144
+ componentWillUnmount ( ) {
145
+ this . unmounting = true ;
146
+
147
+ this . figureCallback ( this . props . onPurge ) ;
148
+
149
+ if ( this . resizeHandler && isBrowser ) {
150
+ window . removeEventListener ( 'resize' , this . resizeHandler ) ;
151
+ this . resizeHandler = null ;
152
+ }
153
+
154
+ this . removeUpdateEvents ( ) ;
155
+
156
+ Plotly . purge ( this . el ) ;
157
+ }
158
+
159
+ attachUpdateEvents ( ) {
160
+ if ( ! this . el || ! this . el . removeListener ) {
161
+ return ;
162
+ }
163
+
164
+ updateEvents . forEach ( updateEvent => {
165
+ this . el . on ( updateEvent , this . handleUpdate ) ;
166
+ } ) ;
167
+ }
168
+
169
+ removeUpdateEvents ( ) {
170
+ if ( ! this . el || ! this . el . removeListener ) {
171
+ return ;
172
+ }
173
+
174
+ updateEvents . forEach ( updateEvent => {
175
+ this . el . removeListener ( updateEvent , this . handleUpdate ) ;
176
+ } ) ;
177
+ }
178
+
179
+ handleUpdate ( ) {
180
+ this . figureCallback ( this . props . onUpdate ) ;
181
+ }
182
+
183
+ figureCallback ( callback ) {
184
+ if ( typeof callback === 'function' ) {
185
+ const { data, layout} = this . el ;
186
+ const frames = this . el . _transitionData ? this . el . _transitionData . _frames : null ;
187
+ const figure = { data, layout, frames} ;
188
+ callback ( figure , this . el ) ;
189
+ }
190
+ }
191
+
192
+ syncWindowResize ( invoke ) {
193
+ if ( ! isBrowser ) {
194
+ return ;
195
+ }
196
+
197
+ if ( this . props . useResizeHandler && ! this . resizeHandler ) {
198
+ this . resizeHandler = ( ) => Plotly . Plots . resize ( this . el ) ;
199
+ window . addEventListener ( 'resize' , this . resizeHandler ) ;
200
+ if ( invoke ) {
201
+ this . resizeHandler ( ) ;
202
+ }
203
+ } else if ( ! this . props . useResizeHandler && this . resizeHandler ) {
204
+ window . removeEventListener ( 'resize' , this . resizeHandler ) ;
205
+ this . resizeHandler = null ;
206
+ }
207
+ }
208
+
209
+ getRef ( el ) {
210
+ this . el = el ;
211
+
212
+ if ( this . props . debug && isBrowser ) {
213
+ window . gd = this . el ;
214
+ }
215
+ }
216
+
217
+ // Attach and remove event handlers as they're added or removed from props:
218
+ syncEventHandlers ( ) {
219
+ eventNames . forEach ( eventName => {
220
+ const prop = this . props [ 'on' + eventName ] ;
221
+ const handler = this . handlers [ eventName ] ;
222
+ const hasHandler = Boolean ( handler ) ;
223
+
224
+ if ( prop && ! hasHandler ) {
225
+ this . addEventHandler ( eventName , prop ) ;
226
+ } else if ( ! prop && hasHandler ) {
227
+ // Needs to be removed:
228
+ this . removeEventHandler ( eventName ) ;
229
+ } else if ( prop && hasHandler && prop !== handler ) {
230
+ // replace the handler
231
+ this . removeEventHandler ( eventName ) ;
232
+ this . addEventHandler ( eventName , prop ) ;
233
+ }
234
+ } ) ;
235
+ }
236
+
237
+ addEventHandler ( eventName , prop ) {
238
+ this . handlers [ eventName ] = prop ;
239
+ this . el . on ( this . getPlotlyEventName ( eventName ) , this . handlers [ eventName ] ) ;
240
+ }
241
+
242
+ removeEventHandler ( eventName ) {
243
+ this . el . removeListener ( this . getPlotlyEventName ( eventName ) , this . handlers [ eventName ] ) ;
244
+ delete this . handlers [ eventName ] ;
245
+ }
246
+
247
+ getPlotlyEventName ( eventName ) {
248
+ return 'plotly_' + eventName . toLowerCase ( ) ;
249
+ }
250
+
251
+ render ( ) {
252
+ return (
253
+ < div
254
+ id = { this . props . divId }
255
+ style = { this . props . style }
256
+ ref = { this . getRef }
257
+ className = { this . props . className }
258
+ />
259
+ ) ;
260
+ }
261
+ }
262
+
263
+ PlotlyComponent . propTypes = {
264
+ data : PropTypes . arrayOf ( PropTypes . object ) ,
265
+ config : PropTypes . object ,
266
+ layout : PropTypes . object ,
267
+ frames : PropTypes . arrayOf ( PropTypes . object ) ,
268
+ revision : PropTypes . number ,
269
+ onInitialized : PropTypes . func ,
270
+ onPurge : PropTypes . func ,
271
+ onError : PropTypes . func ,
272
+ onUpdate : PropTypes . func ,
273
+ debug : PropTypes . bool ,
274
+ style : PropTypes . object ,
275
+ className : PropTypes . string ,
276
+ useResizeHandler : PropTypes . bool ,
277
+ divId : PropTypes . string ,
278
+ } ;
279
+
280
+ eventNames . forEach ( eventName => {
281
+ PlotlyComponent . propTypes [ 'on' + eventName ] = PropTypes . func ;
282
+ } ) ;
283
+
284
+ PlotlyComponent . defaultProps = {
285
+ debug : false ,
286
+ useResizeHandler : false ,
287
+ data : [ ] ,
288
+ style : { position : 'relative' , display : 'inline-block' } ,
289
+ } ;
290
+
291
+ return PlotlyComponent ;
292
+ }
0 commit comments