From b89e72878338290d6195d0deeac9fd689425659d Mon Sep 17 00:00:00 2001 From: Ryan May Date: Mon, 2 Feb 2015 12:20:52 -0700 Subject: [PATCH] Implement a scattertext plot method. Adds a TextCollection object that facilitates drawing a bunch of text with common properties at different locations. scattertext() uses this to plot text in data coordinates, with an optional offset. --- examples/pylab_examples/scattertext.py | 13 ++++ lib/matplotlib/axes/_axes.py | 89 ++++++++++++++++++++++++++ lib/matplotlib/text.py | 75 ++++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 examples/pylab_examples/scattertext.py diff --git a/examples/pylab_examples/scattertext.py b/examples/pylab_examples/scattertext.py new file mode 100644 index 000000000000..077fabb7d080 --- /dev/null +++ b/examples/pylab_examples/scattertext.py @@ -0,0 +1,13 @@ +import matplotlib.pyplot as plt + +x = [0.5, -1.0, 1.0] +y = [1.0, -2.0, 3.0] +data = ['100.1', 'Hello!', '50'] + +fig = plt.figure() +ax = fig.add_subplot(1, 1, 1) +ax.scattertext(x, y, data, loc=(-20, 20)) +ax.scattertext(x, y, data, color='red', loc=(20, -20)) +ax.scattertext(x, y, data, color='blue') + +plt.show() diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 940ebddf8700..aa324f481efd 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -591,6 +591,95 @@ def text(self, x, y, s, fontdict=None, t.set_clip_path(self.patch) return t + @docstring.dedent_interpd + def scattertext(self, x, y, texts, loc=(0, 0), **kw): + """ + Add text to the axes. + + Add text in string `s` to axis at location `x`, `y`, data + coordinates. + + Parameters + ---------- + x, y : array_like, shape (n, ) + Input positions + + texts : array_like, shape (n, ) + Collection of text that will be plotted at each (x,y) location + + loc : length-2 tuple + Offset (in screen coordinates) from x,y position. Allows + positioning text relative to original point. + + Other parameters + ---------------- + kwargs : `~matplotlib.text.TextCollection` properties. + Other miscellaneous text parameters. + + Examples + -------- + Individual keyword arguments can be used to override any given + parameter:: + + >>> scattertext(x, y, texts, fontsize=12) + + The default setting to to center the text at the specified x,y + locations in data coordinates, and to take the data and format as + float without any decimal places. The example below places the text + above and to the right by 10 pixels, with 2 decimal places:: + + >>> scattertext([0.25, 0.75], [0.25, 0.75], [0.5, 1.0], + ... loc=(10, 10)) + """ + # Start with default args and update from kw + new_kw = { + 'verticalalignment': 'center', + 'horizontalalignment': 'center', + 'transform': self.transData, + 'clip_on': False} + new_kw.update(kw) + + # Default to centered on point--special case it to keep tranform + # simpler. + t = new_kw['transform'] + if loc == (0, 0): + trans = t + else: + x0, y0 = loc + trans = t + mtransforms.Affine2D().translate(x0, y0) + new_kw['transform'] = trans + + # Handle masked arrays + x, y, texts = cbook.delete_masked_points(x, y, texts) + + # If there is nothing left after deleting the masked points, return None + if not x.any(): + return None + + # Make the TextCollection object + text_obj = mtext.TextCollection(x, y, texts, **new_kw) + + # Add it to the axes + self.add_artist(text_obj) + + # Update plot range + minx = np.min(x) + maxx = np.max(x) + miny = np.min(y) + maxy = np.max(y) + w = maxx - minx + h = maxy - miny + + # the pad is a little hack to deal with the fact that we don't + # want to transform all the symbols whose scales are in points + # to data coords to get the exact bounding box for efficiency + # reasons. It can be done right if this is deemed important + padx, pady = 0.05 * w, 0.05 * h + corners = (minx - padx, miny - pady), (maxx + padx, maxy + pady) + self.update_datalim(corners) + self.autoscale_view() + return text_obj + @docstring.dedent_interpd def annotate(self, *args, **kwargs): """ diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index e1a43c64d43e..88c0a60c5eff 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -1467,6 +1467,81 @@ def set_figure(self, fig): docstring.interpd.update(TextWithDash=artist.kwdoc(TextWithDash)) +class TextCollection(Text): + def __init__(self, x, y, text, **kwargs): + Text.__init__(self, **kwargs) + self.x = x + self.y = y + self.text = text + + def __str__(self): + return "TextCollection" + + @allow_rasterization + def draw(self, renderer): + """ + Draws the :class:`TextCollection` object to the given *renderer*. + """ + if renderer is not None: + self._renderer = renderer + if not self.get_visible(): + return + if not any(self.text): + return + + renderer.open_group('text', self.get_gid()) + trans = self.get_transform() + + posx = self.convert_xunits(self.x) + posy = self.convert_yunits(self.y) + + pts = np.vstack((posx, posy)).T + pts = trans.transform(pts) + canvasw, canvash = renderer.get_canvas_width_height() + + gc = renderer.new_gc() + gc.set_foreground(self.get_color()) + gc.set_alpha(self.get_alpha()) + gc.set_url(self._url) + self._set_gc_clip(gc) + + if self._bbox: + bbox_artist(self, renderer, self._bbox) + angle = self.get_rotation() + + for (posx, posy), t in zip(pts, self.text): + self._text = t # hack to allow self._get_layout to work + bbox, info, descent = self._get_layout(renderer) + for line, wh, x, y in info: + if not np.isfinite(x) or not np.isfinite(y): + continue + + mtext = self if len(info) == 1 else None + x = x + posx + y = y + posy + if renderer.flipy(): + y = canvash - y + clean_line, ismath = self.is_math_text(line) + + if self.get_path_effects(): + from matplotlib.patheffects import PathEffectRenderer + renderer = PathEffectRenderer(self.get_path_effects(), + renderer) + + if self.get_usetex(): + renderer.draw_tex(gc, x, y, clean_line, + self._fontproperties, angle, mtext=mtext) + else: + renderer.draw_text(gc, x, y, clean_line, + self._fontproperties, angle, + ismath=ismath, mtext=mtext) + + gc.restore() + renderer.close_group('text') + +docstring.interpd.update(TextCollection=artist.kwdoc(TextCollection)) + + class OffsetFrom(object): def __init__(self, artist, ref_coord, unit="points"): self._artist = artist