Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets
1from ipywidgets import widgets 2from traitlets import Unicode, Bool, Int 3 4 5# Register front end javascript 6from IPython import display 7from . import drawing_javascript 8display.display(display.Javascript(drawing_javascript.javascript)) 9del drawing_javascript 10 11 12class DrawingWidget(widgets.DOMWidget): 13 _model_name = Unicode('DrawingModel').tag(sync=True) 14 _model_module = Unicode('drawingview').tag(sync=True) 15 _model_module_version = Unicode('0.1.0').tag(sync=True) 16 _view_name = Unicode('DrawingView').tag(sync=True) 17 _view_module = Unicode('drawingview').tag(sync=True) 18 _view_module_version = Unicode('0.1.0').tag(sync=True) 19 _image = Unicode().tag(sync=True) 20 _mousemove_blocked = Int(0).tag(sync=True) 21 _frame_blocked = Int(0).tag(sync=True) 22 throttle = Bool(True).tag(sync=True) 23 disable = Bool(False).tag(sync=True) 24 frame_delay = Int(-1).tag(sync=True) 25 26 def __init__(self, drawing, throttle=True, disable=False, frame_delay=-1): 27 '''An interactive Jupyter notebook widget. 28 29 This works similarly to displaying a Drawing as a cell output but 30 DrawingWidget can register callbacks for user mouse events. Within a 31 callback modify the drawing then call .refresh() to update the output in 32 real time. 33 34 Arguments: 35 drawing: The initial Drawing to display. Call .refresh() after 36 modifying or just assign a new Drawing. 37 throttle: If True, limit the rate of mousemove events. For drawings 38 with many elements, this will significantly reduce lag. 39 disable: While True, mouse events will be disabled. 40 frame_delay: If greater than or equal to zero, a timed callback will 41 occur frame_delay milliseconds after the previous drawing 42 update. 43 ''' 44 super().__init__() 45 self.throttle = throttle 46 self.disable = disable 47 self.frame_delay = frame_delay 48 self.drawing = drawing 49 self.mousedown_callbacks = [] 50 self.mousemove_callbacks = [] 51 self.mouseup_callbacks = [] 52 self.timed_callbacks = [] 53 self.exception_callbacks = [] 54 55 self.on_msg(self._receive_msg) 56 57 @property 58 def drawing(self): 59 return self._drawing 60 61 @drawing.setter 62 def drawing(self, drawing): 63 self._drawing = drawing 64 self.refresh() 65 66 def refresh(self): 67 ''' 68 Redraw the displayed output with the current value of self.drawing. 69 ''' 70 self._image = self.drawing.as_svg() 71 72 def _receive_msg(self, _, content, buffers): 73 if not isinstance(content, dict): 74 return 75 name = content.get('name') 76 callbacks = { 77 'mousedown': self.mousedown_callbacks, 78 'mousemove': self.mousemove_callbacks, 79 'mouseup': self.mouseup_callbacks, 80 'timed': self.timed_callbacks, 81 }.get(name, ()) 82 try: 83 if callbacks: 84 if name == 'timed': 85 self._call_handlers(callbacks, content) 86 else: 87 self._call_handlers(callbacks, content.get('x'), 88 content.get('y'), content) 89 except BaseException as e: 90 suppress = any( 91 handler(self, e) 92 for handler in self.exception_callbacks 93 ) 94 if not suppress: 95 raise 96 finally: 97 if name == 'timed': 98 self._frame_blocked += 1 99 else: 100 self._mousemove_blocked += 1 101 102 103 def mousedown(self, handler, remove=False): 104 ''' 105 Register (or unregister) a handler for the mousedown event. 106 107 Arguments: 108 remove: If True, unregister, otherwise register. 109 ''' 110 self.on_msg 111 self._register_handler( 112 self.mousedown_callbacks, handler, remove=remove) 113 114 def mousemove(self, handler, remove=False): 115 ''' 116 Register (or unregister) a handler for the mousemove event. 117 118 Arguments: 119 remove: If True, unregister, otherwise register. 120 ''' 121 self._register_handler( 122 self.mousemove_callbacks, handler, remove=remove) 123 124 def mouseup(self, handler, remove=False): 125 ''' 126 Register (or unregister) a handler for the mouseup event. 127 128 Arguments: 129 remove: If True, unregister, otherwise register. 130 ''' 131 self._register_handler( 132 self.mouseup_callbacks, handler, remove=remove) 133 134 def timed(self, handler, remove=False): 135 ''' 136 Register (or unregister) a handler for the timed event. 137 138 Arguments: 139 remove: If True, unregister, otherwise register. 140 ''' 141 self._register_handler( 142 self.timed_callbacks, handler, remove=remove) 143 144 def on_exception(self, handler, remove=False): 145 ''' 146 Register (or unregister) a handler for exceptions in other handlers. 147 148 If any handler returns True, the exception is suppressed. 149 150 Arguments: 151 remove: If True, unregister, otherwise register. 152 ''' 153 self._register_handler( 154 self.exception_callbacks, handler, remove=remove) 155 156 def _register_handler(self, callback_list, handler, remove=False): 157 if remove: 158 callback_list.remove(handler) 159 else: 160 callback_list.append(handler) 161 162 def _call_handlers(self, callback_list, *args, **kwargs): 163 for callback in callback_list: 164 callback(self, *args, **kwargs)