def test_interactive_image_update(): # jbednar: This test uses 1x1 images that are not actually supported # (as they have infinite resolution), but as InteractiveImage is deprecated # anyway the tests have not been updated. p = figure(x_range=(0, 1), y_range=(0, 1), plot_width=2, plot_height=2) img = InteractiveImage(p, create_image) # Ensure bokeh Document is instantiated img._repr_html_() assert isinstance(img.doc, Document) # Ensure image is updated img.update_image({ 'xmin': 0.5, 'xmax': 1, 'ymin': 0.5, 'ymax': 1, 'w': 1, 'h': 1 }) out = np.array([[4287299584]], dtype=np.uint32) assert img.ds.data['x'] == [0.5] assert img.ds.data['y'] == [0.5] assert img.ds.data['dh'] == [0.5] assert img.ds.data['dw'] == [0.5] assert np.array_equal(img.ds.data['image'][0], out) # Ensure patch message is correct msg = img.get_update_event() event = msg.content['events'][0] assert event['kind'] == 'ColumnDataChanged' assert event['column_source'] == img.ds.ref assert sorted(event['cols']) == ['dh', 'dw', 'image', 'x', 'y'] new = event['new'] assert new['dh'] == [0.5] assert new['dw'] == [0.5] assert new['x'] == [0.5] assert new['y'] == [0.5] image = new['image'][0] assert image['dtype'] == 'uint32' assert image['shape'] == [1, 1] # Ensure events are cleared after update assert img.doc._held_events == []
class FigureViewController(ViewController): """docstring for FigureViewController""" def __init__(self, model=PointImageModel(), x_range=(0,1), # datashader cannot handle 0-sized range y_range=(0,1), # datashader cannot handle 0-sized range customize_ranges=customize_ranges, doc=None, log=None, ): self.customize_ranges = customize_ranges self.height_textinput = TextInput( # value="100", width_policy="min", # height_policy="min", sizing_mode='stretch_width', ) self.query = '' self.model = model fig = figure( x_range=x_range, y_range=y_range, reset_policy="event_only", sizing_mode='stretch_both', ) legend = Div( visible=True, height_policy='max', ) # fig.add_layout(Legend(click_policy='hide')) query_textinput = TextInput( title="query", sizing_mode="stretch_width", value='', width=100 ) options_dropdown = Dropdown( label='Options', sizing_mode='fixed', menu=[('Show/Hide Legend','legend'),('Enable/Disable Auto Update','auto')] ) actions_dropdown = Dropdown( label='Actions', sizing_mode='fixed', menu=[('Fit Window','fit')], ) status_button = Button( label='Auto Update', sizing_mode='fixed', #sizing_mode='stretch_width', width_policy='min', ) view = column( row( self.height_textinput, options_dropdown, actions_dropdown, Spacer(sizing_mode='stretch_width', width_policy='max'), status_button, sizing_mode='stretch_width', ), row(legend, fig, sizing_mode='stretch_both',), query_textinput, sizing_mode='stretch_both', ) super(FigureViewController, self).__init__(view, doc, log) self.height_textinput.on_change('value', self.on_change_height_textinput) self.auto_update_image = True self.options_dropdown = options_dropdown self.options_dropdown.on_click(self.on_click_options_dropdown) self.actions_dropdown = actions_dropdown self.actions_dropdown.on_click(self.on_click_actions_dropdown) self.status_button = status_button self.status_button.on_click(self.on_click_status_button) self.fig = fig self.legend = legend self.query_textinput = query_textinput # Has to be executed before inserting fig in doc self.fig.on_event(LODEnd_event, self.callback_LODEnd) # Has to be executed before inserting fig in doc self.fig.on_event(Reset_event, self.callback_Reset) # Has to be executed before inserting fig in doc # self.color_key = datashader_color self.img = Queue(maxsize=1) self.interactiveImage = InteractiveImage(self.fig, self.callback_InteractiveImage) self.user_lock = Lock() # Forbid widget changes when busy self.user_widgets = [ self.query_textinput, self.status_button, self.options_dropdown, self.actions_dropdown, ] self.query_textinput.on_change('value', self.on_change_query_textinput) assert(len(self.fig.renderers) == 1) self.datashader = self.fig.renderers[0] self.source = ColumnDataSource({}) self.hovertool = None self.hide_hovertool_for_category = None self.table = None ####################################### # Functions triggered by User actions # ####################################### def on_change_height_textinput(self, attr, old, new): fname = self.on_change_height_textinput.__name__ try: self.fig.plot_height = int(self.height_textinput.value) except Exception as e: self.log('Exception({}) in {}:{}'.format(type(e), fname, e)) self.log(traceback.format_exc()) def on_click_status_button(self, new): fname = self.on_click_status_button.__name__ if not self.user_lock.acquire(False): self.log('Could not acquire user_lock in {}'.format(fname)) return def target(): try: self.set_busy() self.update_image() self.set_update() except Exception as e: self.log('Exception({}) in {}:{}'.format(type(e), fname, e)) self.log(traceback.format_exc()) self.set_failed() else: self.user_lock.release() Thread(target=target).start() def on_click_actions_dropdown(self, new): if new.item == 'fit': self.action_fit_window() pass else: raise Exception('Exception in on_click_actions_dropdown: {}'.format(new.item)) def on_click_options_dropdown(self, new): # Very short, no need to spawn a Thread if new.item == 'legend': self.legend.visible = not self.legend.visible elif new.item == 'auto': self.auto_update_image = not self.auto_update_image else: raise Exception('Exception in on_click_options_dropdown: {}'.format(new.item)) pass def action_fit_window(self): fname = self.fit_window.__name__ if not self.user_lock.acquire(False): self.log('Could not acquire user_lock in {}'.format(fname)) return def target(): try: self.set_busy() xmin, xmax, ymin, ymax = self.model.result_ranges() self.fit_window(xmin, xmax, ymin, ymax) self.set_update() except Exception as e: self.log('Exception({}) in {}:{}'.format(type(e), fname, e)) self.log(traceback.format_exc()) self.set_failed() else: self.user_lock.release() Thread(target=target).start() def plot(self, **kwargs): fname = self.plot.__name__ if not self.user_lock.acquire(False): self.log('Could not acquire user_lock in {}'.format(fname)) return def target(model=None, config=None, width=None, height=None): try: self.set_busy() self.model = model xmin, xmax, ymin, ymax = self.model.data_ranges() self._plot(config, width, height, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax) self.set_update() except Exception as e: self.log('Exception({}) in {}:{}'.format(type(e), fname, e)) self.log(traceback.format_exc()) self.set_failed() else: self.user_lock.release() Thread(target=target, kwargs=kwargs).start() def on_change_query_textinput(self, attr, old, new): fname = self.on_change_query_textinput.__name__ if not self.user_lock.acquire(False): self.log('Could not acquire user_lock in {}'.format(fname)) return def target(): try: self.set_busy() self.query = self.query_textinput.value if self.auto_update_image: self.update_image() self.set_update() except Exception as e: self.log('Exception({}) in {}:{}'.format(type(e), fname, e)) self.log(traceback.format_exc()) self.set_failed() else: self.user_lock.release() Thread(target=target).start() def callback_LODEnd(self, event): fname = self.callback_LODEnd.__name__ if not self.auto_update_image: return if not self.user_lock.acquire(False): self.log('Could not acquire user_lock in {}'.format(fname)) return def target(): try: self.set_busy() self.update_image() self.set_update() except Exception as e: self.log('Exception({}) in {}:{}'.format(type(e), fname, e)) self.log(traceback.format_exc()) self.set_failed() else: self.user_lock.release() Thread(target=target).start() def callback_Reset(self, event): fname = self.callback_Reset.__name__ if not self.user_lock.acquire(False): self.log('Could not acquire user_lock in {}'.format(fname)) return def target(): try: self.set_busy() xmin, xmax, ymin, ymax = self.model.data_ranges() self.fit_window(xmin, xmax, ymin, ymax) self.update_image(xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax) self.set_update() except Exception as e: self.log('Exception({}) in {}:{}'.format(type(e), fname, e)) self.log(traceback.format_exc()) self.set_failed() else: self.user_lock.release() Thread(target=target).start() #################################### # Functions modifying the document # #################################### # Try to avoid intensive computation # Must use coroutine def fit_window(self, xmin, xmax, ymin, ymax): @gen.coroutine def coroutine(xmin, xmax, ymin, ymax): self.fig.x_range.start = xmin self.fig.x_range.end = xmax self.fig.y_range.start = ymin self.fig.y_range.end = ymax if self.doc: self.doc.add_next_tick_callback(partial(coroutine, xmin, xmax, ymin, ymax)) def set_failed(self): @gen.coroutine def coroutine(): for e in self.user_widgets: e.disabled= False self.visible = True self.status_button.label = "Failed" self.status_button.button_type = "failure" if self.doc: self.doc.add_next_tick_callback(partial(coroutine)) def set_busy(self): @gen.coroutine def coroutine(): for e in self.user_widgets: e.disabled= True self.visible = True self.status_button.label = "Busy" self.status_button.button_type = "warning" if self.doc: self.doc.add_next_tick_callback(partial(coroutine)) def set_update(self): @gen.coroutine def coroutine(): for e in self.user_widgets: e.disabled= False self.visible = True if self.auto_update_image: self.status_button.label = "Auto Update" else: self.status_button.label = "Update" self.status_button.button_type = "success" if self.doc: self.doc.add_next_tick_callback(partial(coroutine)) @ViewController.logFunctionCall def _plot(self, config, width, height, xmin, xmax, ymin, ymax): @gen.coroutine def coroutine(config, width, height, xmin, xmax, ymin, ymax): self.fig.x_range.start = xmin self.fig.x_range.end = xmax self.fig.y_range.start = ymin self.fig.y_range.end = ymax self.fig.plot_width = width self.fig.plot_height = height category = config['c'] self.legend.text = '\n'.join( ['Categories:<ul style="list-style: none;padding-left: 0;">']+ [ '<li><span style="color: {};">◼</span>c[{}]={}</li>'.format( category[i]['color'], i, category[i]['label'] ) for i in range(len(category)) if category[i]['len'] > 0 ]+ ["</ul>"] ) # self.color_key = [c['color'] for c in category if c['len'] > 0] self.hide_hovertool_for_category = [ i for i in range(len(category)) if 'hide_hovertool' in category[i] if category[i]['hide_hovertool'] ] df = self.model.data _df = pd.DataFrame({k:[] for k in df.columns}) self.source.data = ColumnDataSource.from_df(_df) if self.table is not None: self.table.columns = [TableColumn(field=c, title=c) for c in _df.columns] glyph = self.model.bokeh_glyph() renderer = self.fig.add_glyph(self.source, glyph) if self.hovertool is None: tooltips = [ ("(x,y)","($x, $y)"), ] for k in df.columns: tooltips.append((k,"@"+str(k))) self.hovertool = HoverTool(tooltips = tooltips) self.fig.add_tools(self.hovertool) self.update_image() if self.doc: self.doc.add_next_tick_callback(partial(coroutine, config, width, height, xmin, xmax, ymin, ymax)) @ViewController.logFunctionCall def update_source(self, df): @gen.coroutine def coroutine(df): self.source.data = ColumnDataSource.from_df(df) if self.table is not None: self.table.columns = [TableColumn(field=c, title=c) for c in df.columns] if self.doc is not None: self.doc.add_next_tick_callback(partial(coroutine, df)) @ViewController.logFunctionCall def callback_InteractiveImage(self, x_range, y_range, plot_width, plot_height, name=None): fname = self.callback_InteractiveImage.__name__ try: img = self.img.get(block=False) except Exception as e: self.log('Exception({}) in {}: {}'.format(type(e), fname, e)) self.log(traceback.format_exc()) img = self._callback_InteractiveImage(x_range, y_range, plot_width, plot_height, name) return img @ViewController.logFunctionCall def fit_figure(self, ranges): @gen.coroutine def coroutine(ranges): self.fig.x_range.start = ranges['xmin'] self.fig.x_range.end = ranges['xmax'] self.fig.y_range.start = ranges['ymin'] self.fig.y_range.end = ranges['ymax'] self.fig.plot_width = ranges['w'] self.set_fig_height(ranges['h']) if self.doc is not None: self.doc.add_next_tick_callback(partial(coroutine, ranges)) def set_fig_height(self, height): height = int(height) self.height_textinput.value = str(height) ############################### # Compute intensive functions # ############################### # Should not be ran in the interactive thread @ViewController.logFunctionCall def apply_query(self): fname = self.apply_query.__name__ if self.mode == 'lines': no_query = self.lines elif self.mode == 'points': no_query = self.points else: raise Exception('Not Yet Implemented') try: if self.query.strip() == '': return no_query self.log('Applying query {}'.format(self.query)) query = no_query.query(self.query) if len(query) == 0: raise Exception( 'QUERY ERROR', '{} => len(lines) == 0'.format(self.query) ) return query except Exception as e: self.log('Exception({}) in {}: {}'.format(type(e), fname, e)) self.log(traceback.format_exc()) return no_query @ViewController.logFunctionCall def _callback_InteractiveImage(self, *args, **kwargs): return self.model.callback_InteractiveImage(*args, **kwargs) @ViewController.logFunctionCall def compute_hovertool(self, ranges): MAX = 100000. xmin = ranges['xmin'] xmax = ranges['xmax'] ymin = ranges['ymin'] ymax = ranges['ymax'] intersection = self.model.generate_intersection_query(xmin, xmax, ymin, ymax) if len(self.hide_hovertool_for_category)==0: query = intersection else: hide_hovertool = "&".join([ "(c!={})".format(c) for c in self.hide_hovertool_for_category ]) query = "({})&({})".format(intersection, hide_hovertool) self.log("HoverTool query={}".format(query)) result = self.model.result.query(query) n = len(result) if n > MAX: frac = MAX/n self.log('Sampling hovertool frac={}'.format(frac)) result = result.sample(frac=frac) else: self.log('Full hovertool') df = dask.compute(result)[0] self.update_source(df) @ViewController.logFunctionCall def update_image(self, **kwargs): ranges = { 'xmin' : self.fig.x_range.start, 'xmax' : self.fig.x_range.end, 'ymin' : self.fig.y_range.start, 'ymax' : self.fig.y_range.end, 'w' : self.fig.plot_width, 'h' : self.fig.plot_height, } for k in ['xmin', 'xmax', 'ymin', 'ymax', 'w', 'h']: if k in kwargs: ranges[k] = kwargs[k] ranges = self.customize_ranges(ranges) self.model.apply_query(self.query) def target0(): try: self.compute_hovertool(ranges) except Exception as e: self.log('Exception({}) in {}:{}'.format(type(e),target0.__name__,e)) self.log(traceback.format_exc()) def target1(): try: xmin, xmax, ymin, ymax, w, h = [ ranges[k] for k in ['xmin', 'xmax', 'ymin', 'ymax', 'w', 'h'] ] self.log(ranges) # debug self.img.put(self._callback_InteractiveImage((xmin,xmax), (ymin,ymax), w, h)) @gen.coroutine def coroutine(): self.interactiveImage.update_image(ranges) if self.doc: self.doc.add_next_tick_callback(partial(coroutine)) except Exception as e: self.log('Exception({}) in {}:{}'.format(type(e),target1.__name__,e)) self.log(traceback.format_exc()) threads = [Thread(target=target0), Thread(target=target1)] for t in threads: t.start() for t in threads: t.join() self.fit_figure(ranges)