def __init__(self, f=None, reader=None, interactive=True, toolbar=True, *args, **kwargs): # Read in data if f is not None: if reader is None: self.reader = DefaultReader(f, *args, **kwargs) else: self.reader = reader(f, *args, **kwargs) self.data = self.reader.init_data(*args, **kwargs) self._initialized = True else: self.reader = None self._initialized = False # `interactive` determines if the MPL event loop is used or a # raw figure is made. Set to False if using an external event handler, # e.g. if embedding in a separate program. self.interactive = interactive # Static version is not threaded, but want to make sure any subclasses # are thread-safe self.lock = threading.RLock() # Need to keep track of the backend, since not all backends support # all update schemes self._backend = plt.get_backend().lower() self.fig = self._create_fig(toolbar=toolbar) self.axes = None self.canvas = self.fig.canvas self.mode = 'none' self._plotdict = {'autoscalex': True, # Autoscale is meaningless in 'autoscaley': True, # Static, but useful in RT 'windowsize': None}
def switch_file(self, newfile, reader=None, *args, **kwargs): """ Switch the file that is used for plotting. If no reader is specified, attempts to use pre-existing reader. If there is no pre-existing reader, creates a DefaultReader and uses that. """ self.clear() if reader is not None: try: self.reader.close() except AttributeError: pass self.reader = reader(newfile, *args, **kwargs) if not self.reader: self.reader = DefaultReader(newfile, *args, **kwargs) self.data = self.reader.switch_file(newfile, *args, **kwargs) self._initialized = True try: return self._plot_from_dict() except ValueError: self.redraw() return
def __init__(self, reader=None, crucial=True, state_notifier=None, reactor=None, logger=None): super(Channel, self).__init__() self._state_notifier = state_notifier # handler = handler or ChannelHandler(self) self._reactor = reactor or AsyncReactor() self._logger = logger or EmptyLogger() self._state = ChannelState.IDLE self._crucial = crucial if reader is not None: reader.set_channel(self) else: reader = DefaultReader(1, self) self._reader = reader self._buffered = ""
class PyOscopeStatic(object): """ Object for plotting static data sets. Requires the file `f` to be specified. The `reader` may be a customized file reading class. It should not be instantiated before being passed into PyOscopeStatic. The `interactive` flag indicates whether or not the built-in matplotlib event handler loop should be used. You do want the built-in MPL loop to be active if you are using PyOscopeStatic interactively, but it must not be active if the plot is to be embedded in an external application, which will be running its own event handling loop (the two loops would collide). Set to True (default) to use the built-in MPL loop, otherwise False. The desired reader may be specified. The remaining arguments are passed to the reader. See reader.ReaderInterface for reader implementation notes. Example usage: >>> pos = PyOscopeStatic(f='testdata.txt') >>> pos.data.columns >>> pos.plot('second', ['first', 'third'], legend=True) """ def __init__(self, f=None, reader=None, interactive=True, toolbar=True, *args, **kwargs): # Read in data if f is not None: if reader is None: self.reader = DefaultReader(f, *args, **kwargs) else: self.reader = reader(f, *args, **kwargs) self.data = self.reader.init_data(*args, **kwargs) self._initialized = True else: self.reader = None self._initialized = False # `interactive` determines if the MPL event loop is used or a # raw figure is made. Set to False if using an external event handler, # e.g. if embedding in a separate program. self.interactive = interactive # Static version is not threaded, but want to make sure any subclasses # are thread-safe self.lock = threading.RLock() # Need to keep track of the backend, since not all backends support # all update schemes self._backend = plt.get_backend().lower() self.fig = self._create_fig(toolbar=toolbar) self.axes = None self.canvas = self.fig.canvas self.mode = 'none' self._plotdict = {'autoscalex': True, # Autoscale is meaningless in 'autoscaley': True, # Static, but useful in RT 'windowsize': None} @synchronized('lock') def switch_file(self, newfile, reader=None, *args, **kwargs): """ Switch the file that is used for plotting. If no reader is specified, attempts to use pre-existing reader. If there is no pre-existing reader, creates a DefaultReader and uses that. """ self.clear() if reader is not None: try: self.reader.close() except AttributeError: pass self.reader = reader(newfile, *args, **kwargs) if not self.reader: self.reader = DefaultReader(newfile, *args, **kwargs) self.data = self.reader.switch_file(newfile, *args, **kwargs) self._initialized = True try: return self._plot_from_dict() except ValueError: self.redraw() return @synchronized('lock') def _create_fig(self, plotsize=(6., 4.), dpi=100, tight=True, toolbar=True): # autolayout: Automatically call tight_layout() on newly created figure # toolbar: Whether or not to create toolbar attached to plot # Use context manager to prevent global settings from changing tb = 'toolbar2' if toolbar else 'None' rcdict = {'figure.autolayout': bool(tight), 'toolbar': tb} with mpl.rc_context(rc=rcdict): if self.interactive: # pyplot's figure() function creates Figure object and hooks it # into MPL event loop figname = self.__class__.__name__ + '-' + hex(id(self)) fig = plt.figure(figname, figsize=plotsize, dpi=dpi) else: # mpl.figure.Figure is a raw Figure object fig = Figure(plotsize, dpi=dpi) # Both cases return the raw Figure object return fig @synchronized('lock') def _create_axes(self, nrows=1, ncols=1, sharex=False, sharey=False, subplot_kw=None, fig=None): """ Create grid of axes. Slightly modified version of matplotlib.pyplot.subplots. See subplots documentation for most arguments. `fig` is the figure object in which the axes objects should be created. If it is None, uses self.fig. """ if isinstance(sharex, bool): if sharex: sharex = "all" else: sharex = "none" if isinstance(sharey, bool): if sharey: sharey = "all" else: sharey = "none" share_values = ["all", "row", "col", "none"] if sharex not in share_values: raise ValueError("sharex [%s] must be one of %s" % (sharex, share_values)) if sharey not in share_values: raise ValueError("sharey [%s] must be one of %s" % (sharey, share_values)) if subplot_kw is None: subplot_kw = {} if fig is None: fig = self.fig fig.clear() # Create empty object array to hold all axes. It's easiest to make it # 1-d so we can just append subplots upon creation, and then nplots = nrows*ncols axarr = np.empty(nplots, dtype=object) # Create first subplot separately, so we can share it if requested ax0 = fig.add_subplot(nrows, ncols, 1, **subplot_kw) #if sharex: # subplot_kw['sharex'] = ax0 #if sharey: # subplot_kw['sharey'] = ax0 axarr[0] = ax0 r, c = np.mgrid[:nrows, :ncols] r = r.flatten() * ncols c = c.flatten() lookup = {"none": np.arange(nplots), "all": np.zeros(nplots, dtype=int), "row": r, "col": c, } sxs = lookup[sharex] sys = lookup[sharey] # Note off-by-one counting because add_subplot uses the MATLAB 1-based # convention. for i in range(1, nplots): if sxs[i] == i: subplot_kw['sharex'] = None else: subplot_kw['sharex'] = axarr[sxs[i]] if sys[i] == i: subplot_kw['sharey'] = None else: subplot_kw['sharey'] = axarr[sys[i]] axarr[i] = fig.add_subplot(nrows, ncols, i + 1, **subplot_kw) # returned axis array will be always 2-d, even if nrows=ncols=1 axarr = axarr.reshape(nrows, ncols) # turn off redundant tick labeling if sharex in ["col", "all"] and nrows > 1: #if sharex and nrows>1: # turn off all but the bottom row for ax in axarr[:-1, :].flat: for label in ax.get_xticklabels(): label.set_visible(False) ax.xaxis.offsetText.set_visible(False) if sharey in ["row", "all"] and ncols > 1: #if sharey and ncols>1: # turn off all but the first column for ax in axarr[:, 1:].flat: for label in ax.get_yticklabels(): label.set_visible(False) ax.yaxis.offsetText.set_visible(False) ret = axarr.reshape(nrows, ncols) return ret @synchronized('lock') def redraw(self): if self.interactive: self.canvas.draw_idle() @synchronized('lock') def plot(self, xs=None, ys=None, splitx=True, splity=True, sharex='col', sharey=False, xtrans=None, ytrans=None, legend=False, xlabels=None, ylabels=None, labels=None, *args, **kwargs): """ Make plot. Specified by `xs` and `ys`, which specify which column of data should go on each axis. Each of `xs` and/or `ys` may be either a single identifier or a list of identifiers. Identifiers include the column name as a string (e.g. "col1"), the integer column index (eg. 0), or the column data itself in either pandas.Series (1-dimensional) or numpy.ndarray (1-dimensional) types. Note that if a Series or ndarray object are passed, the plotter will use that exact object. Any errors (e.g. mismatched length) are then the responsibility of the user. Note that custom Series or ndarray objects must be included in a list, or else they will be treated as a list of identifiers themselves, i.e. pass in `xs=[myarray]` instead of `xs=myarray`. If `splitx` is True, then each identifier in `xs` will generate its own column of axes, i.e. there will be `len(xs)` columns of axes and each axes object will have only 1 x identifier on it. If it is False, then lines for each x identifier will be generated on a single plot, i.e. there will be only 1 column of axes and each plot in it will have at least `len(xs)` lines in it. If `splitx` is True, then each identifier in `ys` will generate its own row of axes, i.e. there will be `len(ys)` rows of axes and each axes object will have only 1 y identifier on it. If it is False, then lines for each y identifier will be generated on a single plot, i.e. there will only be 1 row of axes and each plot in it will have at least `len(ys)` lines in it. If both `splitx` and `splity` are True, then there will be `len(xs)` columns and `len(ys)` rows of axes and each will have 1 line. If both `splitx` and `splity` are False, then there will be 1 axes and it will have `len(xs)*len(ys)` lines. If either `xs` or `ys` is None, then plots of the data versus their index are created. The index is always on the x axis. If both `xs` and `ys` are None, then all of the possible data sets are plotted against their indices. `xtrans` and `ytrans` are transformation functions for the x and y data, respectively. Their structure must match the structure of `xs` and `ys`. `legend` indicates whether to show the legend and where it should be shown, if not False. If False, no legends are made. If True, the legend is plotted in the top right. A string value may be passed to `legend` indicating where it should be plotted, matching the pyplot.legend()'s `loc` keyword argument. The legend location is shared for all axes. `xlabels` and `ylabels` are display-only labels for the `xs` and `ys`. They are displayed in place of the file-defined labels in legends. They must match the shape of their corresponding data identifiers, `xs` or `ys`. If set to None, then the column labels pulled from the file or assigned by default are used. `labels` are display-only labels that override the programmatically generated labels. No pre-formatting is done on this argument, so it must be specified correctly. For 1D arrays of plots (i.e. only `xs` or `ys` is specified), then `labels` must be a 1D list of matching length. For 2D arrays of plots (i.e. both `xs` and `ys` are specified), then `labels` must be a 2D array of matching length, where `labels[i, j]` corresponds to (`xs[i]`, `ys[j]`). """ if not self._initialized: return # Argument checking # oneD flag indicates if x axis will be indices if (xs is None) and (ys is None): xs = [name for name in self.data.columns] if (ys is None): ys = xs ytrans = xtrans oneD = True elif (xs is None): oneD = True else: oneD = False if ((not isinstance(xs, Iterable)) or isinstance(xs, StringTypes)): xs = [xs] if ((not isinstance(xlabels, Iterable)) or isinstance(xlabels, StringTypes)): xlabels = [xlabels]*len(xs) if xtrans is None: xtrans = [None]*len(xs) elif not isinstance(xtrans, Iterable): xtrans = [xtrans]*len(xs) if ((not isinstance(ys, Iterable)) or isinstance(ys, StringTypes)): ys = [ys] if ((not isinstance(ylabels, Iterable)) or isinstance(ylabels, StringTypes)): ylabels = [ylabels]*len(ys) if ytrans is None: ytrans = [None]*len(ys) elif not isinstance(ytrans, Iterable): ytrans = [ytrans]*len(ys) if not legend: legendflag = False legendloc = None else: legendflag = True if isinstance(legend, StringTypes): legendloc = legend else: legendloc = None # Find data series to be plotted if not oneD: newxs = [] xnames = [] for i, x in enumerate(xs): if isinstance(x, StringTypes): newx = self.data[x] xname = x elif isinstance(x, int): xname = self.data.columns[x] newx = self.data[xname] elif isinstance(x, Iterable): newx = x xname = 'x_{i}'.format(i=i) elif isinstance(x, NoneType): xname = None temp = self.data.columns[0] newx = range(len(self.data[temp])) xnames.append(xname) newxs.append(newx) else: newxs = None xnames = None newys = [] ynames = [] for j, y in enumerate(ys): if isinstance(y, StringTypes): newy = self.data[y] yname = y elif isinstance(y, (int, np.integer)): yname = self.data.columns[y] newy = self.data[yname] elif isinstance(y, Iterable): newy = y yname = 'y_{j}'.format(j=j) elif isinstance(y, NoneType): yname = None temp = self.data.columns[0] newy = range(len(self.data[temp])) ynames.append(yname) newys.append(newy) # Store these so we don't have to look them up again self._plotdict['xs'] = newxs self._plotdict['ys'] = newys self._plotdict['xnames'] = xnames self._plotdict['ynames'] = ynames self._plotdict['xlabels'] = xlabels self._plotdict['ylabels'] = ylabels self._plotdict['xtrans'] = xtrans self._plotdict['ytrans'] = ytrans self._plotdict['label'] = labels self._plotdict['splitx'] = splitx self._plotdict['splity'] = splity self._plotdict['sharex'] = sharex self._plotdict['sharey'] = sharey self._plotdict['oneD'] = oneD self._plotdict['legendflag'] = legendflag self._plotdict['legendloc'] = legendloc # Abort if nothing to plot along either axis ly = len(ynames) lx = 1 if isinstance(xnames, type(None)) else len(xnames) if lx*ly == 0: self.clear() self.lines = [] if self.interactive: self.fig.show() self.redraw() return self.lines # Create axes for plotting if not oneD: numxs = len(xs) lenx = numxs if splitx else 1 else: numxs = 1 lenx = 1 numys = len(ys) leny = numys if splity else 1 self.axes = self._create_axes(leny, lenx, sharex=sharex, sharey=sharey) # Make plots in appropriate axes self.mode = 'plot' self.lines = np.empty([numxs, numys], dtype='object') if oneD: for j, y in enumerate(newys): yname = ynames[j] ylbl = ylabels[j] ylbl = yname if (ylbl is None) else ylbl ytran = ytrans[j] label = None if (labels is None) else labels[j] rownum = j if splity else 0 ax = self.axes[rownum, 0] line = self._plotyt(ax, y, ylbl, transform=ytran, label=label, *args, **kwargs) self.lines[0, j] = line if legendflag: ax.legend(loc=legendloc) else: for i, x in enumerate(newxs): for j, y in enumerate(newys): xname = xnames[i] xlbl = xlabels[i] xlbl = xname if (xlbl is None) else xlbl yname = ynames[j] ylbl = ylabels[j] ylbl = yname if (ylbl is None) else ylbl xtran = xtrans[i] ytran = ytrans[j] label = None if (labels is None) else labels[i][j] rownum = j if splity else 0 colnum = i if splitx else 0 ax = self.axes[rownum, colnum] line = self._plotxy(ax, x, y, xlbl, ylbl, xtrans=xtran, ytrans=ytran, label=label, *args, **kwargs) self.lines[i, j] = line if legendflag: ax.legend(loc=legendloc) if self.interactive: self.fig.show() self.redraw() return self.lines @synchronized('lock') def _plot_from_dict(self, pdict=None): if not self._initialized: return if pdict is None: pdict = self._plotdict try: xnames = pdict['xnames'] ynames = pdict['ynames'] xlabels = pdict['xlabels'] ylabels = pdict['ylabels'] labels = pdict['labels'] legendflag = pdict['legendflag'] legendloc = pdict['legendloc'] splitx = pdict['splitx'] splity = pdict['splity'] sharex = pdict['sharex'] sharey = pdict['sharey'] xtrans = pdict['xtrans'] ytrans = pdict['ytrans'] except KeyError: raise ValueError("Invalid plot dictionary specified:" " {0}".format(repr(pdict))) nameflag = True names = xnames + ynames if xnames else ynames for name in names: flag = (name in self.data.columns) nameflag = (nameflag and flag) if not nameflag: raise ValueError("One or more data names not available!") if legendloc: legend = legendloc if legendflag else None else: legend = legendflag return self.plot(xnames, ynames, splitx, splity, sharex, sharey, xtrans, ytrans, legend, xlabels, ylabels, labels) @synchronized('lock') def _plotyt(self, ax, y, yname, windowsize=None, transform=None, label=None, *args, **kwargs): """ Plot a data set versus its indices on the specified axes. `yname` is the label given to the line. `windowsize` specifies the number of data points to plot. Counts from the end. None (default) specifies all. `windowsize`s that exceed the length of the data set will use the full data set. `transform` is a function that is applied to the data set before it is plotted. It must be a vectorized function, i.e. it must accept as its only argument the full data set and return the full transformed data set. `label` overrides the programmatically generated label for the line. Since the label for this line is directly set to `yname` in this case, this function is redundant. The `label` argument is included to keep its notation similar to _plotxy. The remaining arguments are passed to `ax.plot`. Returns the line object that is created. """ if windowsize is None: ws = len(y) else: ws = min(len(y), windowsize) self._plotdict['windowsize'] = windowsize if transform is None: transform = lambda x: x # Identity function if yname is None: yname = 'index' plabel = yname if (label is None) else label y = y[-ws:] y = transform(y) line, = ax.plot(y, label=plabel, *args, **kwargs) return line @synchronized('lock') def _plotxy(self, ax, x, y, xname, yname, windowsize=None, xtrans=None, ytrans=None, label=None, *args, **kwargs): """ Plot two data sets against each other on the specified axes. `xname` and `yname` are combined to create a label for the plot. This label may be overridden by the `label` argument. `windowsize` specifies the number of data points to plot. Counts from the end. None (default) specifies all. `windowsize`s that exceed the length of the data set will use the full data set. `xtrans` and `ytrans` are functions that are applied to the data sets before they are plotted. They must be vectorized functions, i.e. each must accept as its only argument a full data set and return the full transformed data set. `xtrans` modifies the x data set and `ytrans` modifies the y data set. `label` overrides the programmatically generated label for the line. If `label` is not None, then xname and yname are ignored. The remaining arguments are passed to `ax.plot`. Returns the line object that is created. """ if len(x) != len(y): raise ValueError("x and y values must have same length!") if windowsize is None: ws = len(y) else: ws = min(len(y), windowsize) self._plotdict['windowsize'] = windowsize if xtrans is None: xtrans = lambda x: x # Identity function if ytrans is None: ytrans = lambda x: x # Identity function if xname is None: xname = 'index' if yname is None: yname = 'index' x = x[-ws:] x = xtrans(x) y = y[-ws:] y = ytrans(y) plabel = "{x} (x) vs {y} (y)".format(x=xname, y=yname) plabel = plabel if (label is None) else label line, = ax.plot(x, y, label=plabel, *args, **kwargs) return line @synchronized('lock') def clear(self): """ Clears current plot. """ self.fig.clear() self.mode = 'none' @synchronized('lock') def autoscale_axes(self): xflag = self._plotdict['autoscalex'] yflag = self._plotdict['autoscaley'] if (not xflag) and (not yflag): return for ax in self.axes.flatten(): xminax, xmaxax, yminax, ymaxax = ax.axis() dxax = xmaxax - xminax dyax = ymaxax - yminax xmidax = xminax + dxax/2. ymidax = yminax + dyax/2. xmins = [] xmaxs = [] ymins = [] ymaxs = [] for line in ax.lines: try: xmin, xmax, ymin, ymax = self._get_minmax(line) except ValueError: return xmins.append(xmin) xmaxs.append(xmax) ymins.append(ymin) ymaxs.append(ymax) try: xmin = min(xmins) except ValueError: xmin = xminax try: xmax = max(xmaxs) except ValueError: xmax = xmaxax try: ymin = min(ymins) except ValueError: ymin = yminax try: ymax = max(ymaxs) except ValueError: ymax = ymaxax xmincond = (xmin < xminax) or (xmin > xmidax) xmaxcond = (xmax > xmaxax) or (xmax < xmidax) ymincond = (ymin < yminax) or (ymin > ymidax) ymaxcond = (ymax > ymaxax) or (ymax < ymidax) cond = xmincond or xmaxcond or ymincond or ymaxcond if cond: dx = xmax - xmin dy = ymax - ymin newxmin = xmin - 0.1*dx newxmax = xmax + 0.1*dx newymin = ymin - 0.1*dy newymax = ymax + 0.1*dy if xflag: ax.set_xlim([newxmin, newxmax]) if yflag: ax.set_ylim([newymin, newymax]) @staticmethod def _get_minmax(line): xdata = line.get_xdata() ydata = line.get_ydata() try: xmin = min(xdata) xmax = max(xdata) ymin = min(ydata) ymax = max(ydata) except ValueError: errmsg = "Line {0} has no data.".format(repr(line)) raise ValueError(errmsg) return xmin, xmax, ymin, ymax def autoscale(self, xflag=True, yflag=None): """ Whether or not to autoscale the x or y axis. If only `xflag` is specified, then applies to both x and y axes. If both `xflag` and `yflag` are specified, then `xflag` sets the x axis autoscaling and `yflag` sets the y axis autoscaling. """ if yflag is None: yflag = xflag self._plotdict['autoscalex'] = bool(xflag) self._plotdict['autoscaley'] = bool(yflag) def windowsize(self, windowsize=None): """ Set the window size. The window size is the number of samples from the end of the file that will be plotted, i.e. the last `windowsize` samples will be shown on the plots. As such, it should be an integer. A `windowsize` of `None` indicates that the full set of data should be shown. This is the default setting. """ try: windowsize = int(windowsize) except ValueError: windowsize = None if windowsize <= 1: # Would plot a single point windowsize = None self._plotdict['windowsize'] = windowsize