def _get_text1(self, loc): 'Get the default Text instance' # the y loc is 3 points below the min of y axis # get the affine as an a,b,c,d,tx,ty list # x in data coords, y in axes coords #t = Text( t = TextWithDash( x=loc, y=0, fontproperties=FontProperties(size=rcParams['tick.labelsize']), color=rcParams['tick.color'], verticalalignment='top', horizontalalignment='center', dashdirection=0, xaxis=True, ) trans = blend_xy_sep_transform(self.axes.transData, self.axes.transAxes) #offset the text downward with a post transformation transOffset = translation_transform(Value(0), Value(-1) * self._padPixels) trans.set_offset((0, 0), transOffset) t.set_transform(trans) self._set_artist_props(t) return t
def _get_text1(self, loc): 'Get the default Text instance' # x in axes coords, y in data coords #t = Text( t = TextWithDash( x=0, y=loc, fontproperties=FontProperties(size=rcParams['tick.labelsize']), color=rcParams['tick.color'], verticalalignment='center', horizontalalignment='right', dashdirection=0, xaxis=False, ) trans = blend_xy_sep_transform(self.axes.transAxes, self.axes.transData) # offset the text leftward with a post transformation transOffset = translation_transform( Value(-1) * self._padPixels, Value(0)) trans.set_offset((0, 0), transOffset) t.set_transform(trans) #t.set_transform( self.axes.transData ) self._set_artist_props(t) return t
def __init__(self, x=0, y=0, text='', color=None, # defaults to rc params verticalalignment='bottom', horizontalalignment='left', multialignment=None, fontproperties=None, # defaults to FontProperties() rotation=None, ): Artist.__init__(self) self.cached = {} self._x, self._y = x, y if color is None: color = rcParams['text.color'] if fontproperties is None: fontproperties=FontProperties() self._color = color self._text = text self._verticalalignment = verticalalignment self._horizontalalignment = horizontalalignment self._multialignment = multialignment self._rotation = rotation self._fontproperties = fontproperties
def __init__(self, x=0, y=0, text='', color=None, # defaults to rc params verticalalignment='bottom', horizontalalignment='left', multialignment=None, fontproperties=None, # defaults to FontProperties() rotation=None, ): Artist.__init__(self) if not is_string_like(text): raise TypeError('text must be a string type') self.cached = {} self._x, self._y = x, y if color is None: color = rcParams['text.color'] if fontproperties is None: fontproperties=FontProperties() self._color = color self.set_text(text) self._verticalalignment = verticalalignment self._horizontalalignment = horizontalalignment self._multialignment = multialignment self._rotation = rotation self._fontproperties = fontproperties self._bbox = None self._renderer = None
def set_fontproperties(self, fp): """ Set the font properties that control the text ACCEPTS: a matplotlib.font_manager.FontProperties instance """ if is_string_like(fp): fp = FontProperties(fp) self._fontproperties = fp
def _make_font_props(self): """ Returns a font_manager.FontProperties object that encapsulates our font properties """ # XXX: change the weight to a numerical value if self.style == BOLD or self.style == BOLD_ITALIC: weight = "bold" else: weight = "normal" if self.style == ITALIC or self.style == BOLD_ITALIC: style = "italic" else: style = "normal" fp = FontProperties(family = self.familymap[self.family], style=style, weight=weight, size = self.size) if self.face_name != "": fp.set_name(self.face_name) return fp
def __init__( self, x=0, y=0, text='', color=None, # defaults to rc params verticalalignment='bottom', horizontalalignment='left', multialignment=None, fontproperties=None, # defaults to FontProperties() rotation=None, linespacing=None, **kwargs): """ Create a :class:`~matplotlib.text.Text` instance at *x*, *y* with string *text*. Valid kwargs are %(Text)s """ Artist.__init__(self) self.cached = maxdict(5) self._x, self._y = x, y if color is None: color = rcParams['text.color'] if fontproperties is None: fontproperties = FontProperties() elif is_string_like(fontproperties): fontproperties = FontProperties(fontproperties) self.set_text(text) self.set_color(color) self._verticalalignment = verticalalignment self._horizontalalignment = horizontalalignment self._multialignment = multialignment self._rotation = rotation self._fontproperties = fontproperties self._bbox = None self._renderer = None if linespacing is None: linespacing = 1.2 # Maybe use rcParam later. self._linespacing = linespacing self.update(kwargs)
def _get_offset_text(self): # x in display coords, y in axes coords (to be updated at draw time) offsetText = Text(x=0, y=0.5, fontproperties = FontProperties(size=rcParams['ytick.labelsize']), color = rcParams['ytick.color'], verticalalignment = 'bottom', horizontalalignment = 'left', ) offsetText.set_transform(blend_xy_sep_transform(self.axes.transAxes, identity_transform()) ) self._set_artist_props(offsetText) self.offset_text_position='left' return offsetText
def _get_label(self): # x in axes coords, y in display coords (to be updated at draw # time by _update_label_positions label = Text(x=0.5, y=0, fontproperties = FontProperties(size=rcParams['axes.labelsize']), color = rcParams['axes.labelcolor'], verticalalignment='top', horizontalalignment='center', ) label.set_transform( blend_xy_sep_transform( self.axes.transAxes, identity_transform() )) self._set_artist_props(label) return label
def _get_label(self): # x in display coords (updated by _update_label_position) # y in axes coords label = Text(x=0, y=0.5, # todo: get the label position fontproperties=FontProperties(size=rcParams['axes.labelsize']), color = rcParams['axes.labelcolor'], verticalalignment='center', horizontalalignment='right', rotation='vertical', ) label.set_transform( blend_xy_sep_transform( identity_transform(), self.axes.transAxes) ) self._set_artist_props(label) return label
def _get_text2(self, loc): 'Get the default Text 2 instance' # x in data coords, y in axes coords t = Text(x=loc, y=1, fontproperties=FontProperties(size=rcParams['tick.labelsize']), color=rcParams['tick.color'], verticalalignment='bottom', horizontalalignment='center', ) trans = blend_xy_sep_transform( self.axes.transData, self.axes.transAxes) # offset the text upward with a post transformation transOffset = translation_transform( Value(0), self._padPixels) trans.set_offset( (0,0), transOffset) t.set_transform( trans ) self._set_artist_props(t) return t
def _get_texts(self, labels, left, upper): # height in axes coords HEIGHT = self._approx_text_height() pos = upper x = left ret = [] # the returned list of text instances for l in labels: text = Text( x=x, y=pos, text=l, fontproperties=FontProperties(size='smaller'), verticalalignment='top', horizontalalignment='left', ) self._set_artist_props(text) ret.append(text) pos -= HEIGHT return ret
class ContourLabeler: def __init__(self, ax): self.ax = ax def clabel(self, *args, **kwargs): """ CLABEL(*args, **kwargs) Function signatures CLABEL(C) - plots contour labels, C is the output of contour or a list of contours CLABEL(C,V) - creates labels only for those contours, given in a list V CLABEL(C, **kwargs) - keyword args are explained below: * fontsize = None: as described in http://matplotlib.sf.net/fonts.html * colors = None: - a tuple of matplotlib color args (string, float, rgb, etc), different labels will be plotted in different colors in the order specified - one string color, e.g. colors = 'r' or colors = 'red', all labels will be plotted in this color - if colors == None, the color of each label matches the color of the corresponding contour * inline = 0: controls whether the underlying contour is removed (inline = 1) or not * fmt = '%1.3f': a format string for the label """ # todo, factor this out to a separate class and don't use hidden coll attrs if not self.ax.ishold(): self.ax.cla() fontsize = kwargs.get('fontsize', None) inline = kwargs.get('inline', 0) fmt = kwargs.get('fmt', '%1.3f') colors = kwargs.get('colors', None) if len(args) == 1: contours = args[0] levels = [con._label for con in contours] elif len(args) == 2: contours = args[0] levels = args[1] else: raise TypeError("Illegal arguments to clabel, see help(clabel)") self.fp = FontProperties() if fontsize == None: font_size = int(self.fp.get_size_in_points()) else: if type(fontsize) not in [int, float, str]: raise TypeError("Font size must be an integer number.") else: if type(fontsize) == str: font_size = int(self.fp.get_size_in_points()) else: self.fp.set_size(fontsize) font_size = fontsize fslist = [font_size] * len(levels) if colors == None: colors = [c._colors[0] for c in contours] else: colors = colors * len(contours) if inline not in [0, 1]: raise TypeError("inline must be 0 or 1") self.cl = [] self.cl_xy = [] # we have a list of contours and each contour has a list of # segments. We want changes in the contour color to be # reflected in changes in the label color. This is a good use # for traits observers, but in the interim, until traits are # utilized, we'll create a dict mapping i,j to text instances. # i is the contour level index, j is the sement index self.labeld = {} if inline == 1: self.inline_labels(levels, contours, colors, fslist, fmt) else: self.labels(levels, contours, colors, fslist, fmt) for label in self.cl: self.ax.add_artist(label) ret = silent_list('Text', self.cl) ret.mappable = getattr(contours, 'mappable', None) # support colormapping for label if ret.mappable is not None: ret.mappable.labeld = self.labeld return ret def print_label(self, linecontour, labelwidth): "if contours are too short, don't plot a label" lcsize = len(linecontour) if lcsize > 10 * labelwidth: return 1 xmax = amax(array(linecontour)[:, 0]) xmin = amin(array(linecontour)[:, 0]) ymax = amax(array(linecontour)[:, 1]) ymin = amin(array(linecontour)[:, 1]) lw = labelwidth if (xmax - xmin) > 1.2 * lw or (ymax - ymin) > 1.2 * lw: return 1 else: return 0 def too_close(self, x, y, lw): "if there's a label already nearby, find a better place" if self.cl_xy != []: dist = [ sqrt((x - loc[0])**2 + (y - loc[1])**2) for loc in self.cl_xy ] for d in dist: if d < 1.2 * lw: return 1 else: return 0 else: return 0 def get_label_coords(self, distances, XX, YY, ysize, lw): """ labels are ploted at a location with the smallest dispersion of the contour from a straight line unless there's another label nearby, in which case the second best place on the contour is picked up if there's no good place a label isplotted at the beginning of the contour """ hysize = int(ysize / 2) adist = argsort(distances) for ind in adist: x, y = XX[ind][hysize], YY[ind][hysize] if self.too_close(x, y, lw): continue else: self.cl_xy.append((x, y)) return x, y, ind ind = adist[0] x, y = XX[ind][hysize], YY[ind][hysize] self.cl_xy.append((x, y)) return x, y, ind def get_label_width(self, lev, fmt, fsize): "get the width of the label in points" if is_string_like(lev): lw = (len(lev)) * fsize else: lw = (len(fmt % lev)) * fsize return lw def set_label_props(self, label, text, color): "set the label properties - color, fontsize, text" label.set_text(text) label.set_color(color) label.set_fontproperties(self.fp) label.set_clip_box(self.ax.bbox) def get_text(self, lev, fmt): "get the text of the label" if is_string_like(lev): return lev else: return fmt % lev def break_linecontour(self, linecontour, rot, labelwidth, ind): "break a contour in two contours at the location of the label" lcsize = len(linecontour) hlw = int(labelwidth / 2) #length of label in screen coords ylabel = abs(hlw * sin(rot * pi / 180)) xlabel = abs(hlw * cos(rot * pi / 180)) trans = self.ax.transData slc = trans.seq_xy_tups(linecontour) x, y = slc[ind] xx = array(slc)[:, 0].copy() yy = array(slc)[:, 1].copy() #indices which are under the label inds = nonzero(((xx < x + xlabel) & (xx > x - xlabel)) & ((yy < y + ylabel) & (yy > y - ylabel))) if len(inds) > 0: #if the label happens to be over the beginning of the #contour, the entire contour is removed, i.e. #indices to be removed are #inds= [0,1,2,3,305,306,307] #should rewrite this in a better way linds = nonzero(inds[1:] - inds[:-1] != 1) if inds[0] == 0 and len(linds) != 0: ii = inds[linds[0]] lc1 = linecontour[ii + 1:inds[ii + 1]] lc2 = [] else: lc1 = linecontour[:inds[0]] lc2 = linecontour[inds[-1] + 1:] else: lc1 = linecontour[:ind] lc2 = linecontour[ind + 1:] if rot < 0: new_x1, new_y1 = x - xlabel, y + ylabel new_x2, new_y2 = x + xlabel, y - ylabel else: new_x1, new_y1 = x - xlabel, y - ylabel new_x2, new_y2 = x + xlabel, y + ylabel new_x1d, new_y1d = trans.inverse_xy_tup((new_x1, new_y1)) new_x2d, new_y2d = trans.inverse_xy_tup((new_x2, new_y2)) if rot > 0: if len(lc1) > 0 and (lc1[-1][0] <= new_x1d) and (lc1[-1][1] <= new_y1d): lc1.append((new_x1d, new_y1d)) if len(lc2) > 0 and (lc2[0][0] >= new_x2d) and (lc2[0][1] >= new_y2d): lc2.insert(0, (new_x2d, new_y2d)) else: if len(lc1) > 0 and ((lc1[-1][0] <= new_x1d) and (lc1[-1][1] >= new_y1d)): lc1.append((new_x1d, new_y1d)) if len(lc2) > 0 and ((lc2[0][0] >= new_x2d) and (lc2[0][1] <= new_y2d)): lc2.insert(0, (new_x2d, new_y2d)) return [lc1, lc2] def locate_label(self, linecontour, labelwidth): """find a good place to plot a label (relatively flat part of the contour) and the angle of rotation for the text object """ nsize = len(linecontour) if labelwidth > 1: xsize = int(ceil(nsize / labelwidth)) else: xsize = 1 if xsize == 1: ysize = nsize else: ysize = labelwidth XX = resize(array(linecontour)[:, 0], (xsize, ysize)) YY = resize(array(linecontour)[:, 1], (xsize, ysize)) yfirst = YY[:, 0] ylast = YY[:, -1] xfirst = XX[:, 0] xlast = XX[:, -1] s = (reshape(yfirst, (xsize, 1)) - YY) * (reshape(xlast, (xsize, 1)) - reshape( xfirst, (xsize, 1))) - (reshape(xfirst, (xsize, 1)) - XX) * ( reshape(ylast, (xsize, 1)) - reshape(yfirst, (xsize, 1))) L = sqrt((xlast - xfirst)**2 + (ylast - yfirst)**2) dist = add.reduce(([(abs(s)[i] / L[i]) for i in range(xsize)]), -1) x, y, ind = self.get_label_coords(dist, XX, YY, ysize, labelwidth) angle = arctan2(ylast - yfirst, xlast - xfirst) rotation = angle[ind] * 180 / pi if rotation > 90: rotation = rotation - 180 if rotation < -90: rotation = 180 + rotation dind = list(linecontour).index((x, y)) return x, y, rotation, dind def inline_labels(self, levels, contours, colors, fslist, fmt): trans = self.ax.transData contourNum = 0 for lev, con, color, fsize in zip(levels, contours, colors, fslist): toremove = [] toadd = [] lw = self.get_label_width(lev, fmt, fsize) for segNum, linecontour in enumerate(con._segments): key = contourNum, segNum # for closed contours add one more point to # avoid division by zero if linecontour[0] == linecontour[-1]: linecontour.append(linecontour[1]) # transfer all data points to screen coordinates slc = trans.seq_xy_tups(linecontour) if self.print_label(slc, lw): x, y, rotation, ind = self.locate_label(slc, lw) # transfer the location of the label back to # data coordinates dx, dy = trans.inverse_xy_tup((x, y)) t = Text(dx, dy, rotation=rotation, horizontalalignment='center', verticalalignment='center') self.labeld[key] = t text = self.get_text(lev, fmt) self.set_label_props(t, text, color) self.cl.append(t) new = self.break_linecontour(linecontour, rotation, lw, ind) for c in new: toadd.append(c) toremove.append(linecontour) for c in toremove: con._segments.remove(c) for c in toadd: con._segments.append(c) contourNum += 1 def labels(self, levels, contours, colors, fslist, fmt): trans = self.ax.transData for lev, con, color, fsize in zip(levels, contours, colors, fslist): lw = self.get_label_width(lev, fmt, fsize) for linecontour in con._segments: # for closed contours add one more point if linecontour[0] == linecontour[-1]: linecontour.append(linecontour[1]) # transfer all data points to screen coordinates slc = trans.seq_xy_tups(linecontour) if self.print_label(slc, lw): x, y, rotation, ind = self.locate_label(slc, lw) # transfer the location of the label back into # data coordinates dx, dy = trans.inverse_xy_tup((x, y)) t = Text(dx, dy, rotation=rotation, horizontalalignment='center', verticalalignment='center') text = self.get_text(lev, fmt) self.set_label_props(t, text, color) self.cl.append(t) else: pass
def clabel(self, *args, **kwargs): """ CLABEL(*args, **kwargs) Function signatures CLABEL(C) - plots contour labels, C is the output of contour or a list of contours CLABEL(C,V) - creates labels only for those contours, given in a list V CLABEL(C, **kwargs) - keyword args are explained below: * fontsize = None: as described in http://matplotlib.sf.net/fonts.html * colors = None: - a tuple of matplotlib color args (string, float, rgb, etc), different labels will be plotted in different colors in the order specified - one string color, e.g. colors = 'r' or colors = 'red', all labels will be plotted in this color - if colors == None, the color of each label matches the color of the corresponding contour * inline = 0: controls whether the underlying contour is removed (inline = 1) or not * fmt = '%1.3f': a format string for the label """ # todo, factor this out to a separate class and don't use hidden coll attrs if not self.ax.ishold(): self.ax.cla() fontsize = kwargs.get('fontsize', None) inline = kwargs.get('inline', 0) fmt = kwargs.get('fmt', '%1.3f') colors = kwargs.get('colors', None) if len(args) == 1: contours = args[0] levels = [con._label for con in contours] elif len(args) == 2: contours = args[0] levels = args[1] else: raise TypeError("Illegal arguments to clabel, see help(clabel)") self.fp = FontProperties() if fontsize == None: font_size = int(self.fp.get_size_in_points()) else: if type(fontsize) not in [int, float, str]: raise TypeError("Font size must be an integer number.") else: if type(fontsize) == str: font_size = int(self.fp.get_size_in_points()) else: self.fp.set_size(fontsize) font_size = fontsize fslist = [font_size] * len(levels) if colors == None: colors = [c._colors[0] for c in contours] else: colors = colors * len(contours) if inline not in [0, 1]: raise TypeError("inline must be 0 or 1") self.cl = [] self.cl_xy = [] # we have a list of contours and each contour has a list of # segments. We want changes in the contour color to be # reflected in changes in the label color. This is a good use # for traits observers, but in the interim, until traits are # utilized, we'll create a dict mapping i,j to text instances. # i is the contour level index, j is the sement index self.labeld = {} if inline == 1: self.inline_labels(levels, contours, colors, fslist, fmt) else: self.labels(levels, contours, colors, fslist, fmt) for label in self.cl: self.ax.add_artist(label) ret = silent_list('Text', self.cl) ret.mappable = getattr(contours, 'mappable', None) # support colormapping for label if ret.mappable is not None: ret.mappable.labeld = self.labeld return ret
def __init__(self, parent, handles, labels, loc = None, numpoints = None, # the number of points in the legend line prop = None, pad = None, # the fractional whitespace inside the legend border markerscale = None, # the relative size of legend markers vs. original # the following dimensions are in axes coords labelsep = None, # the vertical space between the legend entries handlelen = None, # the length of the legend lines handletextsep = None, # the space between the legend line and legend text axespad = None, # the border between the axes and legend edge shadow = None ): """ parent # the artist that contains the legend handles # a list of artists (lines, patches) to add to the legend labels # a list of strings to label the legend loc # a location code numpoints = 4 # the number of points in the legend line prop = FontProperties(size='smaller') # the font property pad = 0.2 # the fractional whitespace inside the legend border markerscale = 0.6 # the relative size of legend markers vs. original shadow # if True, draw a shadow behind legend The following dimensions are in axes coords labelsep = 0.005 # the vertical space between the legend entries handlelen = 0.05 # the length of the legend lines handletextsep = 0.02 # the space between the legend line and legend text axespad = 0.02 # the border between the axes and legend edge """ from axes import Axes # local import only to avoid circularity from figure import Figure # local import only to avoid circularity Artist.__init__(self) proplist=[numpoints, pad, markerscale, labelsep, handlelen, handletextsep, axespad, shadow] propnames=['numpoints', 'pad', 'markerscale', 'labelsep', 'handlelen', 'handletextsep', 'axespad', 'shadow'] for name, value in safezip(propnames,proplist): if value is None: value=rcParams["legend."+name] setattr(self,name,value) if self.numpoints <= 0: raise ValueError("numpoints must be >= 0; it was %d"% numpoints) if prop is None: self.prop=FontProperties(size=rcParams["legend.fontsize"]) else: self.prop=prop self.fontsize = self.prop.get_size_in_points() if isinstance(parent,Axes): self.isaxes = True self.set_figure(parent.figure) elif isinstance(parent,Figure): self.isaxes = False self.set_figure(parent) else: raise TypeError("Legend needs either Axes or Figure as parent") self.parent = parent self._offsetTransform = Affine2D() self._parentTransform = BboxTransformTo(parent.bbox) Artist.set_transform(self, self._offsetTransform + self._parentTransform) if loc is None: loc = rcParams["legend.loc"] if not self.isaxes and loc in [0,'best']: loc = 'upper right' if is_string_like(loc): if not self.codes.has_key(loc): if self.isaxes: warnings.warn('Unrecognized location "%s". Falling back on "best"; ' 'valid locations are\n\t%s\n' % (loc, '\n\t'.join(self.codes.keys()))) loc = 0 else: warnings.warn('Unrecognized location "%s". Falling back on "upper right"; ' 'valid locations are\n\t%s\n' % (loc, '\n\t'.join(self.codes.keys()))) loc = 1 else: loc = self.codes[loc] if not self.isaxes and loc == 0: warnings.warn('Automatic legend placement (loc="best") not implemented for figure legend. ' 'Falling back on "upper right".') loc = 1 self._loc = loc self.legendPatch = Rectangle( xy=(0.0, 0.0), width=0.5, height=0.5, facecolor='w', edgecolor='k', ) self._set_artist_props(self.legendPatch) # make a trial box in the middle of the axes. relocate it # based on it's bbox left, top = 0.5, 0.5 textleft = left+ self.handlelen+self.handletextsep self.texts = self._get_texts(labels, textleft, top) self.legendHandles = self._get_handles(handles, self.texts) self._drawFrame = True
def __init__(self, parent, handles, labels, loc, isaxes=True, numpoints = 4, # the number of points in the legend line prop = FontProperties(size='smaller'), pad = 0.2, # the fractional whitespace inside the legend border markerscale = 0.6, # the relative size of legend markers vs. original # the following dimensions are in axes coords labelsep = 0.005, # the vertical space between the legend entries handlelen = 0.05, # the length of the legend lines handletextsep = 0.02, # the space between the legend line and legend text axespad = 0.02, # the border between the axes and legend edge shadow=False, ): """ parent # the artist that contains the legend handles # a list of artists (lines, patches) to add to the legend labels # a list of strings to label the legend loc # a location code isaxes=True # whether this is an axes legend numpoints = 4 # the number of points in the legend line fontprop = FontProperties('smaller') # the font property pad = 0.2 # the fractional whitespace inside the legend border markerscale = 0.6 # the relative size of legend markers vs. original shadow # if True, draw a shadow behind legend The following dimensions are in axes coords labelsep = 0.005 # the vertical space between the legend entries handlelen = 0.05 # the length of the legend lines handletextsep = 0.02 # the space between the legend line and legend text axespad = 0.02 # the border between the axes and legend edge """ Artist.__init__(self) if is_string_like(loc) and not self.codes.has_key(loc): verbose.report_error('Unrecognized location %s. Falling back on upper right; valid locations are\n%s\t' %(loc, '\n\t'.join(self.codes.keys()))) if is_string_like(loc): loc = self.codes.get(loc, 1) self.numpoints = numpoints self.prop = prop self.fontsize = prop.get_size_in_points() self.pad = pad self.markerscale = markerscale self.labelsep = labelsep self.handlelen = handlelen self.handletextsep = handletextsep self.axespad = axespad self.shadow = shadow if isaxes: # parent is an Axes self.set_figure(parent.figure) else: # parent is a Figure self.set_figure(parent) self.parent = parent self.set_transform( get_bbox_transform( unit_bbox(), parent.bbox) ) self._loc = loc # make a trial box in the middle of the axes. relocate it # based on it's bbox left, upper = 0.5, 0.5 if self.numpoints == 1: self._xdata = array([left + self.handlelen*0.5]) else: self._xdata = linspace(left, left + self.handlelen, self.numpoints) textleft = left+ self.handlelen+self.handletextsep self.texts = self._get_texts(labels, textleft, upper) self.handles = self._get_handles(handles, self.texts) left, top = self.texts[-1].get_position() HEIGHT = self._approx_text_height() bottom = top-HEIGHT left -= self.handlelen + self.handletextsep + self.pad self.legendPatch = Rectangle( xy=(left, bottom), width=0.5, height=HEIGHT*len(self.texts), facecolor='w', edgecolor='k', ) self._set_artist_props(self.legendPatch) self._drawFrame = True
class ContourLabeler: def __init__(self, ax): self.ax = ax def clabel(self, *args, **kwargs): """ CLABEL(*args, **kwargs) Function signatures CLABEL(C) - plots contour labels, C is the output of contour or a list of contours CLABEL(C,V) - creates labels only for those contours, given in a list V CLABEL(C, **kwargs) - keyword args are explained below: * fontsize = None: as described in http://matplotlib.sf.net/fonts.html * colors = None: - a tuple of matplotlib color args (string, float, rgb, etc), different labels will be plotted in different colors in the order specified - one string color, e.g. colors = 'r' or colors = 'red', all labels will be plotted in this color - if colors == None, the color of each label matches the color of the corresponding contour * inline = 0: controls whether the underlying contour is removed (inline = 1) or not * fmt = '%1.3f': a format string for the label """ # todo, factor this out to a separate class and don't use hidden coll attrs if not self.ax.ishold(): self.ax.cla() fontsize = kwargs.get('fontsize', None) inline = kwargs.get('inline', 0) fmt = kwargs.get('fmt', '%1.3f') colors = kwargs.get('colors', None) if len(args) == 1: contours = args[0] levels = [con._label for con in contours] elif len(args) == 2: contours = args[0] levels = args[1] else: raise TypeError("Illegal arguments to clabel, see help(clabel)") self.fp = FontProperties() if fontsize == None: font_size = int(self.fp.get_size_in_points()) else: if type(fontsize) not in [int, float, str]: raise TypeError("Font size must be an integer number.") else: if type(fontsize) == str: font_size = int(self.fp.get_size_in_points()) else: self.fp.set_size(fontsize) font_size = fontsize fslist = [font_size] * len(levels) if colors == None: colors = [c._colors[0] for c in contours] else: colors = colors * len(contours) if inline not in [0,1]: raise TypeError("inline must be 0 or 1") self.cl = [] self.cl_xy = [] # we have a list of contours and each contour has a list of # segments. We want changes in the contour color to be # reflected in changes in the label color. This is a good use # for traits observers, but in the interim, until traits are # utilized, we'll create a dict mapping i,j to text instances. # i is the contour level index, j is the sement index self.labeld = {} if inline == 1: self.inline_labels(levels, contours, colors, fslist, fmt) else: self.labels(levels, contours, colors, fslist, fmt) for label in self.cl: self.ax.add_artist(label) ret = silent_list('Text', self.cl) ret.mappable = getattr(contours, 'mappable', None) # support colormapping for label if ret.mappable is not None: ret.mappable.labeld = self.labeld return ret def print_label(self, linecontour,labelwidth): "if contours are too short, don't plot a label" lcsize = len(linecontour) if lcsize > 10 * labelwidth: return 1 xmax = amax(array(linecontour)[:,0]) xmin = amin(array(linecontour)[:,0]) ymax = amax(array(linecontour)[:,1]) ymin = amin(array(linecontour)[:,1]) lw = labelwidth if (xmax - xmin) > 1.2* lw or (ymax - ymin) > 1.2 * lw: return 1 else: return 0 def too_close(self, x,y, lw): "if there's a label already nearby, find a better place" if self.cl_xy != []: dist = [sqrt((x-loc[0]) ** 2 + (y-loc[1]) ** 2) for loc in self.cl_xy] for d in dist: if d < 1.2*lw: return 1 else: return 0 else: return 0 def get_label_coords(self, distances, XX, YY, ysize, lw): """ labels are ploted at a location with the smallest dispersion of the contour from a straight line unless there's another label nearby, in which case the second best place on the contour is picked up if there's no good place a label isplotted at the beginning of the contour """ hysize = int(ysize/2) adist = argsort(distances) for ind in adist: x, y = XX[ind][hysize], YY[ind][hysize] if self.too_close(x,y, lw): continue else: self.cl_xy.append((x,y)) return x,y, ind ind = adist[0] x, y = XX[ind][hysize], YY[ind][hysize] self.cl_xy.append((x,y)) return x,y, ind def get_label_width(self, lev, fmt, fsize): "get the width of the label in points" if is_string_like(lev): lw = (len(lev)) * fsize else: lw = (len(fmt%lev)) * fsize return lw def set_label_props(self, label,text, color): "set the label properties - color, fontsize, text" label.set_text(text) label.set_color(color) label.set_fontproperties(self.fp) label.set_clip_box(self.ax.bbox) def get_text(self, lev, fmt): "get the text of the label" if is_string_like(lev): return lev else: return fmt%lev def break_linecontour(self, linecontour, rot, labelwidth, ind): "break a contour in two contours at the location of the label" lcsize = len(linecontour) hlw = int(labelwidth/2) #length of label in screen coords ylabel = abs(hlw * sin(rot*pi/180)) xlabel = abs(hlw * cos(rot*pi/180)) trans = self.ax.transData slc = trans.seq_xy_tups(linecontour) x,y = slc[ind] xx= array(slc)[:,0].copy() yy=array(slc)[:,1].copy() #indices which are under the label inds=nonzero(((xx < x+xlabel) & (xx > x-xlabel)) & ((yy < y+ylabel) & (yy > y-ylabel))) if len(inds) >0: #if the label happens to be over the beginning of the #contour, the entire contour is removed, i.e. #indices to be removed are #inds= [0,1,2,3,305,306,307] #should rewrite this in a better way linds = nonzero(inds[1:]- inds[:-1] != 1) if inds[0] == 0 and len(linds) != 0: ii = inds[linds[0]] lc1 =linecontour[ii+1:inds[ii+1]] lc2 = [] else: lc1=linecontour[:inds[0]] lc2= linecontour[inds[-1]+1:] else: lc1=linecontour[:ind] lc2 = linecontour[ind+1:] if rot <0: new_x1, new_y1 = x-xlabel, y+ylabel new_x2, new_y2 = x+xlabel, y-ylabel else: new_x1, new_y1 = x-xlabel, y-ylabel new_x2, new_y2 = x+xlabel, y+ylabel new_x1d, new_y1d = trans.inverse_xy_tup((new_x1, new_y1)) new_x2d, new_y2d = trans.inverse_xy_tup((new_x2, new_y2)) if rot > 0: if len(lc1) > 0 and (lc1[-1][0] <= new_x1d) and (lc1[-1][1] <= new_y1d): lc1.append((new_x1d, new_y1d)) if len(lc2) > 0 and (lc2[0][0] >= new_x2d) and (lc2[0][1] >= new_y2d): lc2.insert(0, (new_x2d, new_y2d)) else: if len(lc1) > 0 and ((lc1[-1][0] <= new_x1d) and (lc1[-1][1] >= new_y1d)): lc1.append((new_x1d, new_y1d)) if len(lc2) > 0 and ((lc2[0][0] >= new_x2d) and (lc2[0][1] <= new_y2d)): lc2.insert(0, (new_x2d, new_y2d)) return [lc1,lc2] def locate_label(self, linecontour, labelwidth): """find a good place to plot a label (relatively flat part of the contour) and the angle of rotation for the text object """ nsize= len(linecontour) if labelwidth > 1: xsize = int(ceil(nsize/labelwidth)) else: xsize = 1 if xsize == 1: ysize = nsize else: ysize = labelwidth XX = resize(array(linecontour)[:,0],(xsize, ysize)) YY = resize(array(linecontour)[:,1],(xsize,ysize)) yfirst = YY[:,0] ylast = YY[:,-1] xfirst = XX[:,0] xlast = XX[:,-1] s = (reshape(yfirst, (xsize,1))-YY)*(reshape(xlast,(xsize,1))-reshape(xfirst,(xsize,1)))-(reshape(xfirst,(xsize,1))-XX)*(reshape(ylast,(xsize,1))-reshape(yfirst,(xsize,1))) L=sqrt((xlast-xfirst)**2+(ylast-yfirst)**2) dist = add.reduce(([(abs(s)[i]/L[i]) for i in range(xsize)]),-1) x,y,ind = self.get_label_coords(dist, XX, YY, ysize, labelwidth) angle = arctan2(ylast - yfirst, xlast - xfirst) rotation = angle[ind]*180/pi if rotation > 90: rotation = rotation -180 if rotation < -90: rotation = 180 + rotation dind = list(linecontour).index((x,y)) return x,y, rotation, dind def inline_labels(self, levels, contours, colors, fslist, fmt): trans = self.ax.transData contourNum = 0 for lev, con, color, fsize in zip(levels, contours, colors, fslist): toremove = [] toadd = [] lw = self.get_label_width(lev, fmt, fsize) for segNum, linecontour in enumerate(con._segments): key = contourNum, segNum # for closed contours add one more point to # avoid division by zero if linecontour[0] == linecontour[-1]: linecontour.append(linecontour[1]) # transfer all data points to screen coordinates slc = trans.seq_xy_tups(linecontour) if self.print_label(slc,lw): x,y, rotation, ind = self.locate_label(slc, lw) # transfer the location of the label back to # data coordinates dx,dy = trans.inverse_xy_tup((x,y)) t = Text(dx, dy, rotation = rotation, horizontalalignment='center', verticalalignment='center') self.labeld[key] = t text = self.get_text(lev,fmt) self.set_label_props(t, text, color) self.cl.append(t) new = self.break_linecontour(linecontour, rotation, lw, ind) for c in new: toadd.append(c) toremove.append(linecontour) for c in toremove: con._segments.remove(c) for c in toadd: con._segments.append(c) contourNum += 1 def labels(self, levels, contours, colors, fslist, fmt): trans = self.ax.transData for lev, con, color, fsize in zip(levels, contours, colors, fslist): lw = self.get_label_width(lev, fmt, fsize) for linecontour in con._segments: # for closed contours add one more point if linecontour[0] == linecontour[-1]: linecontour.append(linecontour[1]) # transfer all data points to screen coordinates slc = trans.seq_xy_tups(linecontour) if self.print_label(slc,lw): x,y, rotation, ind = self.locate_label(slc, lw) # transfer the location of the label back into # data coordinates dx,dy = trans.inverse_xy_tup((x,y)) t = Text(dx, dy, rotation = rotation, horizontalalignment='center', verticalalignment='center') text = self.get_text(lev, fmt) self.set_label_props(t, text, color) self.cl.append(t) else: pass
class Legend(Artist): """ Place a legend on the axes at location loc. Labels are a sequence of strings and loc can be a string or an integer specifying the legend location The location codes are 'best' : 0, (only implemented for axis legends) 'upper right' : 1, 'upper left' : 2, 'lower left' : 3, 'lower right' : 4, 'right' : 5, 'center left' : 6, 'center right' : 7, 'lower center' : 8, 'upper center' : 9, 'center' : 10, Return value is a sequence of text, line instances that make up the legend """ codes = {'best' : 0, # only implemented for axis legends 'upper right' : 1, 'upper left' : 2, 'lower left' : 3, 'lower right' : 4, 'right' : 5, 'center left' : 6, 'center right' : 7, 'lower center' : 8, 'upper center' : 9, 'center' : 10, } zorder = 5 def __str__(self): return "Legend" def __init__(self, parent, handles, labels, loc = None, numpoints = None, # the number of points in the legend line prop = None, pad = None, # the fractional whitespace inside the legend border markerscale = None, # the relative size of legend markers vs. original # the following dimensions are in axes coords labelsep = None, # the vertical space between the legend entries handlelen = None, # the length of the legend lines handletextsep = None, # the space between the legend line and legend text axespad = None, # the border between the axes and legend edge shadow = None ): """ parent # the artist that contains the legend handles # a list of artists (lines, patches) to add to the legend labels # a list of strings to label the legend loc # a location code numpoints = 4 # the number of points in the legend line prop = FontProperties(size='smaller') # the font property pad = 0.2 # the fractional whitespace inside the legend border markerscale = 0.6 # the relative size of legend markers vs. original shadow # if True, draw a shadow behind legend The following dimensions are in axes coords labelsep = 0.005 # the vertical space between the legend entries handlelen = 0.05 # the length of the legend lines handletextsep = 0.02 # the space between the legend line and legend text axespad = 0.02 # the border between the axes and legend edge """ from axes import Axes # local import only to avoid circularity from figure import Figure # local import only to avoid circularity Artist.__init__(self) proplist=[numpoints, pad, markerscale, labelsep, handlelen, handletextsep, axespad, shadow] propnames=['numpoints', 'pad', 'markerscale', 'labelsep', 'handlelen', 'handletextsep', 'axespad', 'shadow'] for name, value in safezip(propnames,proplist): if value is None: value=rcParams["legend."+name] setattr(self,name,value) if self.numpoints <= 0: raise ValueError("numpoints must be >= 0; it was %d"% numpoints) if prop is None: self.prop=FontProperties(size=rcParams["legend.fontsize"]) else: self.prop=prop self.fontsize = self.prop.get_size_in_points() if isinstance(parent,Axes): self.isaxes = True self.set_figure(parent.figure) elif isinstance(parent,Figure): self.isaxes = False self.set_figure(parent) else: raise TypeError("Legend needs either Axes or Figure as parent") self.parent = parent self._offsetTransform = Affine2D() self._parentTransform = BboxTransformTo(parent.bbox) Artist.set_transform(self, self._offsetTransform + self._parentTransform) if loc is None: loc = rcParams["legend.loc"] if not self.isaxes and loc in [0,'best']: loc = 'upper right' if is_string_like(loc): if not self.codes.has_key(loc): if self.isaxes: warnings.warn('Unrecognized location "%s". Falling back on "best"; ' 'valid locations are\n\t%s\n' % (loc, '\n\t'.join(self.codes.keys()))) loc = 0 else: warnings.warn('Unrecognized location "%s". Falling back on "upper right"; ' 'valid locations are\n\t%s\n' % (loc, '\n\t'.join(self.codes.keys()))) loc = 1 else: loc = self.codes[loc] if not self.isaxes and loc == 0: warnings.warn('Automatic legend placement (loc="best") not implemented for figure legend. ' 'Falling back on "upper right".') loc = 1 self._loc = loc self.legendPatch = Rectangle( xy=(0.0, 0.0), width=0.5, height=0.5, facecolor='w', edgecolor='k', ) self._set_artist_props(self.legendPatch) # make a trial box in the middle of the axes. relocate it # based on it's bbox left, top = 0.5, 0.5 textleft = left+ self.handlelen+self.handletextsep self.texts = self._get_texts(labels, textleft, top) self.legendHandles = self._get_handles(handles, self.texts) self._drawFrame = True def _set_artist_props(self, a): a.set_figure(self.figure) a.set_transform(self.get_transform()) def _approx_text_height(self): return self.fontsize/72.0*self.figure.dpi/self.parent.bbox.height def draw(self, renderer): if not self.get_visible(): return renderer.open_group('legend') self._update_positions(renderer) if self._drawFrame: if self.shadow: shadow = Shadow(self.legendPatch, -0.005, -0.005) shadow.draw(renderer) self.legendPatch.draw(renderer) if not len(self.legendHandles) and not len(self.texts): return for h in self.legendHandles: if h is not None: h.draw(renderer) if hasattr(h, '_legmarker'): h._legmarker.draw(renderer) if 0: bbox_artist(h, renderer) for t in self.texts: if 0: bbox_artist(t, renderer) t.draw(renderer) renderer.close_group('legend') #draw_bbox(self.save, renderer, 'g') #draw_bbox(self.ibox, renderer, 'r', self.get_transform()) def _get_handle_text_bbox(self, renderer): 'Get a bbox for the text and lines in axes coords' bboxesText = [t.get_window_extent(renderer) for t in self.texts] bboxesHandles = [h.get_window_extent(renderer) for h in self.legendHandles if h is not None] bboxesAll = bboxesText bboxesAll.extend(bboxesHandles) bbox = Bbox.union(bboxesAll) self.save = bbox ibox = bbox.inverse_transformed(self.get_transform()) self.ibox = ibox return ibox def _get_handles(self, handles, texts): handles = list(handles) texts = list(texts) HEIGHT = self._approx_text_height() left = 0.5 ret = [] # the returned legend lines # we need to pad the text with empties for the numpoints=1 # centered marker proxy for handle, label in safezip(handles, texts): if self.numpoints > 1: xdata = np.linspace(left, left + self.handlelen, self.numpoints) xdata_marker = xdata elif self.numpoints == 1: xdata = np.linspace(left, left + self.handlelen, 2) xdata_marker = [left + 0.5*self.handlelen] x, y = label.get_position() x -= self.handlelen + self.handletextsep if isinstance(handle, Line2D): ydata = (y-HEIGHT/2)*np.ones(xdata.shape, float) legline = Line2D(xdata, ydata) legline.update_from(handle) self._set_artist_props(legline) # after update legline.set_clip_box(None) legline.set_clip_path(None) ret.append(legline) legline.set_marker('None') legline_marker = Line2D(xdata_marker, ydata[:len(xdata_marker)]) legline_marker.update_from(handle) legline_marker.set_linestyle('None') self._set_artist_props(legline_marker) # we don't want to add this to the return list because # the texts and handles are assumed to be in one-to-one # correpondence. legline._legmarker = legline_marker elif isinstance(handle, Patch): p = Rectangle(xy=(min(xdata), y-3/4*HEIGHT), width = self.handlelen, height=HEIGHT/2, ) p.update_from(handle) self._set_artist_props(p) p.set_clip_box(None) p.set_clip_path(None) ret.append(p) elif isinstance(handle, LineCollection): ydata = (y-HEIGHT/2)*np.ones(xdata.shape, float) legline = Line2D(xdata, ydata) self._set_artist_props(legline) legline.set_clip_box(None) legline.set_clip_path(None) lw = handle.get_linewidth()[0] dashes = handle.get_dashes()[0] color = handle.get_colors()[0] legline.set_color(color) legline.set_linewidth(lw) legline.set_dashes(dashes) ret.append(legline) elif isinstance(handle, RegularPolyCollection): if self.numpoints == 1: xdata = np.array([left]) p = Rectangle(xy=(min(xdata), y-3/4*HEIGHT), width = self.handlelen, height=HEIGHT/2, ) p.set_facecolor(handle._facecolors[0]) if handle._edgecolors != 'none' and len(handle._edgecolors): p.set_edgecolor(handle._edgecolors[0]) self._set_artist_props(p) p.set_clip_box(None) p.set_clip_path(None) ret.append(p) else: ret.append(None) return ret def _auto_legend_data(self): """ Returns list of vertices and extents covered by the plot. Returns a two long list. First element is a list of (x, y) vertices (in axes-coordinates) covered by all the lines and line collections, in the legend's handles. Second element is a list of bounding boxes for all the patches in the legend's handles. """ assert self.isaxes # should always hold because function is only called internally ax = self.parent vertices = [] bboxes = [] lines = [] inverse_transform = ax.transAxes.inverted() for handle in ax.lines: assert isinstance(handle, Line2D) path = handle.get_path() trans = handle.get_transform() tpath = trans.transform_path(path) apath = inverse_transform.transform_path(tpath) lines.append(apath) for handle in ax.patches: assert isinstance(handle, Patch) if isinstance(handle, Rectangle): transform = handle.get_data_transform() + inverse_transform bboxes.append(handle.get_bbox().transformed(transform)) else: transform = handle.get_transform() + inverse_transform bboxes.append(handle.get_path().get_extents(transform)) return [vertices, bboxes, lines] def draw_frame(self, b): 'b is a boolean. Set draw frame to b' self._drawFrame = b def get_frame(self): 'return the Rectangle instance used to frame the legend' return self.legendPatch def get_lines(self): 'return a list of lines.Line2D instances in the legend' return [h for h in self.legendHandles if isinstance(h, Line2D)] def get_patches(self): 'return a list of patch instances in the legend' return silent_list('Patch', [h for h in self.legendHandles if isinstance(h, Patch)]) def get_texts(self): 'return a list of text.Text instance in the legend' return silent_list('Text', self.texts) def _get_texts(self, labels, left, upper): # height in axes coords HEIGHT = self._approx_text_height() pos = upper x = left ret = [] # the returned list of text instances for l in labels: text = Text( x=x, y=pos, text=l, fontproperties=self.prop, verticalalignment='top', horizontalalignment='left' ) self._set_artist_props(text) ret.append(text) pos -= HEIGHT return ret def get_window_extent(self): return self.legendPatch.get_window_extent() def _offset(self, ox, oy): 'Move all the artists by ox,oy (axes coords)' self._offsetTransform.clear().translate(ox, oy) def _find_best_position(self, width, height, consider=None): """Determine the best location to place the legend. `consider` is a list of (x, y) pairs to consider as a potential lower-left corner of the legend. All are axes coords. """ assert self.isaxes # should always hold because function is only called internally verts, bboxes, lines = self._auto_legend_data() consider = [self._loc_to_axes_coords(x, width, height) for x in range(1, len(self.codes))] tx, ty = self.legendPatch.get_x(), self.legendPatch.get_y() candidates = [] for l, b in consider: legendBox = Bbox.from_bounds(l, b, width, height) badness = 0 badness = legendBox.count_contains(verts) badness += legendBox.count_overlaps(bboxes) for line in lines: if line.intersects_bbox(legendBox): badness += 1 ox, oy = l-tx, b-ty if badness == 0: return ox, oy candidates.append((badness, (ox, oy))) # rather than use min() or list.sort(), do this so that we are assured # that in the case of two equal badnesses, the one first considered is # returned. minCandidate = candidates[0] for candidate in candidates: if candidate[0] < minCandidate[0]: minCandidate = candidate ox, oy = minCandidate[1] return ox, oy def _loc_to_axes_coords(self, loc, width, height): """Convert a location code to axes coordinates. - loc: a location code in range(1, 11). This corresponds to the possible values for self._loc, excluding "best". - width, height: the final size of the legend, axes units. """ assert loc in range(1,11) # called only internally BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11) if loc in (UL, LL, CL): # left x = self.axespad elif loc in (UR, LR, CR, R): # right x = 1.0 - (width + self.axespad) elif loc in (LC, UC, C): # center x x = (0.5 - width/2.0) if loc in (UR, UL, UC): # upper y = 1.0 - (height + self.axespad) elif loc in (LL, LR, LC): # lower y = self.axespad elif loc in (CL, CR, C, R): # center y y = (0.5 - height/2.0) return x,y def _update_positions(self, renderer): # called from renderer to allow more precise estimates of # widths and heights with get_window_extent if not len(self.legendHandles) and not len(self.texts): return def get_tbounds(text): #get text bounds in axes coords bbox = text.get_window_extent(renderer) bboxa = bbox.inverse_transformed(self.get_transform()) return bboxa.bounds hpos = [] for t, tabove in safezip(self.texts[1:], self.texts[:-1]): x,y = t.get_position() l,b,w,h = get_tbounds(tabove) b -= self.labelsep h += 2*self.labelsep hpos.append( (b,h) ) t.set_position( (x, b-0.1*h) ) # now do the same for last line l,b,w,h = get_tbounds(self.texts[-1]) b -= self.labelsep h += 2*self.labelsep hpos.append( (b,h) ) for handle, tup in safezip(self.legendHandles, hpos): y,h = tup if isinstance(handle, Line2D): ydata = y*np.ones(handle.get_xdata().shape, float) handle.set_ydata(ydata+h/2.) handle._legmarker.set_ydata(ydata+h/2.) elif isinstance(handle, Rectangle): handle.set_y(y+1/4*h) handle.set_height(h/2) # Set the data for the legend patch bbox = self._get_handle_text_bbox(renderer) bbox = bbox.expanded(1 + self.pad, 1 + self.pad) l, b, w, h = bbox.bounds self.legendPatch.set_bounds(l, b, w, h) ox, oy = 0, 0 # center if iterable(self._loc) and len(self._loc)==2: xo = self.legendPatch.get_x() yo = self.legendPatch.get_y() x, y = self._loc ox, oy = x-xo, y-yo elif self._loc == 0: # "best" ox, oy = self._find_best_position(w, h) else: x, y = self._loc_to_axes_coords(self._loc, w, h) ox, oy = x-l, y-b self._offset(ox, oy)
class ContourLabeler: '''Mixin to provide labelling capability to ContourSet''' def clabel(self, *args, **kwargs): """ clabel(CS, **kwargs) - add labels to line contours in CS, where CS is a ContourSet object returned by contour. clabel(CS, V, **kwargs) - only label contours listed in V keyword arguments: * fontsize = None: as described in http://matplotlib.sf.net/fonts.html * colors = None: - a tuple of matplotlib color args (string, float, rgb, etc), different labels will be plotted in different colors in the order specified - one string color, e.g. colors = 'r' or colors = 'red', all labels will be plotted in this color - if colors == None, the color of each label matches the color of the corresponding contour * inline = True: controls whether the underlying contour is removed (inline = True) or not (False) * fmt = '%1.3f': a format string for the label """ fontsize = kwargs.get('fontsize', None) inline = kwargs.get('inline', 1) self.fmt = kwargs.get('fmt', '%1.3f') colors = kwargs.get('colors', None) if len(args) == 0: levels = self.levels indices = range(len(self.levels)) elif len(args) == 1: levlabs = list(args[0]) indices, levels = [], [] for i, lev in enumerate(self.levels): if lev in levlabs: indices.append(i) levels.append(lev) if len(levels) < len(levlabs): msg = "Specified levels " + str(levlabs) msg += "\n don't match available levels " msg += str(self.levels) raise ValueError(msg) else: raise TypeError("Illegal arguments to clabel, see help(clabel)") self.label_levels = levels self.label_indices = indices self.fp = FontProperties() if fontsize == None: font_size = int(self.fp.get_size_in_points()) else: if type(fontsize) not in [int, float, str]: raise TypeError("Font size must be an integer number.") # Can't it be floating point, as indicated in line above? else: if type(fontsize) == str: font_size = int(self.fp.get_size_in_points()) else: self.fp.set_size(fontsize) font_size = fontsize self.fslist = [font_size] * len(levels) if colors == None: self.label_mappable = self self.label_cvalues = take(self.cvalues, self.label_indices) else: cmap = ListedColormap(colors, N=len(self.label_levels)) self.label_cvalues = range(len(self.label_levels)) self.label_mappable = ScalarMappable(cmap=cmap, norm=no_norm()) #self.cl = [] # Initialized in ContourSet.__init__ #self.cl_cvalues = [] # same self.cl_xy = [] self.labels(inline) for label in self.cl: self.ax.add_artist(label) self.label_list = silent_list('Text', self.cl) return self.label_list def print_label(self, linecontour, labelwidth): "if contours are too short, don't plot a label" lcsize = len(linecontour) if lcsize > 10 * labelwidth: return 1 xmax = amax(array(linecontour)[:, 0]) xmin = amin(array(linecontour)[:, 0]) ymax = amax(array(linecontour)[:, 1]) ymin = amin(array(linecontour)[:, 1]) lw = labelwidth if (xmax - xmin) > 1.2 * lw or (ymax - ymin) > 1.2 * lw: return 1 else: return 0 def too_close(self, x, y, lw): "if there's a label already nearby, find a better place" if self.cl_xy != []: dist = [ sqrt((x - loc[0])**2 + (y - loc[1])**2) for loc in self.cl_xy ] for d in dist: if d < 1.2 * lw: return 1 else: return 0 else: return 0 def get_label_coords(self, distances, XX, YY, ysize, lw): """ labels are ploted at a location with the smallest dispersion of the contour from a straight line unless there's another label nearby, in which case the second best place on the contour is picked up if there's no good place a label isplotted at the beginning of the contour """ hysize = int(ysize / 2) adist = argsort(distances) for ind in adist: x, y = XX[ind][hysize], YY[ind][hysize] if self.too_close(x, y, lw): continue else: self.cl_xy.append((x, y)) return x, y, ind ind = adist[0] x, y = XX[ind][hysize], YY[ind][hysize] self.cl_xy.append((x, y)) return x, y, ind def get_label_width(self, lev, fmt, fsize): "get the width of the label in points" if is_string_like(lev): lw = (len(lev)) * fsize else: lw = (len(fmt % lev)) * fsize return lw def set_label_props(self, label, text, color): "set the label properties - color, fontsize, text" label.set_text(text) label.set_color(color) label.set_fontproperties(self.fp) label.set_clip_box(self.ax.bbox) def get_text(self, lev, fmt): "get the text of the label" if is_string_like(lev): return lev else: return fmt % lev def break_linecontour(self, linecontour, rot, labelwidth, ind): "break a contour in two contours at the location of the label" lcsize = len(linecontour) hlw = int(labelwidth / 2) #length of label in screen coords ylabel = abs(hlw * sin(rot * pi / 180)) xlabel = abs(hlw * cos(rot * pi / 180)) trans = self.ax.transData slc = trans.seq_xy_tups(linecontour) x, y = slc[ind] xx = asarray(slc)[:, 0].copy() yy = asarray(slc)[:, 1].copy() #indices which are under the label inds = nonzero(((xx < x + xlabel) & (xx > x - xlabel)) & ((yy < y + ylabel) & (yy > y - ylabel))) if len(inds) > 0: #if the label happens to be over the beginning of the #contour, the entire contour is removed, i.e. #indices to be removed are #inds= [0,1,2,3,305,306,307] #should rewrite this in a better way linds = nonzero(inds[1:] - inds[:-1] != 1) if inds[0] == 0 and len(linds) != 0: ii = inds[linds[0]] lc1 = linecontour[ii + 1:inds[ii + 1]] lc2 = [] else: lc1 = linecontour[:inds[0]] lc2 = linecontour[inds[-1] + 1:] else: lc1 = linecontour[:ind] lc2 = linecontour[ind + 1:] if rot < 0: new_x1, new_y1 = x - xlabel, y + ylabel new_x2, new_y2 = x + xlabel, y - ylabel else: new_x1, new_y1 = x - xlabel, y - ylabel new_x2, new_y2 = x + xlabel, y + ylabel new_x1d, new_y1d = trans.inverse_xy_tup((new_x1, new_y1)) new_x2d, new_y2d = trans.inverse_xy_tup((new_x2, new_y2)) new_xy1 = array(((new_x1d, new_y1d), )) new_xy2 = array(((new_x2d, new_y2d), )) if rot > 0: if (len(lc1) > 0 and (lc1[-1][0] <= new_x1d) and (lc1[-1][1] <= new_y1d)): lc1 = concatenate((lc1, new_xy1)) #lc1.append((new_x1d, new_y1d)) if (len(lc2) > 0 and (lc2[0][0] >= new_x2d) and (lc2[0][1] >= new_y2d)): lc2 = concatenate((new_xy2, lc2)) #lc2.insert(0, (new_x2d, new_y2d)) else: if (len(lc1) > 0 and ((lc1[-1][0] <= new_x1d) and (lc1[-1][1] >= new_y1d))): lc1 = concatenate((lc1, new_xy1)) #lc1.append((new_x1d, new_y1d)) if (len(lc2) > 0 and ((lc2[0][0] >= new_x2d) and (lc2[0][1] <= new_y2d))): lc2 = concatenate((new_xy2, lc2)) #lc2.insert(0, (new_x2d, new_y2d)) return [lc1, lc2] def locate_label(self, linecontour, labelwidth): """find a good place to plot a label (relatively flat part of the contour) and the angle of rotation for the text object """ nsize = len(linecontour) if labelwidth > 1: xsize = int(ceil(nsize / labelwidth)) else: xsize = 1 if xsize == 1: ysize = nsize else: ysize = labelwidth XX = resize(asarray(linecontour)[:, 0], (xsize, ysize)) YY = resize(asarray(linecontour)[:, 1], (xsize, ysize)) yfirst = YY[:, 0] ylast = YY[:, -1] xfirst = XX[:, 0] xlast = XX[:, -1] s = ((reshape(yfirst, (xsize, 1)) - YY) * (reshape(xlast, (xsize, 1)) - reshape(xfirst, (xsize, 1))) - (reshape(xfirst, (xsize, 1)) - XX) * (reshape(ylast, (xsize, 1)) - reshape(yfirst, (xsize, 1)))) L = sqrt((xlast - xfirst)**2 + (ylast - yfirst)**2) dist = add.reduce(([(abs(s)[i] / L[i]) for i in range(xsize)]), -1) x, y, ind = self.get_label_coords(dist, XX, YY, ysize, labelwidth) #print 'ind, x, y', ind, x, y angle = arctan2(ylast - yfirst, xlast - xfirst) rotation = angle[ind] * 180 / pi if rotation > 90: rotation = rotation - 180 if rotation < -90: rotation = 180 + rotation # There must be a more efficient way... lc = [tuple(l) for l in linecontour] dind = lc.index((x, y)) #print 'dind', dind #dind = list(linecontour).index((x,y)) return x, y, rotation, dind def labels(self, inline): levels = self.label_levels fslist = self.fslist trans = self.ax.transData colors = self.label_mappable.to_rgba(self.label_cvalues) fmt = self.fmt for icon, lev, color, cvalue, fsize in zip(self.label_indices, self.label_levels, colors, self.label_cvalues, fslist): con = self.collections[icon] lw = self.get_label_width(lev, fmt, fsize) additions = [] for segNum, linecontour in enumerate(con._segments): # for closed contours add one more point to # avoid division by zero if all(linecontour[0] == linecontour[-1]): linecontour = concatenate( (linecontour, linecontour[1][newaxis, :])) #linecontour.append(linecontour[1]) # transfer all data points to screen coordinates slc = trans.seq_xy_tups(linecontour) if self.print_label(slc, lw): x, y, rotation, ind = self.locate_label(slc, lw) # transfer the location of the label back to # data coordinates dx, dy = trans.inverse_xy_tup((x, y)) t = Text(dx, dy, rotation=rotation, horizontalalignment='center', verticalalignment='center') text = self.get_text(lev, fmt) self.set_label_props(t, text, color) self.cl.append(t) self.cl_cvalues.append(cvalue) if inline: new = self.break_linecontour(linecontour, rotation, lw, ind) con._segments[segNum] = new[0] additions.append(new[1]) con._segments.extend(additions)
def clabel(self, *args, **kwargs): """ CLABEL(*args, **kwargs) Function signatures CLABEL(C) - plots contour labels, C is the output of contour or a list of contours CLABEL(C,V) - creates labels only for those contours, given in a list V CLABEL(C, **kwargs) - keyword args are explained below: * fontsize = None: as described in http://matplotlib.sf.net/fonts.html * colors = None: - a tuple of matplotlib color args (string, float, rgb, etc), different labels will be plotted in different colors in the order specified - one string color, e.g. colors = 'r' or colors = 'red', all labels will be plotted in this color - if colors == None, the color of each label matches the color of the corresponding contour * inline = 0: controls whether the underlying contour is removed (inline = 1) or not * fmt = '%1.3f': a format string for the label """ # todo, factor this out to a separate class and don't use hidden coll attrs if not self.ax.ishold(): self.ax.cla() fontsize = kwargs.get('fontsize', None) inline = kwargs.get('inline', 0) fmt = kwargs.get('fmt', '%1.3f') colors = kwargs.get('colors', None) if len(args) == 1: contours = args[0] levels = [con._label for con in contours] elif len(args) == 2: contours = args[0] levels = args[1] else: raise TypeError("Illegal arguments to clabel, see help(clabel)") self.fp = FontProperties() if fontsize == None: font_size = int(self.fp.get_size_in_points()) else: if type(fontsize) not in [int, float, str]: raise TypeError("Font size must be an integer number.") else: if type(fontsize) == str: font_size = int(self.fp.get_size_in_points()) else: self.fp.set_size(fontsize) font_size = fontsize fslist = [font_size] * len(levels) if colors == None: colors = [c._colors[0] for c in contours] else: colors = colors * len(contours) if inline not in [0,1]: raise TypeError("inline must be 0 or 1") self.cl = [] self.cl_xy = [] # we have a list of contours and each contour has a list of # segments. We want changes in the contour color to be # reflected in changes in the label color. This is a good use # for traits observers, but in the interim, until traits are # utilized, we'll create a dict mapping i,j to text instances. # i is the contour level index, j is the sement index self.labeld = {} if inline == 1: self.inline_labels(levels, contours, colors, fslist, fmt) else: self.labels(levels, contours, colors, fslist, fmt) for label in self.cl: self.ax.add_artist(label) ret = silent_list('Text', self.cl) ret.mappable = getattr(contours, 'mappable', None) # support colormapping for label if ret.mappable is not None: ret.mappable.labeld = self.labeld return ret
class Legend(Artist): """ Place a legend on the axes at location loc. Labels are a sequence of strings and loc can be a string or an integer specifying the legend location The location codes are 'best' : 0, 'upper right' : 1, (default) 'upper left' : 2, 'lower left' : 3, 'lower right' : 4, 'right' : 5, 'center left' : 6, 'center right' : 7, 'lower center' : 8, 'upper center' : 9, 'center' : 10, Return value is a sequence of text, line instances that make up the legend """ codes = {'best' : 0, 'upper right' : 1, # default 'upper left' : 2, 'lower left' : 3, 'lower right' : 4, 'right' : 5, 'center left' : 6, 'center right' : 7, 'lower center' : 8, 'upper center' : 9, 'center' : 10, } def __init__(self, parent, handles, labels, loc, isaxes= None, numpoints = None, # the number of points in the legend line prop = None, pad = None, # the fractional whitespace inside the legend border markerscale = None, # the relative size of legend markers vs. original # the following dimensions are in axes coords labelsep = None, # the vertical space between the legend entries handlelen = None, # the length of the legend lines handletextsep = None, # the space between the legend line and legend text axespad = None, # the border between the axes and legend edge shadow= None, ): """ parent # the artist that contains the legend handles # a list of artists (lines, patches) to add to the legend labels # a list of strings to label the legend loc # a location code isaxes=True # whether this is an axes legend numpoints = 4 # the number of points in the legend line fontprop = FontProperties(size='smaller') # the font property pad = 0.2 # the fractional whitespace inside the legend border markerscale = 0.6 # the relative size of legend markers vs. original shadow # if True, draw a shadow behind legend The following dimensions are in axes coords labelsep = 0.005 # the vertical space between the legend entries handlelen = 0.05 # the length of the legend lines handletextsep = 0.02 # the space between the legend line and legend text axespad = 0.02 # the border between the axes and legend edge """ Artist.__init__(self) if is_string_like(loc) and not self.codes.has_key(loc): warnings.warn('Unrecognized location %s. Falling back on upper right; valid locations are\n%s\t' %(loc, '\n\t'.join(self.codes.keys()))) if is_string_like(loc): loc = self.codes.get(loc, 1) proplist=[numpoints, pad, markerscale, labelsep, handlelen, handletextsep, axespad, shadow, isaxes] propnames=['numpoints', 'pad', 'markerscale', 'labelsep', 'handlelen', 'handletextsep', 'axespad', 'shadow', 'isaxes'] for name, value in zip(propnames,proplist): if value is None: value=rcParams["legend."+name] setattr(self,name,value) if prop is None: self.prop=FontProperties(size=rcParams["legend.fontsize"]) else: self.prop=prop self.fontsize = self.prop.get_size_in_points() if self.isaxes: # parent is an Axes self.set_figure(parent.figure) else: # parent is a Figure self.set_figure(parent) self.parent = parent self.set_transform( get_bbox_transform( unit_bbox(), parent.bbox) ) self._loc = loc # make a trial box in the middle of the axes. relocate it # based on it's bbox left, top = 0.5, 0.5 if self.numpoints == 1: self._xdata = array([left + self.handlelen*0.5]) else: self._xdata = linspace(left, left + self.handlelen, self.numpoints) textleft = left+ self.handlelen+self.handletextsep self.texts = self._get_texts(labels, textleft, top) self.legendHandles = self._get_handles(handles, self.texts) if len(self.texts): left, top = self.texts[-1].get_position() HEIGHT = self._approx_text_height()*len(self.texts) else: HEIGHT = 0.2 bottom = top-HEIGHT left -= self.handlelen + self.handletextsep + self.pad self.legendPatch = Rectangle( xy=(left, bottom), width=0.5, height=HEIGHT, facecolor='w', edgecolor='k', ) self._set_artist_props(self.legendPatch) self._drawFrame = True def _set_artist_props(self, a): a.set_figure(self.figure) a.set_transform(self._transform) def _approx_text_height(self): return self.fontsize/72.0*self.figure.dpi.get()/self.parent.bbox.height() def draw(self, renderer): if not self.get_visible(): return renderer.open_group('legend') self._update_positions(renderer) if self._drawFrame: if self.shadow: shadow = Shadow(self.legendPatch, -0.005, -0.005) shadow.draw(renderer) self.legendPatch.draw(renderer) if not len(self.legendHandles) and not len(self.texts): return for h in self.legendHandles: if h is not None: h.draw(renderer) if 0: bbox_artist(h, renderer) for t in self.texts: if 0: bbox_artist(t, renderer) t.draw(renderer) renderer.close_group('legend') #draw_bbox(self.save, renderer, 'g') #draw_bbox(self.ibox, renderer, 'r', self._transform) def _get_handle_text_bbox(self, renderer): 'Get a bbox for the text and lines in axes coords' bboxesText = [t.get_window_extent(renderer) for t in self.texts] bboxesHandles = [h.get_window_extent(renderer) for h in self.legendHandles if h is not None] bboxesAll = bboxesText bboxesAll.extend(bboxesHandles) bbox = bbox_all(bboxesAll) self.save = bbox ibox = inverse_transform_bbox(self._transform, bbox) self.ibox = ibox return ibox def _get_handles(self, handles, texts): HEIGHT = self._approx_text_height() ret = [] # the returned legend lines for handle, label in zip(handles, texts): x, y = label.get_position() x -= self.handlelen + self.handletextsep if isinstance(handle, Line2D): ydata = (y-HEIGHT/2)*ones(self._xdata.shape, Float) legline = Line2D(self._xdata, ydata) legline.update_from(handle) self._set_artist_props(legline) # after update legline.set_clip_box(None) legline.set_markersize(self.markerscale*legline.get_markersize()) ret.append(legline) elif isinstance(handle, Patch): p = Rectangle(xy=(min(self._xdata), y-3/4*HEIGHT), width = self.handlelen, height=HEIGHT/2, ) p.update_from(handle) self._set_artist_props(p) p.set_clip_box(None) ret.append(p) elif isinstance(handle, LineCollection): ydata = (y-HEIGHT/2)*ones(self._xdata.shape, Float) legline = Line2D(self._xdata, ydata) self._set_artist_props(legline) legline.set_clip_box(None) lw = handle.get_linewidth()[0] dashes = handle.get_dashes() color = handle.get_colors()[0] legline.set_color(color) legline.set_linewidth(lw) legline.set_dashes(dashes) ret.append(legline) elif isinstance(handle, RegularPolyCollection): p = Rectangle(xy=(min(self._xdata), y-3/4*HEIGHT), width = self.handlelen, height=HEIGHT/2, ) p.set_facecolor(handle._facecolors[0]) if handle._edgecolors != 'None': p.set_edgecolor(handle._edgecolors[0]) self._set_artist_props(p) p.set_clip_box(None) ret.append(p) else: ret.append(None) return ret def _auto_legend_data(self): """ Returns list of vertices and extents covered by the plot. Returns a two long list. First element is a list of (x, y) vertices (in axes-coordinates) covered by all the lines and line collections, in the legend's handles. Second element is a list of bounding boxes for all the patches in the legend's handles. """ if not self.isaxes: raise Exception, 'Auto legends not available for figure legends.' def get_handles(ax): handles = ax.lines handles.extend(ax.patches) handles.extend([c for c in ax.collections if isinstance(c, LineCollection)]) return handles ax = self.parent handles = get_handles(ax) vertices = [] bboxes = [] lines = [] inv = ax.transAxes.inverse_xy_tup for handle in handles: if isinstance(handle, Line2D): xdata = handle.get_xdata(valid_only = True) ydata = handle.get_ydata(valid_only = True) trans = handle.get_transform() xt, yt = trans.numerix_x_y(xdata, ydata) # XXX need a special method in transform to do a list of verts averts = [inv(v) for v in zip(xt, yt)] lines.append(averts) elif isinstance(handle, Patch): verts = handle.get_verts() trans = handle.get_transform() tverts = trans.seq_xy_tups(verts) averts = [inv(v) for v in tverts] bbox = unit_bbox() bbox.update(averts, True) bboxes.append(bbox) elif isinstance(handle, LineCollection): hlines = handle.get_lines() trans = handle.get_transform() for line in hlines: tline = trans.seq_xy_tups(line) aline = [inv(v) for v in tline] lines.extend(line) return [vertices, bboxes, lines] def draw_frame(self, b): 'b is a boolean. Set draw frame to b' self._drawFrame = b def get_frame(self): 'return the Rectangle instance used to frame the legend' return self.legendPatch def get_lines(self): 'return a list of lines.Line2D instances in the legend' return [h for h in self.legendHandles if isinstance(h, Line2D)] def get_patches(self): 'return a list of patch instances in the legend' return silent_list('Patch', [h for h in self.legendHandles if isinstance(h, Patch)]) def get_texts(self): 'return a list of text.Text instance in the legend' return silent_list('Text', self.texts) def _get_texts(self, labels, left, upper): # height in axes coords HEIGHT = self._approx_text_height() pos = upper x = left ret = [] # the returned list of text instances for l in labels: text = Text( x=x, y=pos, text=l, fontproperties=self.prop, verticalalignment='top', horizontalalignment='left', ) self._set_artist_props(text) ret.append(text) pos -= HEIGHT return ret def get_window_extent(self): return self.legendPatch.get_window_extent() def _offset(self, ox, oy): 'Move all the artists by ox,oy (axes coords)' for t in self.texts: x,y = t.get_position() t.set_position( (x+ox, y+oy) ) for h in self.legendHandles: if isinstance(h, Line2D): x,y = h.get_xdata(valid_only = True), h.get_ydata(valid_only = True) h.set_data( x+ox, y+oy) elif isinstance(h, Rectangle): h.xy[0] = h.xy[0] + ox h.xy[1] = h.xy[1] + oy elif isinstance(h, RegularPolygon): h.verts = [(x + ox, y + oy) for x, y in h.verts] x, y = self.legendPatch.get_x(), self.legendPatch.get_y() self.legendPatch.set_x(x+ox) self.legendPatch.set_y(y+oy) def _find_best_position(self, width, height, consider=None): """Determine the best location to place the legend. `consider` is a list of (x, y) pairs to consider as a potential lower-left corner of the legend. All are axes coords. """ verts, bboxes, lines = self._auto_legend_data() consider = [self._loc_to_axes_coords(x, width, height) for x in range(1, len(self.codes))] tx, ty = self.legendPatch.xy candidates = [] for l, b in consider: legendBox = lbwh_to_bbox(l, b, width, height) badness = 0 badness = legendBox.count_contains(verts) ox, oy = l-tx, b-ty for bbox in bboxes: if legendBox.overlaps(bbox): badness += 1 for line in lines: if line_cuts_bbox(line, legendBox): badness += 1 if badness == 0: return ox, oy candidates.append((badness, (ox, oy))) # rather than use min() or list.sort(), do this so that we are assured # that in the case of two equal badnesses, the one first considered is # returned. minCandidate = candidates[0] for candidate in candidates: if candidate[0] < minCandidate[0]: minCandidate = candidate ox, oy = minCandidate[1] return ox, oy def _loc_to_axes_coords(self, loc, width, height): """Convert a location code to axes coordinates. - loc: a location code, which may be a pair of literal axes coords, or in range(1, 11). This coresponds to the possible values for self._loc, excluding "best". - width, height: the final size of the legend, axes units. """ BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11) left = self.axespad right = 1.0 - (self.axespad + width) upper = 1.0 - (self.axespad + height) lower = self.axespad centerx = 0.5 - (width/2.0) centery = 0.5 - (height/2.0) if loc == UR: return right, upper if loc == UL: return left, upper if loc == LL: return left, lower if loc == LR: return right, lower if loc == CL: return left, centery if loc in (CR, R): return right, centery if loc == LC: return centerx, lower if loc == UC: return centerx, upper if loc == C: return centerx, centery raise TypeError, "%r isn't an understood type code." % (loc,) def _update_positions(self, renderer): # called from renderer to allow more precise estimates of # widths and heights with get_window_extent if not len(self.legendHandles) and not len(self.texts): return def get_tbounds(text): #get text bounds in axes coords bbox = text.get_window_extent(renderer) bboxa = inverse_transform_bbox(self._transform, bbox) return bboxa.get_bounds() hpos = [] for t, tabove in zip(self.texts[1:], self.texts[:-1]): x,y = t.get_position() l,b,w,h = get_tbounds(tabove) b -= self.labelsep h += 2*self.labelsep hpos.append( (b,h) ) t.set_position( (x, b-0.1*h) ) # now do the same for last line l,b,w,h = get_tbounds(self.texts[-1]) b -= self.labelsep h += 2*self.labelsep hpos.append( (b,h) ) for handle, tup in zip(self.legendHandles, hpos): y,h = tup if isinstance(handle, Line2D): ydata = y*ones(self._xdata.shape, Float) handle.set_ydata(ydata+h/2) elif isinstance(handle, Rectangle): handle.set_y(y+1/4*h) handle.set_height(h/2) # Set the data for the legend patch bbox = self._get_handle_text_bbox(renderer).deepcopy() bbox.scale(1 + self.pad, 1 + self.pad) l,b,w,h = bbox.get_bounds() self.legendPatch.set_bounds(l,b,w,h) BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11) ox, oy = 0, 0 # center if iterable(self._loc) and len(self._loc)==2: xo = self.legendPatch.get_x() yo = self.legendPatch.get_y() x, y = self._loc ox = x-xo oy = y-yo self._offset(ox, oy) else: if self._loc in (BEST,): ox, oy = self._find_best_position(w, h) if self._loc in (UL, LL, CL): # left ox = self.axespad - l if self._loc in (UR, LR, R, CR): # right ox = 1 - (l + w + self.axespad) if self._loc in (UR, UL, UC): # upper oy = 1 - (b + h + self.axespad) if self._loc in (LL, LR, LC): # lower oy = self.axespad - b if self._loc in (LC, UC, C): # center x ox = (0.5-w/2)-l if self._loc in (CL, CR, C): # center y oy = (0.5-h/2)-b self._offset(ox, oy)
def __init__(self, parent, handles, labels, loc, isaxes= None, numpoints = None, # the number of points in the legend line prop = None, pad = None, # the fractional whitespace inside the legend border markerscale = None, # the relative size of legend markers vs. original # the following dimensions are in axes coords labelsep = None, # the vertical space between the legend entries handlelen = None, # the length of the legend lines handletextsep = None, # the space between the legend line and legend text axespad = None, # the border between the axes and legend edge shadow= None, ): """ parent # the artist that contains the legend handles # a list of artists (lines, patches) to add to the legend labels # a list of strings to label the legend loc # a location code isaxes=True # whether this is an axes legend numpoints = 4 # the number of points in the legend line fontprop = FontProperties(size='smaller') # the font property pad = 0.2 # the fractional whitespace inside the legend border markerscale = 0.6 # the relative size of legend markers vs. original shadow # if True, draw a shadow behind legend The following dimensions are in axes coords labelsep = 0.005 # the vertical space between the legend entries handlelen = 0.05 # the length of the legend lines handletextsep = 0.02 # the space between the legend line and legend text axespad = 0.02 # the border between the axes and legend edge """ Artist.__init__(self) if is_string_like(loc) and not self.codes.has_key(loc): warnings.warn('Unrecognized location %s. Falling back on upper right; valid locations are\n%s\t' %(loc, '\n\t'.join(self.codes.keys()))) if is_string_like(loc): loc = self.codes.get(loc, 1) proplist=[numpoints, pad, markerscale, labelsep, handlelen, handletextsep, axespad, shadow, isaxes] propnames=['numpoints', 'pad', 'markerscale', 'labelsep', 'handlelen', 'handletextsep', 'axespad', 'shadow', 'isaxes'] for name, value in zip(propnames,proplist): if value is None: value=rcParams["legend."+name] setattr(self,name,value) if prop is None: self.prop=FontProperties(size=rcParams["legend.fontsize"]) else: self.prop=prop self.fontsize = self.prop.get_size_in_points() if self.isaxes: # parent is an Axes self.set_figure(parent.figure) else: # parent is a Figure self.set_figure(parent) self.parent = parent self.set_transform( get_bbox_transform( unit_bbox(), parent.bbox) ) self._loc = loc # make a trial box in the middle of the axes. relocate it # based on it's bbox left, top = 0.5, 0.5 if self.numpoints == 1: self._xdata = array([left + self.handlelen*0.5]) else: self._xdata = linspace(left, left + self.handlelen, self.numpoints) textleft = left+ self.handlelen+self.handletextsep self.texts = self._get_texts(labels, textleft, top) self.legendHandles = self._get_handles(handles, self.texts) if len(self.texts): left, top = self.texts[-1].get_position() HEIGHT = self._approx_text_height()*len(self.texts) else: HEIGHT = 0.2 bottom = top-HEIGHT left -= self.handlelen + self.handletextsep + self.pad self.legendPatch = Rectangle( xy=(left, bottom), width=0.5, height=HEIGHT, facecolor='w', edgecolor='k', ) self._set_artist_props(self.legendPatch) self._drawFrame = True
def clabel(self, *args, **kwargs): """ clabel(CS, **kwargs) - add labels to line contours in CS, where CS is a ContourSet object returned by contour. clabel(CS, V, **kwargs) - only label contours listed in V keyword arguments: * fontsize = None: as described in http://matplotlib.sf.net/fonts.html * colors = None: - a tuple of matplotlib color args (string, float, rgb, etc), different labels will be plotted in different colors in the order specified - one string color, e.g. colors = 'r' or colors = 'red', all labels will be plotted in this color - if colors == None, the color of each label matches the color of the corresponding contour * inline = True: controls whether the underlying contour is removed (inline = True) or not (False) * fmt = '%1.3f': a format string for the label """ fontsize = kwargs.get('fontsize', None) inline = kwargs.get('inline', 1) self.fmt = kwargs.get('fmt', '%1.3f') colors = kwargs.get('colors', None) if len(args) == 0: levels = self.levels indices = range(len(self.levels)) elif len(args) == 1: levlabs = list(args[0]) indices, levels = [], [] for i, lev in enumerate(self.levels): if lev in levlabs: indices.append(i) levels.append(lev) if len(levels) < len(levlabs): msg = "Specified levels " + str(levlabs) msg += "\n don't match available levels " msg += str(self.levels) raise ValueError(msg) else: raise TypeError("Illegal arguments to clabel, see help(clabel)") self.label_levels = levels self.label_indices = indices self.fp = FontProperties() if fontsize == None: font_size = int(self.fp.get_size_in_points()) else: if type(fontsize) not in [int, float, str]: raise TypeError("Font size must be an integer number.") # Can't it be floating point, as indicated in line above? else: if type(fontsize) == str: font_size = int(self.fp.get_size_in_points()) else: self.fp.set_size(fontsize) font_size = fontsize self.fslist = [font_size] * len(levels) if colors == None: self.label_mappable = self self.label_cvalues = take(self.cvalues, self.label_indices) else: cmap = ListedColormap(colors, N=len(self.label_levels)) self.label_cvalues = range(len(self.label_levels)) self.label_mappable = ScalarMappable(cmap = cmap, norm = no_norm()) #self.cl = [] # Initialized in ContourSet.__init__ #self.cl_cvalues = [] # same self.cl_xy = [] self.labels(inline) for label in self.cl: self.ax.add_artist(label) self.label_list = silent_list('Text', self.cl) return self.label_list
class ContourLabeler: '''Mixin to provide labelling capability to ContourSet''' def clabel(self, *args, **kwargs): """ clabel(CS, **kwargs) - add labels to line contours in CS, where CS is a ContourSet object returned by contour. clabel(CS, V, **kwargs) - only label contours listed in V keyword arguments: * fontsize = None: as described in http://matplotlib.sf.net/fonts.html * colors = None: - a tuple of matplotlib color args (string, float, rgb, etc), different labels will be plotted in different colors in the order specified - one string color, e.g. colors = 'r' or colors = 'red', all labels will be plotted in this color - if colors == None, the color of each label matches the color of the corresponding contour * inline = True: controls whether the underlying contour is removed (inline = True) or not (False) * fmt = '%1.3f': a format string for the label """ fontsize = kwargs.get('fontsize', None) inline = kwargs.get('inline', 1) self.fmt = kwargs.get('fmt', '%1.3f') colors = kwargs.get('colors', None) if len(args) == 0: levels = self.levels indices = range(len(self.levels)) elif len(args) == 1: levlabs = list(args[0]) indices, levels = [], [] for i, lev in enumerate(self.levels): if lev in levlabs: indices.append(i) levels.append(lev) if len(levels) < len(levlabs): msg = "Specified levels " + str(levlabs) msg += "\n don't match available levels " msg += str(self.levels) raise ValueError(msg) else: raise TypeError("Illegal arguments to clabel, see help(clabel)") self.label_levels = levels self.label_indices = indices self.fp = FontProperties() if fontsize == None: font_size = int(self.fp.get_size_in_points()) else: if type(fontsize) not in [int, float, str]: raise TypeError("Font size must be an integer number.") # Can't it be floating point, as indicated in line above? else: if type(fontsize) == str: font_size = int(self.fp.get_size_in_points()) else: self.fp.set_size(fontsize) font_size = fontsize self.fslist = [font_size] * len(levels) if colors == None: self.label_mappable = self self.label_cvalues = take(self.cvalues, self.label_indices) else: cmap = ListedColormap(colors, N=len(self.label_levels)) self.label_cvalues = range(len(self.label_levels)) self.label_mappable = ScalarMappable(cmap = cmap, norm = no_norm()) #self.cl = [] # Initialized in ContourSet.__init__ #self.cl_cvalues = [] # same self.cl_xy = [] self.labels(inline) for label in self.cl: self.ax.add_artist(label) self.label_list = silent_list('Text', self.cl) return self.label_list def print_label(self, linecontour,labelwidth): "if contours are too short, don't plot a label" lcsize = len(linecontour) if lcsize > 10 * labelwidth: return 1 xmax = amax(array(linecontour)[:,0]) xmin = amin(array(linecontour)[:,0]) ymax = amax(array(linecontour)[:,1]) ymin = amin(array(linecontour)[:,1]) lw = labelwidth if (xmax - xmin) > 1.2* lw or (ymax - ymin) > 1.2 * lw: return 1 else: return 0 def too_close(self, x,y, lw): "if there's a label already nearby, find a better place" if self.cl_xy != []: dist = [sqrt((x-loc[0]) ** 2 + (y-loc[1]) ** 2) for loc in self.cl_xy] for d in dist: if d < 1.2*lw: return 1 else: return 0 else: return 0 def get_label_coords(self, distances, XX, YY, ysize, lw): """ labels are ploted at a location with the smallest dispersion of the contour from a straight line unless there's another label nearby, in which case the second best place on the contour is picked up if there's no good place a label isplotted at the beginning of the contour """ hysize = int(ysize/2) adist = argsort(distances) for ind in adist: x, y = XX[ind][hysize], YY[ind][hysize] if self.too_close(x,y, lw): continue else: self.cl_xy.append((x,y)) return x,y, ind ind = adist[0] x, y = XX[ind][hysize], YY[ind][hysize] self.cl_xy.append((x,y)) return x,y, ind def get_label_width(self, lev, fmt, fsize): "get the width of the label in points" if is_string_like(lev): lw = (len(lev)) * fsize else: lw = (len(fmt%lev)) * fsize return lw def set_label_props(self, label, text, color): "set the label properties - color, fontsize, text" label.set_text(text) label.set_color(color) label.set_fontproperties(self.fp) label.set_clip_box(self.ax.bbox) def get_text(self, lev, fmt): "get the text of the label" if is_string_like(lev): return lev else: return fmt%lev def break_linecontour(self, linecontour, rot, labelwidth, ind): "break a contour in two contours at the location of the label" lcsize = len(linecontour) hlw = int(labelwidth/2) #length of label in screen coords ylabel = abs(hlw * sin(rot*pi/180)) xlabel = abs(hlw * cos(rot*pi/180)) trans = self.ax.transData slc = trans.seq_xy_tups(linecontour) x,y = slc[ind] xx= array(slc)[:,0].copy() yy=array(slc)[:,1].copy() #indices which are under the label inds=nonzero(((xx < x+xlabel) & (xx > x-xlabel)) & ((yy < y+ylabel) & (yy > y-ylabel))) if len(inds) >0: #if the label happens to be over the beginning of the #contour, the entire contour is removed, i.e. #indices to be removed are #inds= [0,1,2,3,305,306,307] #should rewrite this in a better way linds = nonzero(inds[1:]- inds[:-1] != 1) if inds[0] == 0 and len(linds) != 0: ii = inds[linds[0]] lc1 =linecontour[ii+1:inds[ii+1]] lc2 = [] else: lc1=linecontour[:inds[0]] lc2= linecontour[inds[-1]+1:] else: lc1=linecontour[:ind] lc2 = linecontour[ind+1:] if rot <0: new_x1, new_y1 = x-xlabel, y+ylabel new_x2, new_y2 = x+xlabel, y-ylabel else: new_x1, new_y1 = x-xlabel, y-ylabel new_x2, new_y2 = x+xlabel, y+ylabel new_x1d, new_y1d = trans.inverse_xy_tup((new_x1, new_y1)) new_x2d, new_y2d = trans.inverse_xy_tup((new_x2, new_y2)) if rot > 0: if len(lc1) > 0 and (lc1[-1][0] <= new_x1d) and (lc1[-1][1] <= new_y1d): lc1.append((new_x1d, new_y1d)) if len(lc2) > 0 and (lc2[0][0] >= new_x2d) and (lc2[0][1] >= new_y2d): lc2.insert(0, (new_x2d, new_y2d)) else: if len(lc1) > 0 and ((lc1[-1][0] <= new_x1d) and (lc1[-1][1] >= new_y1d)): lc1.append((new_x1d, new_y1d)) if len(lc2) > 0 and ((lc2[0][0] >= new_x2d) and (lc2[0][1] <= new_y2d)): lc2.insert(0, (new_x2d, new_y2d)) return [lc1,lc2] def locate_label(self, linecontour, labelwidth): """find a good place to plot a label (relatively flat part of the contour) and the angle of rotation for the text object """ nsize= len(linecontour) if labelwidth > 1: xsize = int(ceil(nsize/labelwidth)) else: xsize = 1 if xsize == 1: ysize = nsize else: ysize = labelwidth XX = resize(array(linecontour)[:,0],(xsize, ysize)) YY = resize(array(linecontour)[:,1],(xsize,ysize)) yfirst = YY[:,0] ylast = YY[:,-1] xfirst = XX[:,0] xlast = XX[:,-1] s = ( (reshape(yfirst, (xsize,1))-YY) * (reshape(xlast,(xsize,1)) - reshape(xfirst,(xsize,1))) - (reshape(xfirst,(xsize,1))-XX) * (reshape(ylast,(xsize,1)) - reshape(yfirst,(xsize,1))) ) L=sqrt((xlast-xfirst)**2+(ylast-yfirst)**2) dist = add.reduce(([(abs(s)[i]/L[i]) for i in range(xsize)]),-1) x,y,ind = self.get_label_coords(dist, XX, YY, ysize, labelwidth) angle = arctan2(ylast - yfirst, xlast - xfirst) rotation = angle[ind]*180/pi if rotation > 90: rotation = rotation -180 if rotation < -90: rotation = 180 + rotation dind = list(linecontour).index((x,y)) return x,y, rotation, dind def labels(self, inline): levels = self.label_levels fslist = self.fslist trans = self.ax.transData colors = self.label_mappable.to_rgba(self.label_cvalues) fmt = self.fmt for icon, lev, color, cvalue, fsize in zip(self.label_indices, self.label_levels, colors, self.label_cvalues, fslist): con = self.collections[icon] toremove = [] toadd = [] lw = self.get_label_width(lev, fmt, fsize) for segNum, linecontour in enumerate(con._segments): # for closed contours add one more point to # avoid division by zero if linecontour[0] == linecontour[-1]: linecontour.append(linecontour[1]) # transfer all data points to screen coordinates slc = trans.seq_xy_tups(linecontour) if self.print_label(slc,lw): x,y, rotation, ind = self.locate_label(slc, lw) # transfer the location of the label back to # data coordinates dx,dy = trans.inverse_xy_tup((x,y)) t = Text(dx, dy, rotation = rotation, horizontalalignment='center', verticalalignment='center') text = self.get_text(lev,fmt) self.set_label_props(t, text, color) self.cl.append(t) self.cl_cvalues.append(cvalue) if inline: new = self.break_linecontour(linecontour, rotation, lw, ind) toadd.extend(new) #for c in new: toadd.append(c) toremove.append(linecontour) for c in toremove: con._segments.remove(c) for c in toadd: con._segments.append(c)
def clabel(self, *args, **kwargs): """ clabel(CS, **kwargs) - add labels to line contours in CS, where CS is a ContourSet object returned by contour. clabel(CS, V, **kwargs) - only label contours listed in V keyword arguments: * fontsize = None: as described in http://matplotlib.sf.net/fonts.html * colors = None: - a tuple of matplotlib color args (string, float, rgb, etc), different labels will be plotted in different colors in the order specified - one string color, e.g. colors = 'r' or colors = 'red', all labels will be plotted in this color - if colors == None, the color of each label matches the color of the corresponding contour * inline = True: controls whether the underlying contour is removed (inline = True) or not (False) * fmt = '%1.3f': a format string for the label """ fontsize = kwargs.get('fontsize', None) inline = kwargs.get('inline', 1) self.fmt = kwargs.get('fmt', '%1.3f') colors = kwargs.get('colors', None) if len(args) == 0: levels = self.levels indices = range(len(self.levels)) elif len(args) == 1: levlabs = list(args[0]) indices, levels = [], [] for i, lev in enumerate(self.levels): if lev in levlabs: indices.append(i) levels.append(lev) if len(levels) < len(levlabs): msg = "Specified levels " + str(levlabs) msg += "\n don't match available levels " msg += str(self.levels) raise ValueError(msg) else: raise TypeError("Illegal arguments to clabel, see help(clabel)") self.label_levels = levels self.label_indices = indices self.fp = FontProperties() if fontsize == None: font_size = int(self.fp.get_size_in_points()) else: if type(fontsize) not in [int, float, str]: raise TypeError("Font size must be an integer number.") # Can't it be floating point, as indicated in line above? else: if type(fontsize) == str: font_size = int(self.fp.get_size_in_points()) else: self.fp.set_size(fontsize) font_size = fontsize self.fslist = [font_size] * len(levels) if colors == None: self.label_mappable = self self.label_cvalues = take(self.cvalues, self.label_indices) else: cmap = ListedColormap(colors, N=len(self.label_levels)) self.label_cvalues = range(len(self.label_levels)) self.label_mappable = ScalarMappable(cmap=cmap, norm=no_norm()) #self.cl = [] # Initialized in ContourSet.__init__ #self.cl_cvalues = [] # same self.cl_xy = [] self.labels(inline) for label in self.cl: self.ax.add_artist(label) self.label_list = silent_list('Text', self.cl) return self.label_list