"""
This module contains all classes and structures needed for live plotting of data via
polling an object. While specifically made for the CBD simulator, it is independent
of any internal workings.
"""
import math
try:
	import matplotlib.pyplot as plt
	import matplotlib.animation as animation
	import matplotlib
	from matplotlib.patches import Arrow as mplArrow
	_MPL_FOUND = True

	try:
		# Note: Seaborn is built on top of matplotlib
		import seaborn as sns

		_SNS_FOUND = True
	except ImportError:
		_SNS_FOUND = False
except ImportError:
	_MPL_FOUND = False
	_SNS_FOUND = False

try:
	from bokeh.plotting import curdoc
	_BOKEH_FOUND = True
except ImportError:
	_BOKEH_FOUND = False

# TODO: Bokeh (see TODOs)
# TODO: More Plot Kinds

class Backend:
	"""
	In which framework the plots need to be made.

	Note:
		Some attributes won't be available if the corresponding framework is not installed.
	"""
	if _MPL_FOUND:
		MPL         = 1
		""" : : Use `Matplotlib <https://matplotlib.org/>`_."""
		MATPLOTLIB  = 1
		""" : : Use `Matplotlib <https://matplotlib.org/>`_."""

	if _BOKEH_FOUND:
		BOKEH       = 2
		"""
		 : : Use `Bokeh <//docs.bokeh.org/en/latest/index.html>`_.
		
		Important:
			For :code:`BOKEH`, a server must be started with:
	
			.. code-block:: bash
	
			   bokeh serve
	
			|
			
		Attention:
			Because of how Bokeh works, it is currently impossible to capture premature
			close events on a system. The plots will always remain active once started.
		"""

	if _SNS_FOUND:
		SNS         = 3
		""" : : Use `Seaborn <https://seaborn.pydata.org/>`_."""
		SEABORN     = 3
		""" : : Use `Seaborn <https://seaborn.pydata.org/>`_."""

	@staticmethod
	def exists(value):
		"""
		Checks that a given backend exists.
		"""
		return value in [getattr(Backend, x) for x in dir(Backend) if not x.startswith("_") and \
		                                                                not callable(getattr(Backend, x))]

	@staticmethod
	def compare(name, value):
		"""
		Compares the value against a backend name.

		Args:
			name (str):     The name of the backend to check for.
			value (int):    The value to compare.
		"""
		return name in dir(Backend) and Backend.exists(value) and getattr(Backend, name) == value

	@staticmethod
	def get(name):
		"""
		Gets the backend with a specific name if it exists and if it's installed.

		Args:
			name (str):     The name of the backend to get.
		"""
		return getattr(Backend, name) if name in dir(Backend) else None


