Skip to content

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

Merged
merged 37 commits into from
Jan 22, 2015
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9b812f3
first pass at plotly-ipython graph widget
chriddyp Dec 31, 2014
432fa10
load up widget.js code on import
chriddyp Jan 6, 2015
0f14ee1
Update widget methods with new functionality.
theengineear Jan 9, 2015
695dd28
Handle communication with local plotly.
theengineear Jan 10, 2015
b66b017
Update syntax for new plotly js code.
theengineear Jan 10, 2015
964d193
Tricky double-sending fix… (more)
theengineear Jan 10, 2015
67e2b0c
missed listen event here.
theengineear Jan 11, 2015
415f8b5
Better fix for plotly_domain. (more)
theengineear Jan 11, 2015
f732045
Merge branch 'widgets' into js-import
chriddyp Jan 12, 2015
49498a0
Merge pull request #172 from plotly/js-import
chriddyp Jan 12, 2015
4296f5c
Merge branch 'master' of https://github.com/plotly/python-api into wi…
theengineear Jan 12, 2015
4bbe79c
message-uids
chriddyp Jan 12, 2015
fa35b2d
rm debug info
chriddyp Jan 12, 2015
7b5abf3
Merge pull request #178 from plotly/message-uids
chriddyp Jan 12, 2015
0e459c0
Merge branch 'widgets' of https://github.com/plotly/python-api into w…
theengineear Jan 12, 2015
b405404
Merge branch 'widgets' into widgets-update-syntax
theengineear Jan 13, 2015
ad65d7e
update to current postMessage names.
theengineear Jan 13, 2015
edf7ec6
use synced _plotly_domain value instead of message
theengineear Jan 13, 2015
03f2409
frameId -> graphId to be consistent with python side
chriddyp Jan 14, 2015
f0d571b
variable plotlyDomain in postMessage
chriddyp Jan 14, 2015
feca305
Merge pull request #177 from plotly/widgets-update-syntax
chriddyp Jan 14, 2015
8e1a602
use our JSON encoder
chriddyp Jan 21, 2015
3cbc340
update hover api
chriddyp Jan 21, 2015
9320ae6
new docstring format
chriddyp Jan 21, 2015
34cff2b
GraphWidget spec first draft
chriddyp Jan 21, 2015
0144370
new pong message
chriddyp Jan 21, 2015
10098d5
remove trace data from events
chriddyp Jan 21, 2015
138bbd2
docstrings
chriddyp Jan 21, 2015
e07b25c
de-nest messages for callbacks
chriddyp Jan 21, 2015
9ae4421
update language of callback arguments
chriddyp Jan 21, 2015
1092e8a
add example of updating nested object in restyle
chriddyp Jan 21, 2015
a9f98ad
update indices=None behavior
chriddyp Jan 21, 2015
3840a85
describe replacing object in relayout
chriddyp Jan 21, 2015
e8aebaa
update widget name to GraphWidget
chriddyp Jan 21, 2015
d6a00d7
GraphWidget name change
chriddyp Jan 22, 2015
9b4542b
update docstrings to GraphWidget
chriddyp Jan 22, 2015
749eea5
bump version
chriddyp Jan 22, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plotly/widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . graph_widget import Graph
105 changes: 105 additions & 0 deletions plotly/widgets/graphWidget.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

130 changes: 130 additions & 0 deletions plotly/widgets/graph_widget.py
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
Copy link
Member Author

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


__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))
Copy link
Member Author

Choose a reason for hiding this comment

The 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 require.js can help us here?

Copy link
Member Author

Choose a reason for hiding this comment

The 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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: save the state of the messages in the object, so that Graph is a current representation of the current view of the embedded graph - the ranges, the data, etc


def restyle(self, data, traces=None):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might want to let people use their standard graph_objs here, and then convert it to the restyle single-level format here, e.g.

Scatter(marker=Marker(color='blue')) -> {"marker.color": "blue"}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote for making restyle and relayout private and adding an update public method which would call restyle / relayout depending on the nature of the data sent.

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}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: doc string + validation

self._handle_outgoing_message(message)