Skip to content

Commit 9b812f3

Browse files
committed
first pass at plotly-ipython graph widget
1 parent d9b7cdc commit 9b812f3

File tree

3 files changed

+236
-0
lines changed

3 files changed

+236
-0
lines changed

plotly/widgets/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . graph_widget import Graph

plotly/widgets/graphWidget.js

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

plotly/widgets/graph_widget.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
from collections import deque
2+
import json
3+
import os
4+
5+
# TODO: protected imports?
6+
from IPython.html import widgets
7+
from IPython.utils.traitlets import Unicode
8+
from IPython.display import Javascript, display
9+
10+
__all__ = None
11+
12+
class Graph(widgets.DOMWidget):
13+
"""An interactive Plotly graph widget for use in IPython
14+
Notebooks.
15+
"""
16+
_view_name = Unicode('GraphView', sync=True)
17+
_message = Unicode(sync=True)
18+
_graph_url = Unicode(sync=True)
19+
20+
def __init__(self, graph_url, **kwargs):
21+
"""Initialize a plotly graph object.
22+
Parameters
23+
----------
24+
graph_url: The url of a Plotly graph
25+
26+
Examples
27+
--------
28+
GraphWidget('https://plot.ly/~chris/3375')
29+
"""
30+
directory = os.path.dirname(os.path.realpath(__file__))
31+
js_widget_file = os.path.join(directory, 'graphWidget.js')
32+
with open(js_widget_file) as f:
33+
js_widget_code = f.read()
34+
35+
display(Javascript(js_widget_code))
36+
37+
super(Graph, self).__init__(**kwargs)
38+
39+
# TODO: Validate graph_url
40+
self._graph_url = graph_url
41+
self._listener_set = set()
42+
self._event_handlers = {
43+
'click': widgets.CallbackDispatcher(),
44+
'hover': widgets.CallbackDispatcher(),
45+
'zoom': widgets.CallbackDispatcher()
46+
}
47+
48+
self._graphId = ''
49+
self.on_msg(self._handle_msg)
50+
51+
# messages to the iframe client need to wait for the
52+
# iframe to communicate that it is ready
53+
# unfortunately, this two-way blocking communication
54+
# isn't possible (https://github.com/ipython/ipython/wiki/IPEP-21:-Widget-Messages#caveats)
55+
# so we'll just cue up messages until they're ready to be sent
56+
self._clientMessages = deque()
57+
58+
def _handle_msg(self, message):
59+
"""Handle a msg from the front-end.
60+
Parameters
61+
----------
62+
content: dict
63+
Content of the msg."""
64+
content = message['content']['data']['content']
65+
if content.get('event', '') == 'pong':
66+
self._graphId = content['graphId']
67+
68+
# ready to recieve - pop out all of the items in the deque
69+
while self._clientMessages:
70+
_message = self._clientMessages.popleft()
71+
_message['graphId'] = self._graphId
72+
_message = json.dumps(_message)
73+
self._message = _message
74+
75+
if content.get('event', '') in ['click', 'hover', 'zoom']:
76+
self._event_handlers[content['event']](self, content)
77+
78+
def _handle_registration(self, event_type, callback, remove):
79+
self._event_handlers[event_type].register_callback(callback,
80+
remove=remove)
81+
event_callbacks = self._event_handlers[event_type].callbacks
82+
if (len(event_callbacks) and event_type not in self._listener_set):
83+
self._listener_set.add(event_type)
84+
message = {'listen': list(self._listener_set)}
85+
self._handle_outgoing_message(message)
86+
87+
def _handle_outgoing_message(self, message):
88+
if self._graphId == '':
89+
self._clientMessages.append(message)
90+
else:
91+
message['graphId'] = self._graphId
92+
self._message = json.dumps(message)
93+
94+
def on_click(self, callback, remove=False):
95+
"""Register a callback to execute when the graph is clicked.
96+
Parameters
97+
----------
98+
remove : bool (optional)
99+
Set to true to remove the callback from the list of callbacks."""
100+
self._handle_registration('click', callback, remove)
101+
102+
def on_hover(self, callback, remove=False):
103+
"""Register a callback to execute when you hover over points in the graph.
104+
Parameters
105+
----------
106+
remove : bool (optional)
107+
Set to true to remove the callback from the list of callbacks."""
108+
self._handle_registration('hover', callback, remove)
109+
110+
def on_zoom(self, callback, remove=False):
111+
"""Register a callback to execute when you zoom in the graph.
112+
Parameters
113+
----------
114+
remove : bool (optional)
115+
Set to true to remove the callback from the list of callbacks."""
116+
self._handle_registration('zoom', callback, remove)
117+
118+
def restyle(self, data, traces=None):
119+
message = {'restyle': data, 'graphId': self._graphId}
120+
if traces:
121+
message['traces'] = traces
122+
self._handle_outgoing_message(message)
123+
124+
def relayout(self, layout):
125+
message = {'relayout': layout, 'graphId': self._graphId}
126+
self._handle_outgoing_message(message)
127+
128+
def hover(self, hover_obj):
129+
message = {'hover': hover_obj, 'graphId': self._graphId}
130+
self._handle_outgoing_message(message)

0 commit comments

Comments
 (0)