class PlotHandler:
	"""
	Handles Real-Time plotting, independent of a plotting framework.

	Every :code:`interval` time, data will be polled from the :code:`object`,
	w.r.t. the given plotting :code:`backend`. The handler will use the knowledge
	of a backend to determine how the given :code:`figure` must be updated.

	This class can be used in any simulation context, where plotting data can be
	polled from an object or a resource.

	Note:
		While technically framework-independent, matplotlib has the least external
		overhead in your code.

	Tip:
		When using multiple plots/plotting instances, you should make use of the
		:class:`PlotManager`.

	Warning:
		Bokeh is still in active development. Not all features will work as required.

	Raises:
		AssertionError: if the backend cannot be located.

	Args:
		object (Any):       The object from which data needs to be polled. By default,
							the :code:`data_xy` attribute will be used on the object,
							which should result in a 2xN array in the form of
							:code:`[[x1, x2, x3...], [y1, y2, y3...]]`. Change this
							behaviour with the :func:`set_data_getter` function.
		figure:             The figure object required for plotting. This has been made
							externally to allow for full manipulation of the plotting
							framework itself. The following explains the axxepted data
							types:

							- **matplotlib:** Tuple (pair) of :code:`(Figure, Axes)`
							- **bokeh:** :code:`Figure`
							- **seaborn:** Tuple (pair) of :code:`(Figure, Axes)`

		kind (PlotKind):    What needs to be plotted. See :class:`PlotKind` for more
							info.
		backend (Backend):  The backend to use for plotting. Defaults to
							:attr:`Backend.MPL` (= matplotlib).
		interval (int):     Amount of milliseconds between plot refreshes.
		frames (int):       The amount of frames for the animation. Only used if the
							animation needs to be saved.

	See Also:
		- :class:`Backend`
		- :class:`PlotKind`
		- :class:`PlotManager`
	"""
	def __init__(self, object, figure, kind, backend=Backend.MPL, interval=100, frames=None):
		assert Backend.exists(backend), "Invalid backend."
		self.object = object
		self.kind = kind
		self.figure = figure

		self.kind._backend = backend
		self.elm = self.kind.create(figure)
		self.__opened = True
		self.__finished = False
		self.__hidden = False

		# obtaining information
		self.__get_data = lambda obj: obj.data_xy
		self.__events = {
			"preupdate": [],
			"update": [],
			"closed": []
		}

		# backend info:
		if Backend.compare("MPL", backend) or Backend.compare("SNS", backend):
			self.__ani = animation.FuncAnimation(figure[0], lambda _: self.update(),
			                                     interval=interval, frames=frames)
			figure[0].canvas.mpl_connect('close_event', lambda evt: self.__close_event())
		elif Backend.compare("BOKEH", backend):
			curdoc().add_periodic_callback(lambda: self.update(), interval)
			# TODO (is this even possible?):
			curdoc().on_session_destroyed(lambda ctx: self.__close_event())

	def signal(self, name, *args):
		"""
		Raise a signal with a specific name and arguments.

		The accepted signals are:

		- :code:`preupdate(data)`: Raised whenever the plot is about to update.
		  It takes the new plot data that has been polled.
		- :code:`update(data)`: Raised whenever the plot updates. It takes the
		  new plot data that has been polled.
		- :code:`closed`: Raised whenever the plot closes. No additional arguments
		  are associated with this event.

		Note:
			Normally, users do not need to call this function.

		Args:
			name (str):     The name of the signal to raise.
			*args:          Additional arguments to pass to the connected events.

		See Also:
			- :func:`PlotHandler.connect`
			- :func:`PlotManager.connect`
			- :func:`follow`
		"""
		if name not in self.__events:
			raise ValueError("Invalid signal '%s' in PlotHandler." % name)
		for evt in self.__events[name]:
			evt(*args)

	def connect(self, name, function):
		"""
		Connect an event with an additional function.
		Useful for e.g. updating the plot axes w.r.t. the data.

		The functions will be called in the order they were connected to the
		events, with the associated arguments. The accepted signals are:

		- :code:`preupdate(data)`: Raised whenever the plot is about to update.
		  It takes the new plot data that has been polled.
		- :code:`update(data)`: Raised whenever the plot updates. It takes the
		  new plot data that has been polled.
		- :code:`closed`: Raised whenever the plot closes. No additional arguments
		  are associated with this event.

		Args:
			name (str):     The name of the signal to raise.
			function:       A function that will be called with the optional arguments
							whenever the event is raised.

		See Also:
			- :func:`PlotHandler.signal`
			- :func:`PlotManager.connect`
			- :func:`follow`
		"""
		if name not in self.__events:
			raise ValueError("Invalid signal '%s' in PlotHandler." % name)
		self.__events[name].append(function)

	def set_data_getter(self, function):
		"""
		Sets the data getter function call. By default, the :code:`data_xy` attribute
		is polled, but this function provides the flexibility to alter this behaviour.

		Args:
			function:   The function to obtain the data. Takes the :code:`object` as an
						input and should return a 2xN array in the form of
						:code:`[[x1, x2, x3...], [y1, y2, y3...]]`.
		"""
		self.__get_data = function

	def get_data(self):
		"""
		Actually gets the data from the object.
		"""
		return self.__get_data(self.object)

	def get_animation(self):
		"""
		Returns the animation object, if MatPlotLib is used as a backend.
		"""
		return self.__ani

	def close_event(self):
		"""
		Closes the plot/figure associated with this handler.

		See Also:
			:func:`PlotHandler.close_event`
		"""
		if self.kind.is_backend("MPL", "SNS"):
			plt.close(self.figure[0])
		elif self.kind.is_backend("BOKEH"):
			# TODO
			pass
		# Make sure we close the plot if the backend fails to do so
		self.__close_event()

	def __close_event(self):
		"""
		Raises the `closed` signal.

		Note:
			This function is only used in the backend to prevent double
			closing of the plots.
		"""
		# print("closed")
		if self.__opened:
			self.__opened = False
			self.stop()
			self.signal('closed')

	def is_opened(self):
		"""
		Checks if the plots are still open.
		"""
		return self.__opened

	def update(self):
		"""
		Updates the plot information.
		"""
		data = self.get_data()
		if not self.__hidden:
			self.signal('preupdate', data)
			res = self.kind.update(self.elm, *data)
			if res is not None:
				self.elm = res
			self.signal('update', data)
		else:
			self.signal('preupdate', data)
			res = self.kind.update(self.elm, [], [])
			if res is not None:
				self.elm = res
			self.signal('update', data)

		if self.__finished:
			self._stop()

	def stop(self):
		"""
		Requests the process to finish polling.
		"""
		self.__finished = True

	def _stop(self):
		"""
		Stops polling for updates, but keeps the plot alive.
		"""
		if self.kind.is_backend("MPL", "SNS"):
			self.__ani.event_source.stop()
		elif self.kind.is_backend("BOKEH"):
			# TODO
			pass

	def terminate(self):
		"""
		Terminate the animation and close the plot.
		"""
		self._stop()
		self.close_event()

	def set_kind_args(self, *args, **kwargs):
		"""
		Sets the (keyword) arguments used for the plot.
		"""
		self.kind.args = args
		self.kind.kwargs = kwargs
		# TODO: pass the args on for the full plot.
		#  Redrawing only changes the x, y coords!

	def hide(self):
		"""
		Hides the plot in the view.

		See Also:
			- :func:`show`
			- :func:`toggle`
		"""
		self.__hidden = True

	def show(self):
		"""
		Shows the plot in the view.

		See Also:
			- :func:`hide`
			- :func:`toggle`
		"""
		self.__hidden = False

	def toggle(self):
		"""
		Toggles between hiding and showing the plot in the view.

		See Also:
			- :func:`hide`
			- :func:`show`
		"""
		self.__hidden = not self.__hidden


