|
|
@@ -7,8 +7,17 @@ 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
|
|
|
|
|
|
@@ -18,8 +27,8 @@ try:
|
|
|
except ImportError:
|
|
|
_BOKEH_FOUND = False
|
|
|
|
|
|
-# TODO: Bokeh (see TODOs), GGplot, Seaborn
|
|
|
-# Note: Seaborn is built on top of matplotlib
|
|
|
+# TODO: Bokeh (see TODOs), GGplot
|
|
|
+# TODO: More Plot Kinds
|
|
|
|
|
|
class Backend:
|
|
|
"""
|
|
|
@@ -49,9 +58,15 @@ class Backend:
|
|
|
|
|
|
|
|
|
|
Attention:
|
|
|
- This backend is still in beta.
|
|
|
+ This backend is still in active development. Some features may not work as expected.
|
|
|
"""
|
|
|
|
|
|
+ if _SNS_FOUND:
|
|
|
+ SNS = 3
|
|
|
+ """ : : Use `Seaborn <https://seaborn.pydata.org/>`_."""
|
|
|
+ SEABORN = 3
|
|
|
+ """ : : Use `Seaborn <https://seaborn.pydata.org/>`_."""
|
|
|
+
|
|
|
@staticmethod
|
|
|
def exists(value):
|
|
|
"""
|
|
|
@@ -59,6 +74,26 @@ class Backend:
|
|
|
"""
|
|
|
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:
|
|
|
@@ -81,7 +116,7 @@ class PlotHandler:
|
|
|
:class:`PlotManager`.
|
|
|
|
|
|
Warning:
|
|
|
- Bokeh is still in beta. Not all features will work as required.
|
|
|
+ Bokeh is still in active development. Not all features will work as required.
|
|
|
|
|
|
Raises:
|
|
|
AssertionError: if the backend cannot be located.
|
|
|
@@ -133,11 +168,11 @@ class PlotHandler:
|
|
|
}
|
|
|
|
|
|
# backend info:
|
|
|
- if backend == Backend.MPL:
|
|
|
+ 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 == Backend.BOKEH:
|
|
|
+ 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())
|
|
|
@@ -232,9 +267,9 @@ class PlotHandler:
|
|
|
See Also:
|
|
|
:func:`PlotHandler.close_event`
|
|
|
"""
|
|
|
- if self.kind.is_backend(Backend.MPL):
|
|
|
+ if self.kind.is_backend("MPL", "SNS"):
|
|
|
plt.close(self.figure[0])
|
|
|
- elif self.kind.is_backend(Backend.BOKEH):
|
|
|
+ elif self.kind.is_backend("BOKEH"):
|
|
|
# TODO
|
|
|
pass
|
|
|
# Make sure we close the plot if the backend fails to do so
|
|
|
@@ -283,7 +318,7 @@ class PlotHandler:
|
|
|
"""
|
|
|
Stops polling for updates, but keeps the plot alive.
|
|
|
"""
|
|
|
- if self.kind.is_backend(Backend.MPL):
|
|
|
+ if self.kind.is_backend(Backend.MPL, Backend.SNS):
|
|
|
self.__ani.event_source.stop()
|
|
|
elif self.kind.is_backend(Backend.BOKEH):
|
|
|
# TODO
|
|
|
@@ -404,6 +439,10 @@ class PlotKind:
|
|
|
|
|
|
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:
|
|
|
@@ -417,11 +456,14 @@ class PlotKind:
|
|
|
self.args = args
|
|
|
self.kwargs = kwargs
|
|
|
|
|
|
- def is_backend(self, backend):
|
|
|
+ def is_backend(self, *backends):
|
|
|
"""
|
|
|
Checks the backend for the plot.
|
|
|
"""
|
|
|
- return self._backend == backend
|
|
|
+ for back in backends:
|
|
|
+ if Backend.compare(back, self._backend):
|
|
|
+ return True
|
|
|
+ return False
|
|
|
|
|
|
def create(self, figure):
|
|
|
"""
|
|
|
@@ -451,6 +493,7 @@ class LinePlot(PlotKind):
|
|
|
Backend Information:
|
|
|
- **matplotlib:** :func:`matplotlib.axes.Axes.plot`
|
|
|
- **bokeh:** :func:`bokeh.plotting.Figure.line`
|
|
|
+ - **seaborn:** :func:`seaborn.lineplot`
|
|
|
|
|
|
See Also:
|
|
|
- :class:`PlotKind`
|
|
|
@@ -458,17 +501,21 @@ class LinePlot(PlotKind):
|
|
|
- :class:`ScatterPlot`
|
|
|
"""
|
|
|
def create(self, figure):
|
|
|
- if self.is_backend(Backend.MPL):
|
|
|
+ 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(Backend.BOKEH):
|
|
|
+ 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(Backend.MPL):
|
|
|
+ if self.is_backend("MPL", "SNS"):
|
|
|
element.set_data(data[0], data[1])
|
|
|
- elif self.is_backend(Backend.BOKEH):
|
|
|
+ elif self.is_backend("BOKEH"):
|
|
|
element.data_source.data.update(x=data[0], y=data[1])
|
|
|
|
|
|
|
|
|
@@ -480,6 +527,7 @@ class StepPlot(PlotKind):
|
|
|
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`
|
|
|
@@ -487,17 +535,21 @@ class StepPlot(PlotKind):
|
|
|
- :class:`StepPlot`
|
|
|
"""
|
|
|
def create(self, figure):
|
|
|
- if self.is_backend(Backend.MPL):
|
|
|
+ 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(Backend.BOKEH):
|
|
|
+ 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(Backend.MPL):
|
|
|
+ if self.is_backend("MPL", "SNS"):
|
|
|
element.set_data(data[0], data[1])
|
|
|
- elif self.is_backend(Backend.BOKEH):
|
|
|
+ elif self.is_backend("BOKEH"):
|
|
|
element.data_source.data.update(x=data[0], y=data[1])
|
|
|
|
|
|
|
|
|
@@ -509,6 +561,7 @@ class ScatterPlot(PlotKind):
|
|
|
Backend Information:
|
|
|
- **matplotlib:** :func:`matplotlib.axes.Axes.scatter`
|
|
|
- **bokeh:** :func:`bokeh.plotting.Figure.scatter`
|
|
|
+ - **seaborn:** :func:`seaborn.scatterplot`
|
|
|
|
|
|
See Also:
|
|
|
- :class:`PlotKind`
|
|
|
@@ -516,50 +569,63 @@ class ScatterPlot(PlotKind):
|
|
|
- :class:`StepPlot`
|
|
|
"""
|
|
|
def create(self, figure):
|
|
|
- if self.is_backend(Backend.MPL):
|
|
|
+ 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(Backend.BOKEH):
|
|
|
+ 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(Backend.MPL):
|
|
|
+ if self.is_backend("MPL", "SNS"):
|
|
|
element.set_offsets(list(zip(*data)))
|
|
|
- elif self.is_backend(Backend.BOKEH):
|
|
|
+ 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(Backend.MPL):
|
|
|
+ if self.is_backend("MPL"):
|
|
|
# matplotlib: figure[1] is the axis
|
|
|
arrow = mplArrow(*self.position, self.size, self.size, *self.args, **self.kwargs)
|
|
|
line = figure[1].add_patch(arrow)
|
|
|
line.set_zorder(20)
|
|
|
return line
|
|
|
- elif self.is_backend(Backend.BOKEH):
|
|
|
- raise NotImplementedError("This feature is not yet available.")
|
|
|
+ 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(Backend.MPL):
|
|
|
+ if self.is_backend("MPL"):
|
|
|
ax = element.axes
|
|
|
element.remove()
|
|
|
arrow = mplArrow(*self.position, x, y, *self.args, **self.kwargs)
|
|
|
line = ax.add_patch(arrow)
|
|
|
line.set_zorder(20)
|
|
|
return line
|
|
|
- elif self.is_backend(Backend.BOKEH):
|
|
|
- raise NotImplementedError("This feature is not yet available.")
|
|
|
+ elif self.is_backend("BOKEH", "SNS"):
|
|
|
+ raise NotImplementedError("This feature is not (yet) available for this backend.")
|
|
|
|
|
|
|
|
|
class PlotManager:
|
|
|
@@ -577,7 +643,7 @@ class PlotManager:
|
|
|
- :class:`Backend`
|
|
|
- :class:`PlotHandler`
|
|
|
"""
|
|
|
- def __init__(self, backend=Backend.MPL):
|
|
|
+ def __init__(self, backend=Backend.get("MPL")):
|
|
|
assert Backend.exists(backend), "Invalid backend."
|
|
|
self.__handlers = {}
|
|
|
self.backend = backend
|
|
|
@@ -702,7 +768,7 @@ def mpl():
|
|
|
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(PlotHandler.follow(d[0], 10.0, 0.0)))
|
|
|
+ manager.connect('sin', 'update', lambda d, axis=ax: axis.set_xlim(follow(d[0], 10.0, 0.0)))
|
|
|
|
|
|
plt.show()
|
|
|
|
|
|
@@ -725,7 +791,7 @@ def bkh():
|
|
|
lower, upper = limits
|
|
|
fig.x_range.start = lower
|
|
|
fig.x_range.end = upper
|
|
|
- manager.connect('sin', 'update', lambda d: set_xlim(PlotHandler.follow(d[0], 10.0, 0.0)))
|
|
|
+ manager.connect('sin', 'update', lambda d: set_xlim(follow(d[0], 10.0, 0.0)))
|
|
|
|
|
|
session = push_session(curdoc())
|
|
|
session.show()
|
|
|
@@ -737,5 +803,24 @@ def bkh():
|
|
|
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()
|
|
|
+ sea()
|