Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets

Add interactive Jupyter widget

Changed files
+193
drawSvg
+1
drawSvg/widgets/__init__.py
···
+
from .drawing_widget import DrawingWidget
+77
drawSvg/widgets/drawing_javascript.py
···
+
javascript = '''
+
require.undef('drawingview');
+
+
define('drawingview', ["@jupyter-widgets/base"], function(widgets) {
+
var DrawingView = widgets.DOMWidgetView.extend({
+
render: function() {
+
this.container = document.createElement('a');
+
this.image_changed();
+
this.container.appendChild(this.svg_view);
+
this.el.appendChild(this.container);
+
this.model.on('change:_image', this.image_changed, this);
+
},
+
image_changed: function() {
+
this.container.innerHTML = this.model.get('_image');
+
this.svg_view = this.container.getElementsByTagName('svg')[0];
+
this.cursor_point = this.svg_view.createSVGPoint();
+
this.register_events();
+
},
+
send_mouse_event: function(name, e) {
+
if (this.model.get('disable')) {
+
return;
+
}
+
if (this.model.get('throttle')) {
+
this.model.set('_mousemove_blocked', true);
+
this.model.save_changes();
+
}
+
+
this.cursor_point.x = e.clientX;
+
this.cursor_point.y = e.clientY;
+
var svg_pt = this.cursor_point.matrixTransform(
+
this.svg_view.getScreenCTM().inverse());
+
+
this.send({
+
name: name,
+
x: svg_pt.x,
+
y: -svg_pt.y,
+
type: e.type,
+
button: e.button,
+
buttons: e.buttons,
+
shiftKey: e.shiftKey,
+
altKey: e.altKey,
+
ctrlKey: e.ctrlKey,
+
metaKey: e.metaKey,
+
clientX: e.clientX,
+
clientY: e.clientY,
+
movementX: e.movementX,
+
movementY: e.movementY,
+
timeStamp: e.timeStamp,
+
targetId: e.target ? e.target.id : null,
+
currentTargetId: e.currentTarget ? e.currentTarget.id : null,
+
relatedTargetId: e.relatedTarget ? e.relatedTarget.id : null,
+
});
+
},
+
register_events: function() {
+
var widget = this;
+
this.svg_view.addEventListener('mousedown', function(e) {
+
e.preventDefault();
+
widget.send_mouse_event('mousedown', e);
+
});
+
this.svg_view.addEventListener('mousemove', function(e) {
+
e.preventDefault();
+
if (!widget.model.get('_mousemove_blocked')) {
+
widget.send_mouse_event('mousemove', e);
+
}
+
});
+
this.svg_view.addEventListener('mouseup', function(e) {
+
e.preventDefault();
+
widget.send_mouse_event('mouseup', e);
+
});
+
}
+
});
+
+
return {
+
DrawingView: DrawingView
+
};
+
});
+
'''
+115
drawSvg/widgets/drawing_widget.py
···
+
from ipywidgets import widgets
+
from traitlets import Unicode, Bool
+
+
+
# Register front end javascript
+
from IPython import display
+
from . import drawing_javascript
+
display.display(display.Javascript(drawing_javascript.javascript))
+
del drawing_javascript
+
+
+
class DrawingWidget(widgets.DOMWidget):
+
_view_name = Unicode('DrawingView').tag(sync=True)
+
_view_module = Unicode('drawingview').tag(sync=True)
+
_view_module_version = Unicode('0.1.0').tag(sync=True)
+
_image = Unicode().tag(sync=True)
+
_mousemove_blocked = Bool(False).tag(sync=True)
+
throttle = Bool(True).tag(sync=True)
+
disable = Bool(False).tag(sync=True)
+
+
def __init__(self, drawing, throttle=True, disable=False):
+
'''
+
DrawingWidget is an interactive Jupyter notebook widget. It works
+
similarly to displaying a Drawing as a cell output but DrawingWidget
+
can register callbacks for user mouse events. Within a callback modify
+
the drawing then call .refresh() to update the output in real time.
+
+
Arguments:
+
drawing: The initial Drawing to display. Call .refresh() after
+
modifying or just assign a new Drawing.
+
throttle: If True, limit the rate of mousemove events. For drawings
+
with many elements, this will significantly reduce lag.
+
disable: While True, mouse events will be disabled.
+
'''
+
super().__init__()
+
self.throttle = throttle
+
self.disable = disable
+
self.drawing = drawing
+
self.mousedown_callbacks = []
+
self.mousemove_callbacks = []
+
self.mouseup_callbacks = []
+
+
self.on_msg(self._receive_msg)
+
+
@property
+
def drawing(self):
+
return self._drawing
+
+
@drawing.setter
+
def drawing(self, drawing):
+
self._drawing = drawing
+
self.refresh()
+
+
def refresh(self):
+
'''
+
Redraw the displayed output with the current value of self.drawing.
+
'''
+
self._image = self.drawing.asSvg()
+
+
def _receive_msg(self, _, content, buffers):
+
if not isinstance(content, dict):
+
return
+
callbacks = {
+
'mousedown': self.mousedown_callbacks,
+
'mousemove': self.mousemove_callbacks,
+
'mouseup': self.mouseup_callbacks,
+
}.get(content.get('name'), ())
+
try:
+
if callbacks:
+
self._call_handlers(callbacks, content.get('x'),
+
content.get('y'), content)
+
finally:
+
self._mousemove_blocked = False
+
+
+
def mousedown(self, handler, remove=False):
+
'''
+
Register (or unregister) a handler for the mousedown event.
+
+
Arguments:
+
remove: If True, unregister, otherwise register.
+
'''
+
self.on_msg
+
self._register_handler(
+
self.mousedown_callbacks, handler, remove=remove)
+
+
def mousemove(self, handler, remove=False):
+
'''
+
Register (or unregister) a handler for the mousemove event.
+
+
Arguments:
+
remove: If True, unregister, otherwise register.
+
'''
+
self._register_handler(
+
self.mousemove_callbacks, handler, remove=remove)
+
+
def mouseup(self, handler, remove=False):
+
'''
+
Register (or unregister) a handler for the mouseup event.
+
+
Arguments:
+
remove: If True, unregister, otherwise register.
+
'''
+
self._register_handler(
+
self.mouseup_callbacks, handler, remove=remove)
+
+
def _register_handler(self, callback_list, handler, remove=False):
+
if remove:
+
callback_list.remove(handler)
+
else:
+
callback_list.append(handler)
+
+
def _call_handlers(self, callback_list, *args, **kwargs):
+
for callback in callback_list:
+
callback(self, *args, **kwargs)