def follow(data, size=None, lower_bound=float('-inf'), upper_bound=float('inf'),
           lower_lim=0.0, upper_lim=0.0, perc_keep=0.5):
	"""
	Compute the new limits for the given dataset if the last data point must be followed.
	This is a convenience function for updating the plotting axes' limits. Whenever not
	enough data is available, the axis won't scale down.

	Args:
		data (list):            The data that must be plotted on the axes. Can be a
								shortened version of only the final n values (n > 0).
		size (float):           The total size of the axis to show, even if the data
								did not get there. When :code:`None`, the size is taken
								from the bounds or limits. Defaults to :code:`None`.
		lower_bound (float):    The lower bound of the axis, i.e. the minimal value to
								show, even if the data lies outside of this interval.
								Defaults to :code:`-float('inf')` (-infinity = no limit).
		upper_bound (float):    The upper bound of the axis, i.e. the maximal value to
								show, even if the data lies outside of this interval.
								Defaults to :code:`float('inf')` (infinity = no limit).
		lower_lim (float):      Lowest value to show, even if this value is not reached.
								While :attr:`lower_bound` bounds this point, so no smaller
								values can be shown, :attr:`lower_limit` makes it so this
								valus is always shown, but smaller values may still rescale
								beyond this point. Will be ignored when :attr:`size` is set.
								Defaults to :code:`0.0`.
		upper_lim (float):      Highest value to show, even if this value is not reached.
								While :attr:`upper_bound` bounds this point, so no higher
								values can be shown, :attr:`upper_limit` makes it so this
								valus is always shown, but larger values may still rescale
								beyond this point. Will be ignored when :attr:`size` is set.
								Defaults to :code:`0.0`.
		perc_keep (float):      The percentage at which the final data point must be
								shown. When :code:`0.0`, this point is the lowest value
								shown. When :code:`1.0`, this point is the highest value
								shown. Use this value to change how "centered" the last
								datapoint will be. When the axis grows in a positive
								direction, a value of :code:`0.0` will never show the
								data. When growing negatively, the opposite is true:
								the data will not be shown for a value of :code:`1.0`.
								Defaults to :code:`0.5` (= the middle).

	Danger:
		* The lower bound must be strictly smaller than the upper bound.
		* The size is :code:`None` and the bounds are infinity and the limits are equal.
		* The size cannot be larger than the distance between the bounds.
		* The lower limit must be smaller than (or equal to) the upper limit.

	Examples:
		- Follow a sine wave in the positive-x direction, always keeping a width of 10, in matplotlib.

		.. code-block:: python

			manager = PlotManager()
			manager.register("sin", cbd.findBlock("plot")[0], (fig, ax), LinePlot())
			manager.connect('sin', 'update', lambda d, a=ax: a.set_xlim(follow(d[0], 10.0, lower_bound=0.0)))

		- Follow a sine wave in the negative-x direction, always keeping a width of 10, in matplotlib.

		.. code-block:: python

			manager = PlotManager()
			manager.register("sin", cbd.findBlock("plot")[0], (fig, ax), LinePlot())
			manager.connect('sin', 'update', lambda d, a=ax: a.set_xlim(follow(d[0], 10.0, upper_bound=0.0)))

		- Follow a positive sine-wave, but keep a 10% margin from the right edge of the plot, in matplotlib.

		.. code-block:: python

			manager = PlotManager()
			manager.register("sin", cbd.findBlock("plot")[0], (fig, ax), LinePlot())
			manager.connect('sin', 'update', lambda d, a=ax: a.set_xlim(follow(d[0], 10.0, 0.0, perc_keep=0.9)))
	"""
	if lower_bound >= upper_bound:
		raise ValueError("Lower bound must be strictly smaller than the upper bound.")
	if size is None and (lower_bound == float('-inf') or upper_bound == float('inf')) and lower_lim == upper_lim:
		raise ValueError("When size is unset, the bounds may not be infinity and the limits may not equal each other.")
	if size is not None and upper_bound - lower_bound < size:
		raise ValueError("Invalid size: outside bounds.")
	if lower_lim > upper_lim:
		raise ValueError("Lower limit must be smaller than (or equal to) the upper limit.")

	if size is None:
		if len(data) == 0:
			return lower_lim, upper_lim
		return min(min(data), lower_lim), max(max(data), upper_lim)
	if upper_bound - lower_bound == size:
		return lower_bound, upper_bound
	if len(data) == 0:
		return -size / 2.0, size / 2.0
	value = data[-1]
	low = max(value - size * perc_keep, lower_bound)
	high = min(low + size, upper_bound)
	if high == upper_bound:
		low = high - size
	return low, high


