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 ''' 28 DrawingWidget is an interactive Jupyter notebook widget. It works 29 similarly to displaying a Drawing as a cell output but DrawingWidget 30 can register callbacks for user mouse events. Within a callback modify 31 the drawing then call .refresh() to update the output in real time. 32 33 Arguments: 34 drawing: The initial Drawing to display. Call .refresh() after 35 modifying or just assign a new Drawing. 36 throttle: If True, limit the rate of mousemove events. For drawings 37 with many elements, this will significantly reduce lag. 38 disable: While True, mouse events will be disabled. 39 frame_delay: If greater than or equal to zero, a timed callback will 40 occur frame_delay milliseconds after the previous drawing 41 update. 42 ''' 43 super().__init__() 44 self.throttle = throttle 45 self.disable = disable 46 self.frame_delay = frame_delay 47 self.drawing = drawing 48 self.mousedown_callbacks = [] 49 self.mousemove_callbacks = [] 50 self.mouseup_callbacks = [] 51 self.timed_callbacks = [] 52 self.exception_callbacks = [] 53 54 self.on_msg(self._receive_msg) 55 56 @property 57 def drawing(self): 58 return self._drawing 59 60 @drawing.setter 61 def drawing(self, drawing): 62 self._drawing = drawing 63 self.refresh() 64 65 def refresh(self): 66 ''' 67 Redraw the displayed output with the current value of self.drawing. 68 ''' 69 self._image = self.drawing.asSvg() 70 71 def _receive_msg(self, _, content, buffers): 72 if not isinstance(content, dict): 73 return 74 name = content.get('name') 75 callbacks = { 76 'mousedown': self.mousedown_callbacks, 77 'mousemove': self.mousemove_callbacks, 78 'mouseup': self.mouseup_callbacks, 79 'timed': self.timed_callbacks, 80 }.get(name, ()) 81 try: 82 if callbacks: 83 if name == 'timed': 84 self._call_handlers(callbacks, content) 85 else: 86 self._call_handlers(callbacks, content.get('x'), 87 content.get('y'), content) 88 except BaseException as e: 89 suppress = any( 90 handler(self, e) 91 for handler in self.exception_callbacks 92 ) 93 if not suppress: 94 raise 95 finally: 96 if name == 'timed': 97 self._frame_blocked += 1 98 else: 99 self._mousemove_blocked += 1 100 101 102 def mousedown(self, handler, remove=False): 103 ''' 104 Register (or unregister) a handler for the mousedown event. 105 106 Arguments: 107 remove: If True, unregister, otherwise register. 108 ''' 109 self.on_msg 110 self._register_handler( 111 self.mousedown_callbacks, handler, remove=remove) 112 113 def mousemove(self, handler, remove=False): 114 ''' 115 Register (or unregister) a handler for the mousemove event. 116 117 Arguments: 118 remove: If True, unregister, otherwise register. 119 ''' 120 self._register_handler( 121 self.mousemove_callbacks, handler, remove=remove) 122 123 def mouseup(self, handler, remove=False): 124 ''' 125 Register (or unregister) a handler for the mouseup event. 126 127 Arguments: 128 remove: If True, unregister, otherwise register. 129 ''' 130 self._register_handler( 131 self.mouseup_callbacks, handler, remove=remove) 132 133 def timed(self, handler, remove=False): 134 ''' 135 Register (or unregister) a handler for the timed event. 136 137 Arguments: 138 remove: If True, unregister, otherwise register. 139 ''' 140 self._register_handler( 141 self.timed_callbacks, handler, remove=remove) 142 143 def on_exception(self, handler, remove=False): 144 ''' 145 Register (or unregister) a handler for exceptions in other handlers. 146 147 If any handler returns True, the exception is suppressed. 148 149 Arguments: 150 remove: If True, unregister, otherwise register. 151 ''' 152 self._register_handler( 153 self.exception_callbacks, handler, remove=remove) 154 155 def _register_handler(self, callback_list, handler, remove=False): 156 if remove: 157 callback_list.remove(handler) 158 else: 159 callback_list.append(handler) 160 161 def _call_handlers(self, callback_list, *args, **kwargs): 162 for callback in callback_list: 163 callback(self, *args, **kwargs)