-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
first pass at plotly-ipython graph widget #171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
9b812f3
432fa10
0f14ee1
695dd28
b66b017
964d193
67e2b0c
415f8b5
f732045
49498a0
4296f5c
4bbe79c
fa35b2d
7b5abf3
0e459c0
b405404
ad65d7e
edf7ec6
03f2409
f0d571b
feca305
8e1a602
3cbc340
9320ae6
34cff2b
0144370
10098d5
138bbd2
e07b25c
9ae4421
1092e8a
a9f98ad
3840a85
e8aebaa
d6a00d7
9b4542b
749eea5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . graph_widget import Graph |
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 |
---|---|---|
@@ -0,0 +1,130 @@ | ||
from collections import deque | ||
import json | ||
import os | ||
|
||
# TODO: protected imports? | ||
from IPython.html import widgets | ||
from IPython.utils.traitlets import Unicode | ||
from IPython.display import Javascript, display | ||
|
||
__all__ = None | ||
|
||
class Graph(widgets.DOMWidget): | ||
"""An interactive Plotly graph widget for use in IPython | ||
Notebooks. | ||
""" | ||
_view_name = Unicode('GraphView', sync=True) | ||
_message = Unicode(sync=True) | ||
_graph_url = Unicode(sync=True) | ||
|
||
def __init__(self, graph_url, **kwargs): | ||
"""Initialize a plotly graph object. | ||
Parameters | ||
---------- | ||
graph_url: The url of a Plotly graph | ||
|
||
Examples | ||
-------- | ||
GraphWidget('https://plot.ly/~chris/3375') | ||
""" | ||
directory = os.path.dirname(os.path.realpath(__file__)) | ||
js_widget_file = os.path.join(directory, 'graphWidget.js') | ||
with open(js_widget_file) as f: | ||
js_widget_code = f.read() | ||
|
||
display(Javascript(js_widget_code)) | ||
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. not sure what the best way to inject JS code into ipython is. we should only have to do this once - maybe 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. Or maybe do it on import? |
||
|
||
super(Graph, self).__init__(**kwargs) | ||
|
||
# TODO: Validate graph_url | ||
self._graph_url = graph_url | ||
self._listener_set = set() | ||
self._event_handlers = { | ||
'click': widgets.CallbackDispatcher(), | ||
'hover': widgets.CallbackDispatcher(), | ||
'zoom': widgets.CallbackDispatcher() | ||
} | ||
|
||
self._graphId = '' | ||
self.on_msg(self._handle_msg) | ||
|
||
# messages to the iframe client need to wait for the | ||
# iframe to communicate that it is ready | ||
# unfortunately, this two-way blocking communication | ||
# isn't possible (https://github.com/ipython/ipython/wiki/IPEP-21:-Widget-Messages#caveats) | ||
# so we'll just cue up messages until they're ready to be sent | ||
self._clientMessages = deque() | ||
|
||
def _handle_msg(self, message): | ||
"""Handle a msg from the front-end. | ||
Parameters | ||
---------- | ||
content: dict | ||
Content of the msg.""" | ||
content = message['content']['data']['content'] | ||
if content.get('event', '') == 'pong': | ||
self._graphId = content['graphId'] | ||
|
||
# ready to recieve - pop out all of the items in the deque | ||
while self._clientMessages: | ||
_message = self._clientMessages.popleft() | ||
_message['graphId'] = self._graphId | ||
_message = json.dumps(_message) | ||
self._message = _message | ||
|
||
if content.get('event', '') in ['click', 'hover', 'zoom']: | ||
self._event_handlers[content['event']](self, content) | ||
|
||
def _handle_registration(self, event_type, callback, remove): | ||
self._event_handlers[event_type].register_callback(callback, | ||
remove=remove) | ||
event_callbacks = self._event_handlers[event_type].callbacks | ||
if (len(event_callbacks) and event_type not in self._listener_set): | ||
self._listener_set.add(event_type) | ||
message = {'listen': list(self._listener_set)} | ||
self._handle_outgoing_message(message) | ||
|
||
def _handle_outgoing_message(self, message): | ||
if self._graphId == '': | ||
self._clientMessages.append(message) | ||
else: | ||
message['graphId'] = self._graphId | ||
self._message = json.dumps(message) | ||
|
||
def on_click(self, callback, remove=False): | ||
"""Register a callback to execute when the graph is clicked. | ||
Parameters | ||
---------- | ||
remove : bool (optional) | ||
Set to true to remove the callback from the list of callbacks.""" | ||
self._handle_registration('click', callback, remove) | ||
|
||
def on_hover(self, callback, remove=False): | ||
"""Register a callback to execute when you hover over points in the graph. | ||
Parameters | ||
---------- | ||
remove : bool (optional) | ||
Set to true to remove the callback from the list of callbacks.""" | ||
self._handle_registration('hover', callback, remove) | ||
|
||
def on_zoom(self, callback, remove=False): | ||
"""Register a callback to execute when you zoom in the graph. | ||
Parameters | ||
---------- | ||
remove : bool (optional) | ||
Set to true to remove the callback from the list of callbacks.""" | ||
self._handle_registration('zoom', callback, remove) | ||
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. TODO: save the state of the messages in the object, so that |
||
|
||
def restyle(self, data, traces=None): | ||
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. might want to let people use their standard
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'd vote for making |
||
message = {'restyle': data, 'graphId': self._graphId} | ||
if traces: | ||
message['traces'] = traces | ||
self._handle_outgoing_message(message) | ||
|
||
def relayout(self, layout): | ||
message = {'relayout': layout, 'graphId': self._graphId} | ||
self._handle_outgoing_message(message) | ||
|
||
def hover(self, hover_obj): | ||
message = {'hover': hover_obj, 'graphId': self._graphId} | ||
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. todo: doc string + validation |
||
self._handle_outgoing_message(message) |
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.
might want to protect these, and give some useful error messages to folks running older versions of ipython