class PlotKind:
	"""
	The kind of plot that needs to be rendered. This class is an abstract superclass.

	Not all backends provide the same functionality for plotting the same shape.
	This class provides a wrapper around all those functionalities to allow for users
	that they only need to change the figure information.

	Args:
		*args:      The arguments to add to the 'shape', excluding the data points.

					.. note::
						The :code:`seaborn` backend does not require any normal args.

		**kwargs:   The keyword arguments to add to the 'shape'.

	See Also:
		- :class:`LinePlot`
		- :class:`StepPlot`
		- :class:`ScatterPlot`
		- Information on the individual plot instances in the corresponding frameworks.
	"""
	def __init__(self, *args, **kwargs):
		self._backend = None
		self.args = args
		self.kwargs = kwargs

	def is_backend(self, *backends):
		"""
		Checks the backend for the plot.
		"""
		for back in backends:
			if Backend.compare(back, self._backend):
				return True
		return False

	def create(self, figure):
		"""
		Called when the figure is first created in the manager.

		Returns:
			The 'shape' information that is created on the plot.
		"""
		raise NotImplementedError()

	def update(self, element, *data):
		"""
		Called when the figure is updated in the manager.

		Args:
			element:    The shape that was returned from the :func:`PlotKind.create`
						method.
			*data:      The data with which to update the plot.
		"""
		raise NotImplementedError()


