def subplots_adjust(self, *args): """ Adjusts spacings. """ dimThing = args[0] if args else self.fig.get_window_extent() fWidth, fHeight = [getattr(dimThing, x) for x in ('width', 'height')] self.adj.updateFigSize(fWidth, fHeight) if self._figTitle: kw = { 'm': 10, 'fontsize': self.fontsize('title', 14), 'alpha': 1.0, 'fDims': (fWidth, fHeight), } ax = self.fig.get_axes()[0] if self.tbmTitle: self.tbmTitle.remove() self.tbmTitle = TextBoxMaker(self.fig, **kw)("N", self._figTitle) titleObj = self.tbmTitle.tList[0] else: self.tbmTitle = titleObj = None kw = self.adj(self._universal_xlabel, titleObj) try: self.fig.subplots_adjust(**kw) except ValueError as e: if self.verbose: print( (sub("WARNING: ValueError '{}' doing subplots_adjust({})", e.message, ", ".join([sub("{}={}", x, kw[x]) for x in kw])))) self.updateAnnotations()
def dims(self, textObj, size=None): """ Returns the dimensions of the supplied text object in pixels. If there's no renderer yet, estimates the dimensions based on font size and DPI. If you supply a string as the I{textObj}, you must also specify the font I{size}. """ if size is None: # Size not specified if isinstance(textObj, str): raise ValueError("You must specify size of text in a string") # Compute size from text object specified (not rendered) size size = self.DPI * textObj.get_size() / 72 if size in self.fontsizeMap: # Specified by name size = self.fontsizeMap[size] elif isinstance(size, int): # Specified as an int size = float(size) elif not isinstance(size, float): # Bogus size specification! raise ValueError(sub("Unknown font size {}", size)) return self.realistic_dims(textObj, size)
def plotVectors(self): """ Here is where I finally plot my X, Y vector pairs. B{TODO:} Support yscale, last seen in commit #e20e6c15. Or maybe don't bother. """ for k, pair in enumerate(self.pairs): kw = {} if pair.fmt else self.p.doKeywords(k, pair.kw) plotter = self.pickPlotter(pair.call, kw) # Finally, the actual plotting call args = [pair.X, pair.Y] if pair.fmt: args.append(pair.fmt) self.lineInfo[0].extend(plotter(*args, **kw)) # Add legend, if selected legend = self.p.opts['legend'] if k < len(legend): # We have a legend for this subplot legend = legend[k] elif self.p.opts['autolegend']: # We don't have a legend for the subplot, but # autolegend is enabled, so make one up legend = pair.Yname if not legend: legend = sub("#{:d}", k + 1) else: # We have gone past the last defined legend, or none # have been defined continue self.lineInfo[1].append(legend) if self.p.opts['useLabels']: self.addLegend(k, legend)
def avoid(self, obj): """ Call to have annotations avoid the region defined by any supplied Matplotlib obj having a C{get_window_extent} method. """ if not hasattr(obj, 'get_window_extent'): raise TypeError( sub("Supplied object {} has no 'get_window_extent' method!", obj)) self.avoided.add(obj)
def setDims(self, k, name, dims): """ For subplot I{k} (index starts at zero), sets the X and Y dimensions of an object with the specified I{name} to the supplied sequence I{dims}. """ dims = tuple(dims) if self.debug: print(sub("DIMS {:d}: {} <-- {}", k, name, dims)) self.sp_dicts.setdefault(k, {})[name] = dims
def __init__(self, ax, pairs, **kw): self.ax = ax self.annotations = [] self.pos = PositionEvaluator(ax, pairs, self.annotations) for name in kw: setattr(self, name, kw[name]) self.boxprops = self._boxprops.copy() self.boxprops['boxstyle'] = sub("round,pad={:0.3f}", self._paddingForSize()) self.db = DebugBoxer(self.ax) if self.verbose else None
def addAnnotations(self): """ Creates an L{Annotator} for my annotations to this subplot and then populates it. Twinned C{Axes} objects in a single subplot are not yet supported (and may never be), so the annotator object is constructed with my single C{Axes} object and operates only with that. That does B{not} mean that only a single vector can be annotated within one subplot. Twinning is an unusual use case and multiple vectors typically share the same C{Axes} object. An annotation can point its arrow to any X,Y coordinate in the subplot, whether that is on a plotted curve or not. This method only adds the annotations, plopping them right on top of the data points of interest. You need to call L{updateAnnotations} to have them intelligently repositioned after the subplot has been completed and its final dimensions are established, or (B{TODO}) if it is resized and the annotations need repositioning. """ self.p.plt.draw() annotator = self.get_annotator() for k, text, kVector, is_yValue in self.p.opts['annotations']: X, Y = self.pairs[kVector].getXY() if not isinstance(k, (int, np.int64)): if is_yValue: k = np.argmin(np.abs(Y - k)) else: k = np.searchsorted(X, k) if k < 0 or k >= len(X): continue x = X[k] y = Y[k] if isinstance(text, int): text = sub("{:d}", text) elif isinstance(text, float): text = sub("{:.2f}", text) # Annotator does not yet support twinned axes, nor are # they yet used annotator.add(x, y, text)
def getDims(self, k, name): """ For subplot I{k}, returns the dimension of the object with the specified I{name} or C{None} if no such dimension has been set. """ if k not in self.sp_dicts: return value = self.sp_dicts[k].get(name, None) if self.debug: print(sub("DIMS {:d}: {} -> {}", k, name, value)) return value
def show(self, windowTitle=None, fh=None, filePath=None, noShow=False): """ Call this to show the figure with suplots after the last call to my instance. If I have a non-C{None} I{fc} attribute (which must reference an instance of Qt's C{FigureCanvas}, then the FigureCanvas is drawn instead of PyPlot doing a window show. You can supply an open file-like object for PNG data to be written to (instead of a Matplotlib Figure being displayed) with the I{fh} keyword. (It's up to you to close the file object.) Or, with the I{filePath} keyword, you can specify the file path of a PNG file for me to create or overwrite. (That overrides any I{filePath} you set in the constructor.) """ try: self.fig.tight_layout() except ValueError as e: if self.verbose: proto = "WARNING: ValueError '{}' doing tight_layout "+\ "on {:.5g} x {:.5g} figure" print((sub(proto, e.message, self.width, self.height))) self.subplots_adjust() # Calling plt.draw massively slows things down when generating # plot images on Rpi. And without it, the (un-annotated) plot # still updates! if False and self.annotators: # This is not actually run, see above comment self.plt.draw() for annotator in list(self.annotators.values()): if self.verbose: annotator.setVerbose() annotator.update() if fh is None: if not filePath: filePath = self.filePath if filePath: fh = open(filePath, 'wb+') if fh is None: self.plt.draw() if windowTitle: self.fig.canvas.set_window_title(windowTitle) if self.fc is not None: self.fc.draw() elif not noShow: self.plt.show() else: self.fig.savefig(fh, format='png') self.plt.close() if filePath is not None: # Only close a file handle I opened myself fh.close() if not noShow: self.clear()
def pickPlotter(self, call, kw): """ Returns a reference to the plotting method of my I{ax} object named with I{call}, modifying the supplied I{kw} dict in-place as needed to work with that call. """ if call in PLOTTER_NAMES: func = getattr(self.ax, call, None) if func: for bogus in self.bogusMap.get(call, []): kw.pop(bogus, None) return func raise LookupError(sub("No recognized Axes method '{}'", call))
def __getattr__(self, name): """ You can access plotting methods and a given subplot's plotting options as attributes. If you request a plotting method, you'll get an instance of me with my I{_plotter} method set to I{name} first. """ if name in PLOTTER_NAMES: self._plotter = name return self if name in self.opts: return self.opts[name] raise AttributeError(sub("No plotting option or attribute '{}'", name))
def __call__(self, location, proto, *args, **options): kw = self.kw.copy() kw.update(options) location = self.conformLocation(location) text = sub(proto, *args) fDims = kw.pop('fDims') margin = kw.pop('m') if fDims: dims = self.tsc.pixels2fraction( self.tsc.dims(text, kw['fontsize']), fDims) if isinstance(margin, int): margins = [float(margin)/x for x in fDims] else: margins = [margin, margin] else: dims = [0, 0] if isinstance(margin, int): raise ValueError( "You must supply figure dims with integer margin") margins = [margin, margin] # Tall text boxes (many lines) get too much y-axis margin; # correct that. N_lines = text.count('\n') + 1 if N_lines > 3: margins[1] = margins[1]*(4.0/(4 + N_lines)) # Get the x, y location of the center of the box x, y = self.get_XY(location, dims, margins) # Come up with the appropriate keywords and then do the call # to obj.text kw['horizontalalignment'], kw['verticalalignment'] = \ self._textAlignment[location] if self.ax: kw['transform'] = self.ax.transAxes \ if hasattr(self.ax, 'transAxes') else self.ax.transFigure obj = self.ax else: obj = self.fig t = obj.text(x, y, text, **kw) if self.DEBUG: rectprops = {} rectprops['facecolor'] = "white" rectprops['edgecolor'] = "red" t.set_bbox(rectprops) self.tList.append(t) return self
def __repr__(self): args = [int(round(x)) for x in (self.Ax, self.Ay)] for x in (self.x0, self.y0, self.x1, self.y1): args.append(int(round(x))) return sub("({:d}, {:d}) --> [{:d},{:d} {:d},{:d}]", *args)
def withSuffix(x): return sub("{:+.2f}{}", x, suffix)
def addCall(self, args, kw): """ Parses the supplied I{args} into X, Y pairs of vectors, appending a L{Pair} object for each to my I{pairs} list. Pops the first arg and uses it as a container if it is a container (e.g., a dict, not a Numpy array). Otherwise, refers to the I{V} attribute of my L{Plotter} I{p} as a possible container. If there is indeed a container, and the remaining args are all strings referencing items that are in it, makes the remaining args into vectors. Each item of the names list is a string with the name of a vector at the same index of I{vectors}, or C{None} if that vector was supplied as-is and thus no name name was available. Called with the next subplot's local options, so need to temporarily switch back to options for the current subplot, for the duration of this call. """ def likeArray(X): return isinstance(X, (list, tuple, np.ndarray)) self.p.opts.usePrevLocal() args = list(args) # The 'plotter' keyword is reserved for Yampex, unrecognized # by Matplotlib call = kw.pop('plotter', self.p.opts['call']) # The 'legend' keyword gets co-opted, but ultimately shows up # in the plot like you would expect legend = kw.pop('legend', None) if legend and not isinstance(legend, (list, tuple)): legend = [legend] # Process args if args and self.p.V is None: if likeArray(args[0]): V = None OK = True elif len(args) < 2: OK = False else: V = args.pop(0) try: OK = likeArray(V[args[0]]) except: OK = False if not OK: raise ValueError(sub( "No instance-wide V container and first "+\ "arg {} is not a valid container of a next arg", V)) else: V = self.p.V X0, X0_name = self.pairs.firstX() Xs = [] names = [] strings = {} for k, arg in enumerate(args): X, name, isArray = self.arrayify(V, arg) if isArray: Xs.append(X) # A keyword-specified legend overrides any auto-generated name set # otherwise thisLegend = None if legend: if len(args) == 1: thisLegend = legend[0] elif k > 0 and k <= len(legend): thisLegend = legend[k - 1] if thisLegend: if self.p.opts['autolegend']: name = thisLegend else: self.p.add_legend(thisLegend) names.append(name) else: strings[k] = X if len(Xs) == 1: kStart = 0 # Just one vector supplied... if X0 is None: # ...and no x-axis range vector yet, so create one X0 = np.arange(len(Xs[0])) X0_name = None # ... but we have an x-axis range vector, so we'll just # re-use it elif Xs: kStart = 1 # Use this call's x-axis vector X = Xs.pop(0) # Set time scaling based on this x-axis vector if it's the # first one if X0 is None: self._timeScaling(X) X0 = X X0_name = names.pop(0) # Make pairs with the x-axis vector and the remaining vector(s) for k, Y in enumerate(Xs): if X0.shape != Y.shape: raise ValueError( sub("Shapes differ for X, Y: {} vs {}", X0.shape, Y.shape)) pair = Pair() pair.call = call pair.X = X0 pair.Xname = X0_name pair.Y = Y pair.Yname = names[k] key = k + kStart + 1 pair.fmt = strings.pop(key) if key in strings else None pair.kw = kw self.pairs.append(pair) self.p.opts.useLastLocal()
def add_annotations(self, k, prefix): for kVector in (0, 1): text = sub("{}, {}", prefix, "upper" if kVector else "lower") self.sp.add_annotation(k, text, kVector=kVector)
def __repr__(self): return sub("tox={:.5g}, NA={:.5g}", self.tox, self.NA)