Example #1
0
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 == []
Example #2
0
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)