class LinePlot(PlotKind):
	"""
	Draws a line between all given coordinates.

	Backend Information:
		- **matplotlib:** :func:`matplotlib.axes.Axes.plot`
		- **bokeh:** :func:`bokeh.plotting.Figure.line`
		- **seaborn:** :func:`seaborn.lineplot`

	See Also:
		- :class:`PlotKind`
		- :class:`StepPlot`
		- :class:`ScatterPlot`
	"""
	def create(self, figure):
		if self.is_backend("MPL"):
			# matplotlib: figure[1] is the axis
			line, = figure[1].plot([], [], *self.args, **self.kwargs)
			return line
		elif self.is_backend("BOKEH"):
			return figure.line([], [], *self.args, **self.kwargs)
		elif self.is_backend("SNS"):
			# matplotlib: figure[1] is the axis
			a = sns.lineplot(x=[0], y=[0], ax=figure[1], **self.kwargs)
			return a.get_lines()[-1]

	def update(self, element, *data):
		if self.is_backend("MPL", "SNS"):
			element.set_data(data[0], data[1])
		elif self.is_backend("BOKEH"):
			element.data_source.data.update(x=data[0], y=data[1])


class StepPlot(PlotKind):
	"""
	Draws a stepped line between all given coordinates.
	See the corresponding backend information to change the step "levels".

	Backend Information:
		- **matplotlib:** :func:`matplotlib.axes.Axes.step`
		- **bokeh:** :func:`bokeh.plotting.Figure.step`
		- **seaborn:** :func:`seaborn.lineplot` with :code:`drawstyle='steps-pre'`

	See Also:
		- :class:`PlotKind`
		- :class:`LinePlot`
		- :class:`StepPlot`
	"""
	def create(self, figure):
		if self.is_backend("MPL"):
			# matplotlib: figure[1] is the axis
			line, = figure[1].step([], [], *self.args, **self.kwargs)
			return line
		elif self.is_backend("BOKEH"):
			return figure.step([], [], *self.args, **self.kwargs)
		elif self.is_backend("SNS"):
			# matplotlib: figure[1] is the axis
			a = sns.lineplot(x=[], y=[], ax=figure[1], drawstyle='steps-pre', **self.kwargs)
			return a.get_lines()[-1]

	def update(self, element, *data):
		if self.is_backend("MPL", "SNS"):
			element.set_data(data[0], data[1])
		elif self.is_backend("BOKEH"):
			element.data_source.data.update(x=data[0], y=data[1])


