crappy.blocks.grapher — Crappy 1.5.3 documentation

# Source code for crappy.blocks.grapher

    # coding: utf-8

    import numpy as np

    from .block import Block
    from .._global import OptionalModule

    try:
      import matplotlib.pyplot as plt
      from matplotlib.widgets import Button
    except (ModuleNotFoundError, ImportError):
      plt = OptionalModule("matplotlib")
      Button = OptionalModule("matplotlib")

    class Grapher(Block):
      """The grapher receive data from a block (via a :ref:`Link`) and plots it."""

      def __init__(self,
                   *labels,
                   length=0,
                   freq=2,
                   maxpt=20000,
                   window_size=(8, 8),
                   window_pos=None,
                   interp=True,
                   backend="TkAgg"):
        """Sets the args and initializes the parent class.

        Args:
          *labels (:obj:`tuple`): Tuples of the columns labels of input data for
            plotting. You can add as much as you want, depending on your
            performances. The first value is the `x` label, the second is the `y`
            label.
          length (:obj:`int`, optional): If `0` the graph is static and displays
            all data from the start of the assay. Else only displays the last
            ``length`` received chunks, and drops the previous ones.
          freq (:obj:`float`, optional): The refresh rate of the graph. May cause
            high memory consumption if set too high.
          maxpt (:obj:`int`, optional): The maximum number of points displayed on
            the graph. When reaching this limit, the block deletes one point out of
            two but this is almost invisible to the user.
          window_size (:obj:`tuple`, optional): The size of the graph, in inches.
          window_pos (:obj:`tuple`, optional): The position of the graph in pixels.
            The first value is for the `x` direction, the second for the `y`
            direction. The origin is the top left corner. Works with multiple
            screens.
          interp (:obj:`bool`, optional): If :obj:`True`, the points of data will
            be linked to the following by straight lines. Else, each value wil be
            displayed as constant until the next update.
          backend (:obj:`int`, optional): The :mod:`matplotlib` backend to use.

        Example:
          ::

            graph = Grapher(('t(s)', 'F(N)'), ('t(s)', 'def(%)'))

          will plot a dynamic graph with two lines plot (`F=f(t)` and `def=f(t)`).
          ::

            graph = Grapher(('def(%)', 'F(N)'), length=0)

          will plot a static graph.
          ::

            graph = Grapher(('t(s)', 'F(N)'), length=30)

          will plot a dynamic graph displaying the last 30 chunks of data.
        """

        Block.__init__(self)
        self.niceness = 10
        self.length = length
        self.freq = freq
        self.maxpt = maxpt
        self.window_size = window_size
        self.window_pos = window_pos
        self.interp = interp
        self.backend = backend

        self.labels = labels

      def prepare(self):
        if self.backend:
          plt.switch_backend(self.backend)
        self.f = plt.figure(figsize=self.window_size)
        self.ax = self.f.add_subplot(111)
        self.lines = []
        for _ in self.labels:
          if self.interp:
            self.lines.append(self.ax.plot([], [])[0])
          else:
            self.lines.append(self.ax.step([], [])[0])
        # Keep only 1/factor points on each line
        self.factor = [1 for _ in self.labels]
        # Count to drop exactly 1/factor points, no more and no less
        self.counter = [0 for _ in self.labels]
        legend = [y for x, y in self.labels]
        plt.legend(legend, bbox_to_anchor=(-0.03, 1.02, 1.06, .102), loc=3,
                   ncol=len(legend), mode="expand", borderaxespad=1)
        plt.xlabel(self.labels[0][0])
        plt.ylabel(self.labels[0][1])
        plt.grid()
        self.axclear = plt.axes([.8, .02, .15, .05])
        self.bclear = Button(self.axclear, 'Clear')
        self.bclear.on_clicked(self.clear)

        if self.window_pos:
          mng = plt.get_current_fig_manager()
          mng.window.wm_geometry("+%s+%s" % self.window_pos)
        plt.draw()
        plt.pause(.001)

      def clear(self, event=None):
        for line in self.lines:
          line.set_xdata([])
          line.set_ydata([])
        self.factor = [1 for _ in self.labels]
        self.counter = [0 for _ in self.labels]

      def loop(self):
        # We need to recv data from all the links, but keep
        # ALL of the data, even with the same label (so not get_all_last)
        data = self.recv_all_delay()
        for i, (lx, ly) in enumerate(self.labels):
          x, y = 0, 0  # So that if we don't find it, we do nothing
          for d in data:
            if lx in d and ly in d:  # Find the first input with both labels
              dx = d[lx][self.factor[i]-self.counter[i]-1::self.factor[i]]
              dy = d[ly][self.factor[i]-self.counter[i]-1::self.factor[i]]
              self.counter[i] = (self.counter[i]+len(d[lx])) % self.factor[i]
              x = np.hstack((self.lines[i].get_xdata(), dx))
              y = np.hstack((self.lines[i].get_ydata(), dy))
              break
          if isinstance(x, int):
            break
          if self.length and len(x) >= self.length:
            # Remove the beginning if the graph is dynamic
            x = x[-self.length:]
            y = y[-self.length:]
          elif len(x) > self.maxpt:
            # Reduce the number of points if we have to many to display
            print("[Grapher] Too many points on the graph {} ({}>{})".format(
              i, len(x), self.maxpt))
            x, y = x[::2], y[::2]
            self.factor[i] *= 2
            print("[Grapher] Resampling factor is now {}".format(self.factor[i]))
          self.lines[i].set_xdata(x)
          self.lines[i].set_ydata(y)
        self.ax.relim()  # Update the window
        self.ax.autoscale_view(True, True, True)
        self.f.canvas.draw()  # Update the graph
        self.f.canvas.flush_events()

      def finish(self):
        plt.close("all")