Skip to content

Commit 4733b66

Browse files
author
jgould
committed
1 parent e3a0efe commit 4733b66

File tree

1 file changed

+292
-0
lines changed

1 file changed

+292
-0
lines changed

src/factory.js

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
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

Comments
 (0)