class ScatterPlot(PlotKind):
	"""
	Draws all given coordinates as points in a 2D space.
	See the corresponding backend information to change the shape of the indicators.

	Backend Information:
		- **matplotlib:** :func:`matplotlib.axes.Axes.scatter`
		- **bokeh:** :func:`bokeh.plotting.Figure.scatter`
		- **seaborn:** :func:`seaborn.scatterplot`

	See Also:
		- :class:`PlotKind`
		- :class:`LinePlot`
		- :class:`StepPlot`
	"""
	def create(self, figure):
		if self.is_backend("MPL"):
			# matplotlib: figure[1] is the axis
			pathc = figure[1].scatter([], [], *self.args, **self.kwargs)
			return pathc
		elif self.is_backend("BOKEH"):
			return figure.scatter([], [], *self.args, **self.kwargs)
		elif self.is_backend("SNS"):
			# matplotlib: figure[1] is the axis
			a = sns.scatterplot(x=[0], y=[0], ax=figure[1], **self.kwargs)
			return a.findobj(matplotlib.collections.PathCollection)[-1]


	def update(self, element, *data):
		if self.is_backend("MPL", "SNS"):
			element.set_offsets(list(zip(*data)))
		elif self.is_backend("BOKEH"):
			element.data_source.data.update(x=data[0], y=data[1])


class Arrow(PlotKind):
	"""
	Draws a vector at the given position

	Backend Information:
		- **matplotlib:** :func:`matplotlib.axes.Axes.scatter`
		- **bokeh:** Not available.
		- **seaborn:** Not available.
	"""
	def __init__(self, size, *args, **kwargs):
		PlotKind.__init__(self, *args, **kwargs)
		self.position = 0.0, 0.0
		self.size = size

	def create(self, figure):
		if self.is_backend("MPL"):
			# matplotlib: figure[1] is the axis
			x, y = self.position
			arrow = mplArrow(x, y, self.size, self.size, *self.args, **self.kwargs)
			line = figure[1].add_patch(arrow)
			line.set_zorder(20)
			return line
		elif self.is_backend("BOKEH", "SNS"):
			raise NotImplementedError("This feature is not (yet) available for this backend.")

	def update(self, element, *data):
		heading = data[1]
		x = math.cos(heading[-1]) * self.size
		y = math.sin(heading[-1]) * self.size

		if self.is_backend("MPL"):
			ax = element.axes
			element.remove()
			nx, ny = self.position
			arrow = mplArrow(nx, ny, x, y, *self.args, **self.kwargs)
			line = ax.add_patch(arrow)
			line.set_zorder(20)
			return line
		elif self.is_backend("BOKEH", "SNS"):
			raise NotImplementedError("This feature is not (yet) available for this backend.")


class PlotManager:
	"""
	Manages multiple :class:`PlotHandler` instances.
	Use this class as a centralized unit to manage one or more plots in the system.

	Args:
		backend (Backend):  The backend to use.

	Raises:
		AssertionError: if the backend cannot be located.

	See Also:
		- :class:`Backend`
		- :class:`PlotHandler`
	"""
	def __init__(self, backend=Backend.get("MPL")):
		assert Backend.exists(backend), "Invalid backend."
		self.__handlers = {}
		self.backend = backend

	def is_opened(self):
		"""
		Checks if there is at least a single plot left open.
		"""
		return any([h.is_opened() for h in self.__handlers.values()])

	def all_opened(self):
		"""
		Checks if all plots are still open.
		"""
		return all([h.is_opened() for h in self.__handlers.values()])

	def register(self, name, object, figure, kind, **kwargs):
		"""
		Register a single :class:`PlotHandler` to plot information.

		Args:
			name (str):         The name for this handler. Will be used to optionally
								access it afterwards.
			object (Any):       The object from which data needs to be polled. By default,
								the :code:`data_xy` attribute will be used on the object,
								which should result in a 2xN array in the form of
								:code:`([x1, x2, x3...], [y1, y2, y3...])`.
			figure:             The figure object required for plotting. This has been made
								externally to allow for full manipulation of the plotting
								framework itself.
			kind (PlotKind):    What needs to be plotted. See :class:`PlotKind` for more
								info.
			**kwargs:           Additional arguments to pass to the :class:`PlotHandler`
								constructor.

		See Also:
			:class:`PlotHandler`
		"""
		if name in self.__handlers:
			raise ValueError("PlotManager: PlotHandler '%s' already registered." % name)
		self.__handlers[name] = PlotHandler(object, figure, kind, self.backend, **kwargs)

	def unregister(self, name):
		"""
		Removes a single :class:`PlotHandler` from the manager.

		Args:
			name (str): The identifying name for the handler.
		"""
		h = self.get(name)
		h.close_event()
		del self.__handlers[name]

	def get(self, name):
		"""
		Obtains a single :class:`PlotHandler`, given its name.

		Args:
			name (str): The identifying name for the handler to obtain.
		"""
		if name not in self.__handlers:
			raise ValueError("PlotManager: No PlotHandler exists with name '%s'." % name)
		return self.__handlers[name]

	def connect(self, handler_name, event_name, function):
		"""
		Connect an event to a specific handler.

		Args:
			handler_name (str): The identifying name for the handler.
			event_name (str):   The name for the event.
			function:           A function to execute whenever the event is raised.

		See Also:
			- :func:`PlotHandler.connect`
			- :func:`PlotHandler.signal`
		"""
		self.get(handler_name).connect(event_name, function)

	def terminate(self):
		"""
		Forcibly terminate the manager and all its handlers.
		"""
		for handler in self.__handlers.values():
			handler.terminate()

	def stop(self):
		"""
		Stop polling for all updates.
		"""
		for handler in self.__handlers.values():
			handler.stop()


