diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index f82ab5142d62..ec480c61aabb 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -1,16 +1,20 @@ -from __future__ import (absolute_import, division, print_function, - unicode_literals) +from __future__ import ( + absolute_import, division, print_function, unicode_literals) import six +import warnings import numpy as np from numpy.testing import assert_almost_equal +from nose import SkipTest +from nose.tools import assert_raises, eq_ + import matplotlib -from matplotlib.testing.decorators import image_comparison, knownfailureif, cleanup import matplotlib.pyplot as plt -import warnings -from nose import SkipTest -from nose.tools import with_setup, assert_raises +from matplotlib.testing.decorators import image_comparison, cleanup +from matplotlib.figure import Figure +from matplotlib.text import Annotation, Text +from matplotlib.backends.backend_agg import RendererAgg @image_comparison(baseline_images=['font_styles']) @@ -24,60 +28,77 @@ def find_matplotlib_font(**kw): return FontProperties(fname=path) from matplotlib.font_manager import FontProperties, findfont - warnings.filterwarnings('ignore', ('findfont: Font family \[u?\'Foo\'\] '+ - 'not found. Falling back to .'), - UserWarning, - module='matplotlib.font_manager') - fig = plt.figure() + warnings.filterwarnings( + 'ignore', + ('findfont: Font family \[u?\'Foo\'\] not found. Falling back to .'), + UserWarning, + module='matplotlib.font_manager') + + plt.figure() ax = plt.subplot(1, 1, 1) - normalFont = find_matplotlib_font(family="sans-serif", - style="normal", - variant="normal", - size=14, - ) - ax.annotate("Normal Font", (0.1, 0.1), xycoords='axes fraction', - fontproperties=normalFont) - - boldFont = find_matplotlib_font(family="Foo", - style="normal", - variant="normal", - weight="bold", - stretch=500, - size=14, - ) - ax.annotate("Bold Font", (0.1, 0.2), xycoords='axes fraction', - fontproperties=boldFont) - - boldItemFont = find_matplotlib_font(family="sans serif", - style="italic", - variant="normal", - weight=750, - stretch=500, - size=14, - ) - ax.annotate("Bold Italic Font", (0.1, 0.3), xycoords='axes fraction', - fontproperties=boldItemFont) - - lightFont = find_matplotlib_font(family="sans-serif", - style="normal", - variant="normal", - weight=200, - stretch=500, - size=14, - ) - ax.annotate("Light Font", (0.1, 0.4), xycoords='axes fraction', - fontproperties=lightFont) - - condensedFont = find_matplotlib_font(family="sans-serif", - style="normal", - variant="normal", - weight=500, - stretch=100, - size=14, - ) - ax.annotate("Condensed Font", (0.1, 0.5), xycoords='axes fraction', - fontproperties=condensedFont) + normalFont = find_matplotlib_font( + family="sans-serif", + style="normal", + variant="normal", + size=14) + ax.annotate( + "Normal Font", + (0.1, 0.1), + xycoords='axes fraction', + fontproperties=normalFont) + + boldFont = find_matplotlib_font( + family="Foo", + style="normal", + variant="normal", + weight="bold", + stretch=500, + size=14) + ax.annotate( + "Bold Font", + (0.1, 0.2), + xycoords='axes fraction', + fontproperties=boldFont) + + boldItemFont = find_matplotlib_font( + family="sans serif", + style="italic", + variant="normal", + weight=750, + stretch=500, + size=14) + ax.annotate( + "Bold Italic Font", + (0.1, 0.3), + xycoords='axes fraction', + fontproperties=boldItemFont) + + lightFont = find_matplotlib_font( + family="sans-serif", + style="normal", + variant="normal", + weight=200, + stretch=500, + size=14) + ax.annotate( + "Light Font", + (0.1, 0.4), + xycoords='axes fraction', + fontproperties=lightFont) + + condensedFont = find_matplotlib_font( + family="sans-serif", + style="normal", + variant="normal", + weight=500, + stretch=100, + size=14) + ax.annotate( + "Condensed Font", + (0.1, 0.5), + xycoords='axes fraction', + fontproperties=condensedFont) ax.set_xticks([]) ax.set_yticks([]) @@ -85,18 +106,20 @@ def find_matplotlib_font(**kw): @image_comparison(baseline_images=['multiline']) def test_multiline(): - fig = plt.figure() + plt.figure() ax = plt.subplot(1, 1, 1) ax.set_title("multiline\ntext alignment") - plt.text(0.2, 0.5, "TpTpTp\n$M$\nTpTpTp", size=20, - ha="center", va="top") + plt.text( + 0.2, 0.5, "TpTpTp\n$M$\nTpTpTp", size=20, ha="center", va="top") - plt.text(0.5, 0.5, "TpTpTp\n$M^{M^{M^{M}}}$\nTpTpTp", size=20, - ha="center", va="top") + plt.text( + 0.5, 0.5, "TpTpTp\n$M^{M^{M^{M}}}$\nTpTpTp", size=20, + ha="center", va="top") - plt.text(0.8, 0.5, "TpTpTp\n$M_{q_{q_{q}}}$\nTpTpTp", size=20, - ha="center", va="top") + plt.text( + 0.8, 0.5, "TpTpTp\n$M_{q_{q_{q}}}$\nTpTpTp", size=20, + ha="center", va="top") plt.xlim(0, 1) plt.ylim(0, 0.8) @@ -137,15 +160,15 @@ def test_contains(): fig = plt.figure() ax = plt.axes() - mevent = mbackend.MouseEvent('button_press_event', fig.canvas, 0.5, - 0.5, 1, None) + mevent = mbackend.MouseEvent( + 'button_press_event', fig.canvas, 0.5, 0.5, 1, None) xs = np.linspace(0.25, 0.75, 30) ys = np.linspace(0.25, 0.75, 30) xs, ys = np.meshgrid(xs, ys) - txt = plt.text(0.48, 0.52, 'hello world', ha='center', fontsize=30, - rotation=30) + txt = plt.text( + 0.48, 0.52, 'hello world', ha='center', fontsize=30, rotation=30) # uncomment to draw the text's bounding box # txt.set_bbox(dict(edgecolor='black', facecolor='none')) @@ -155,9 +178,7 @@ def test_contains(): for x, y in zip(xs.flat, ys.flat): mevent.x, mevent.y = plt.gca().transAxes.transform_point([x, y]) - contains, _ = txt.contains(mevent) - color = 'yellow' if contains else 'red' # capture the viewLim, plot a point, and reset the viewLim @@ -169,7 +190,7 @@ def test_contains(): @image_comparison(baseline_images=['titles']) def test_titles(): # left and right side titles - fig = plt.figure() + plt.figure() ax = plt.subplot(1, 1, 1) ax.set_title("left title", loc="left") ax.set_title("right title", loc="right") @@ -179,15 +200,17 @@ def test_titles(): @image_comparison(baseline_images=['text_alignment']) def test_alignment(): - fig = plt.figure() + plt.figure() ax = plt.subplot(1, 1, 1) x = 0.1 for rotation in (0, 30): for alignment in ('top', 'bottom', 'baseline', 'center'): - ax.text(x, 0.5, alignment + " Tj", va=alignment, rotation=rotation, - bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5)) - ax.text(x, 1.0, r'$\sum_{i=0}^{j}$', va=alignment, rotation=rotation) + ax.text( + x, 0.5, alignment + " Tj", va=alignment, rotation=rotation, + bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5)) + ax.text( + x, 1.0, r'$\sum_{i=0}^{j}$', va=alignment, rotation=rotation) x += 0.1 ax.plot([0, 1], [0.5, 0.5]) @@ -202,8 +225,8 @@ def test_alignment(): @image_comparison(baseline_images=['axes_titles'], extensions=['png']) def test_axes_titles(): # Related to issue #3327 - fig = plt.figure() - ax = plt.subplot(1,1,1) + plt.figure() + ax = plt.subplot(1, 1, 1) ax.set_title('center', loc='center', fontsize=20, fontweight=700) ax.set_title('left', loc='left', fontsize=12, fontweight=400) ax.set_title('right', loc='right', fontsize=12, fontweight=400) @@ -214,7 +237,8 @@ def test_set_position(): fig, ax = plt.subplots() # test set_position - ann = ax.annotate('test', (0, 0), xytext=(0, 0), textcoords='figure pixels') + ann = ax.annotate( + 'test', (0, 0), xytext=(0, 0), textcoords='figure pixels') plt.draw() init_pos = ann.get_window_extent(fig.canvas.renderer) @@ -227,7 +251,8 @@ def test_set_position(): assert a + shift_val == b # test xyann - ann = ax.annotate('test', (0, 0), xytext=(0, 0), textcoords='figure pixels') + ann = ax.annotate( + 'test', (0, 0), xytext=(0, 0), textcoords='figure pixels') plt.draw() init_pos = ann.get_window_extent(fig.canvas.renderer) @@ -279,8 +304,110 @@ def test_get_rotation_mod360(): for i, j in zip([360., 377., 720+177.2], [0., 17., 177.2]): assert_almost_equal(text.get_rotation(i), j) + @image_comparison(baseline_images=['text_bboxclip']) def test_bbox_clipping(): plt.text(0.9, 0.2, 'Is bbox clipped?', backgroundcolor='r', clip_on=True) t = plt.text(0.9, 0.5, 'Is fancy bbox clipped?', clip_on=True) t.set_bbox({"boxstyle": "round, pad=0.1"}) + + +def test_text_annotation_get_window_extent(): + figure = Figure(dpi=100) + renderer = RendererAgg(200, 200, 100) + + # Only text annotation + annotation = Annotation('test', xy=(0, 0)) + annotation.set_figure(figure) + + text = Text(text='test', x=0, y=0) + text.set_figure(figure) + + bbox = annotation.get_window_extent(renderer=renderer) + + text_bbox = text.get_window_extent(renderer=renderer) + eq_(bbox.width, text_bbox.width) + eq_(bbox.height, text_bbox.height) + + _, _, d = renderer.get_text_width_height_descent( + 'text', annotation._fontproperties, ismath=False) + _, _, lp_d = renderer.get_text_width_height_descent( + 'lp', annotation._fontproperties, ismath=False) + below_line = max(d, lp_d) + + # These numbers are specific to the current implementation of Text + points = bbox.get_points() + eq_(points[0, 0], 0.0) + eq_(points[1, 0], text_bbox.width) + eq_(points[0, 1], -below_line) + eq_(points[1, 1], text_bbox.height - below_line) + + +def test_text_with_arrow_annotation_get_window_extent(): + figure = Figure(dpi=100) + renderer = RendererAgg(600, 600, 100) + headwidth = 21 + + text = Text(text='test', x=0, y=0) + text.set_figure(figure) + text_bbox = text.get_window_extent(renderer=renderer) + + # Text annotation with arrow + annotation = Annotation( + 'test', + xy=(0.0, 50.0 + (headwidth / 0.72) * 0.5), + xytext=(50.0, 50.0), xycoords='figure pixels', + arrowprops={ + 'facecolor': 'black', 'width': 2, + 'headwidth': headwidth, 'shrink': 0.0}) + annotation.set_figure(figure) + annotation.draw(renderer) + + bbox = annotation.get_window_extent(renderer=renderer) + eq_(bbox.width, text_bbox.width + 50.0) + expected_height = max(text_bbox.height, headwidth / 0.72) + assert_almost_equal(bbox.height, expected_height) + + +def test_arrow_annotation_get_window_extent(): + figure = Figure(dpi=100) + figure.set_figwidth(2.0) + figure.set_figheight(2.0) + renderer = RendererAgg(200, 200, 100) + + # Text annotation with arrow + annotation = Annotation( + '', xy=(0.0, 50.0), xytext=(50.0, 50.0), xycoords='figure pixels', + arrowprops={ + 'facecolor': 'black', 'width': 8, 'headwidth': 10, 'shrink': 0.0}) + annotation.set_figure(figure) + annotation.draw(renderer) + + bbox = annotation.get_window_extent() + points = bbox.get_points() + + eq_(bbox.width, 50.0) + assert_almost_equal(bbox.height, 10.0 / 0.72) + eq_(points[0, 0], 0.0) + eq_(points[0, 1], 50.0 - 5 / 0.72) + + +def test_empty_annotation_get_window_extent(): + figure = Figure(dpi=100) + figure.set_figwidth(2.0) + figure.set_figheight(2.0) + renderer = RendererAgg(200, 200, 100) + + # Text annotation with arrow + annotation = Annotation( + '', xy=(0.0, 50.0), xytext=(0.0, 50.0), xycoords='figure pixels') + annotation.set_figure(figure) + annotation.draw(renderer) + + bbox = annotation.get_window_extent() + points = bbox.get_points() + + eq_(points[0, 0], 0.0) + eq_(points[1, 0], 0.0) + eq_(points[1, 1], 50.0) + eq_(points[0, 1], 50.0) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 1b86d4597801..24d0b45f9e14 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -1957,7 +1957,7 @@ def _update_position_xytext(self, renderer, xy_pixel): if self.arrowprops: x0, y0 = xy_pixel - l, b, w, h = self.get_window_extent(renderer).bounds + l, b, w, h = Text.get_window_extent(self, renderer).bounds r = l + w t = b + h xc = 0.5 * (l + r) @@ -1975,7 +1975,7 @@ def _update_position_xytext(self, renderer, xy_pixel): # the textbox. # TODO : Rotation needs to be accounted. relpos = self._arrow_relpos - bbox = self.get_window_extent(renderer) + bbox = Text.get_window_extent(self, renderer) ox0 = bbox.x0 + bbox.width * relpos[0] oy0 = bbox.y0 + bbox.height * relpos[1] @@ -2007,7 +2007,7 @@ def _update_position_xytext(self, renderer, xy_pixel): self.arrow_patch.set_patchA(None) return - bbox = self.get_window_extent(renderer) + bbox = Text.get_window_extent(self, renderer) l, b, w, h = bbox.bounds l -= pad / 2. b -= pad / 2. @@ -2045,7 +2045,6 @@ def _update_position_xytext(self, renderer, xy_pixel): width = d.pop('width', 4) headwidth = d.pop('headwidth', 12) frac = d.pop('frac', 0.1) - self.arrow = YAArrow(self.figure, (x0 + dx, y0 + dy), (x - dx, y - dy), width=width, headwidth=headwidth, @@ -2108,5 +2107,39 @@ def draw(self, renderer): Text.draw(self, renderer) + def get_window_extent(self, renderer=None): + ''' + Return a :class:`~matplotlib.transforms.Bbox` object bounding + the text and arrow annotation, in display units. + + *renderer* defaults to the _renderer attribute of the text + object. This is not assigned until the first execution of + :meth:`draw`, so you must use this kwarg if you want + to call :meth:`get_window_extent` prior to the first + :meth:`draw`. For getting web page regions, it is + simpler to call the method after saving the figure. The + *dpi* used defaults to self.figure.dpi; the renderer dpi is + irrelevant. + + ''' + arrow = self.arrow + arrow_patch = self.arrow_patch + + text_bbox = Text.get_window_extent(self, renderer=renderer) + if text_bbox.width == 0.0 and text_bbox.height == 0.0: + bboxes = [] + else: + bboxes = [text_bbox] + + if self.arrow is not None: + bboxes.append(arrow.get_window_extent(renderer=renderer)) + elif self.arrow_patch is not None: + bboxes.append(arrow_patch.get_window_extent(renderer=renderer)) + + if len(bboxes) == 0: + return Bbox.from_bounds(self._x, self._y, 0.0, 0.0) + else: + return Bbox.union(bboxes) + docstring.interpd.update(Annotation=Annotation.__init__.__doc__)