# TEMPORARY TEST OBJECT:
import math
class __Block:
	def __init__(self, method):
		self.x = []
		self.y = []
		self.int = 0.0
		self.method = method

	@property
	def data_xy(self):
		self.x.append(self.int)
		self.y.append(getattr(math, self.method)(self.int))
		self.int += 0.1
		return self.x, self.y



def mpl():
	"""
	Example usage for a matplotlib backend.
	"""
	import matplotlib.pyplot as plt
	fig = plt.figure(figsize=(5, 5), dpi=100)
	ax = fig.add_subplot(111)
	ax.set_ylim((-1, 1))

	manager = PlotManager()
	manager.register("sin", __Block('sin'), (fig, ax), ScatterPlot())
	manager.register("cos", __Block('cos'), (fig, ax), LinePlot(c='red'))
	manager.connect('sin', 'update', lambda d, axis=ax: axis.set_xlim(follow(d[0], 10.0, 0.0)))

	plt.show()


def bkh():
	"""
	Example usage for a bokeh backend.
	"""
	from bokeh.plotting import figure
	from bokeh.client import push_session

	fig = figure(plot_width=400, plot_height=400, y_range=(-1, 1))
	curdoc().add_root(fig)

	manager = PlotManager(Backend.BOKEH)
	manager.register("sin", __Block('sin'), fig, ScatterPlot())
	manager.register("cos", __Block('cos'), fig, LinePlot(color='red'))

	def set_xlim(limits):
		"""
		Bokeh-alternative of the :code:`mpl.Axes.set_xlim()` function call.
		"""
		lower, upper = limits
		fig.x_range.start = lower
		fig.x_range.end = upper
	manager.connect('sin', 'update', lambda d: set_xlim(follow(d[0], 10.0, 0.0)))

	session = push_session(curdoc())
	session.show()

	# TODO: disable flickering? (is this possible?)
	import time
	while manager.is_opened():
		session.push()
		time.sleep(0.1)


def sea():
	"""
	Example usage for a seaborn backend.
	"""
	sns.set_theme(style="darkgrid")

	import matplotlib.pyplot as plt
	fig = plt.figure(figsize=(5, 5), dpi=100)
	ax = fig.add_subplot(111)
	ax.set_ylim((-1, 1))

	manager = PlotManager(Backend.SNS)
	manager.register("sin", __Block('sin'), (fig, ax), ScatterPlot())
	manager.register("cos", __Block('cos'), (fig, ax), LinePlot(color='red'))
	manager.connect('sin', 'update', lambda d, axis=ax: axis.set_xlim(follow(d[0], 10.0, 0.0)))

	plt.show()


if __name__ == '__main__':
	bkh()
