Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Propagate mpl.text.Text instances to the backends and fix documentation #1081

Merged
merged 5 commits into from Nov 30, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG
@@ -1,3 +1,8 @@
2012-11-27 Added the *mtext* parameter for supplying matplotlib.text.Text
instances to RendererBase.draw_tex and RendererBase.draw_text.
This allows backends to utilize additional text attributes, like
the alignment of text elements. - pwuertz

2012-11-16 plt.set_cmap no longer throws errors if there is not already
an active colorable artist, such as an image, and just sets
up the colormap to use from that point forward. - PI
Expand Down
8 changes: 8 additions & 0 deletions doc/users/whats_new.rst
Expand Up @@ -41,6 +41,14 @@ the whole figure. This was already the behavior for both
:func:`~matplotlib.pyplot.axes` and :func:`~matplotlib.pyplot.subplots`, and
now this consistency is shared with :func:`~matplotlib.pyplot.subplot`.

Anchored text support
---------------------
The `svg` and `pgf` backends are now able to save text alignment information
to their output formats. This allows to edit text elements in saved figures,
using Inkscape for example, while preserving their intended position. For
`svg` please note that you'll have to disable the default text-to-path
conversion (`mpl.rc('svg', fonttype='none')`).

.. _whats-new-1-2:

new in matplotlib-1.2
Expand Down
5 changes: 3 additions & 2 deletions lib/matplotlib/axis.py
Expand Up @@ -1847,9 +1847,10 @@ def _get_label(self):
size=rcParams['axes.labelsize'],
weight=rcParams['axes.labelweight']),
color=rcParams['axes.labelcolor'],
verticalalignment='center',
horizontalalignment='right',
verticalalignment='bottom',
horizontalalignment='center',
rotation='vertical',
rotation_mode='anchor',
)
label.set_transform(mtransforms.blended_transform_factory(
mtransforms.IdentityTransform(), self.axes.transAxes))
Expand Down
9 changes: 6 additions & 3 deletions lib/matplotlib/backend_bases.py
Expand Up @@ -443,12 +443,12 @@ def option_scale_image(self):
"""
return False

def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!'):
def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
"""
"""
self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX")

def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
"""
Draw the text instance

Expand All @@ -462,14 +462,17 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
the y location of the text in display coords

*s*
a :class:`matplotlib.text.Text` instance
the text string

*prop*
a :class:`matplotlib.font_manager.FontProperties` instance

*angle*
the rotation angle in degrees

*mtext*
a :class:`matplotlib.text.Text` instance

**backend implementers note**

When you are trying to determine if you have gotten your bounding box
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/backends/backend_agg.py
Expand Up @@ -158,7 +158,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
y = int(y) - oy
self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)

def draw_text(self, gc, x, y, s, prop, angle, ismath):
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
"""
Render the text
"""
Expand Down Expand Up @@ -215,7 +215,7 @@ def get_text_width_height_descent(self, s, prop, ismath):
return w, h, d


def draw_tex(self, gc, x, y, s, prop, angle):
def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
# todo, handle props, angle, origins
size = prop.get_size_in_points()

Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/backends/backend_cairo.py
Expand Up @@ -177,7 +177,7 @@ def draw_image(self, gc, x, y, im):

im.flipud_out()

def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
# Note: x,y are device/display coords, not user-coords, unlike other
# draw_* methods
if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/backends/backend_emf.py
Expand Up @@ -358,7 +358,7 @@ def draw_rectangle(self, gcEdge, rgbFace, x, y, width, height):
if debugPrint: print("draw_rectangle: optimizing away (%f,%f) w=%f,h=%f" % (x,y,width,height))


def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
"""
Draw the text.Text instance s at x,y (display coords) with font
properties instance prop at angle in degrees, using GraphicsContext gc
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/backends/backend_gdk.py
Expand Up @@ -138,7 +138,7 @@ def draw_image(self, gc, x, y, im):
im.flipud_out()


def draw_text(self, gc, x, y, s, prop, angle, ismath):
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
x, y = int(x), int(y)

if x < 0 or y < 0: # window has shrunk and text is off the edge
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/backends/backend_macosx.py
Expand Up @@ -111,7 +111,7 @@ def draw_image(self, gc, x, y, im):
*gc.get_clip_path())
im.flipud_out()

def draw_tex(self, gc, x, y, s, prop, angle):
def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
# todo, handle props, angle, origins
size = prop.get_size_in_points()
texmanager = self.get_texmanager()
Expand All @@ -128,7 +128,7 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle):
self.mathtext_parser.parse(s, self.dpi, prop)
gc.draw_mathtext(x, y, angle, 255 - image.as_array())

def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
if ismath:
self._draw_mathtext(gc, x, y, s, prop, angle)
else:
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/backends/backend_pdf.py
Expand Up @@ -1671,7 +1671,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
# Pop off the global transformation
self.file.output(Op.grestore)

def draw_tex(self, gc, x, y, s, prop, angle):
def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
texmanager = self.get_texmanager()
fontsize = prop.get_size_in_points()
dvifile = texmanager.make_dvi(s, fontsize)
Expand Down Expand Up @@ -1763,7 +1763,7 @@ def encode_string(self, s, fonttype):
return s.encode('cp1252', 'replace')
return s.encode('utf-16be', 'replace')

def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
# TODO: combine consecutive texts into one BT/ET delimited section

# This function is rather complex, since there is no way to
Expand Down
94 changes: 35 additions & 59 deletions lib/matplotlib/backends/backend_pgf.py
Expand Up @@ -595,30 +595,53 @@ def draw_image(self, gc, x, y, im):
writeln(self.fh, r"\pgftext[at=\pgfqpoint{%fin}{%fin},left,bottom]{\pgfimage[interpolate=true,width=%fin,height=%fin]{%s}}" % (x * f, y * f, w * f, h * f, fname_img))
writeln(self.fh, r"\end{pgfscope}")

def draw_tex(self, gc, x, y, s, prop, angle, ismath="TeX!"):
self.draw_text(gc, x, y, s, prop, angle, ismath)
def draw_tex(self, gc, x, y, s, prop, angle, ismath="TeX!", mtext=None):
self.draw_text(gc, x, y, s, prop, angle, ismath, mtext)

def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
# prepare string for tex
s = common_texification(s)

# apply font properties
prop_cmds = _font_properties_str(prop)
s = ur"{%s %s}" % (prop_cmds, s)

# draw text at given coordinates
x = x * 1. / self.dpi
y = y * 1. / self.dpi

writeln(self.fh, r"\begin{pgfscope}")

alpha = gc.get_alpha()
if alpha != 1.0:
writeln(self.fh, r"\pgfsetfillopacity{%f}" % alpha)
writeln(self.fh, r"\pgfsetstrokeopacity{%f}" % alpha)
stroke_rgb = tuple(gc.get_rgb())[:3]
if stroke_rgb != (0, 0, 0):
writeln(self.fh, r"\definecolor{textcolor}{rgb}{%f,%f,%f}" % stroke_rgb)
rgb = tuple(gc.get_rgb())[:3]
if rgb != (0, 0, 0):
writeln(self.fh, r"\definecolor{textcolor}{rgb}{%f,%f,%f}" % rgb)
writeln(self.fh, r"\pgfsetstrokecolor{textcolor}")
writeln(self.fh, r"\pgfsetfillcolor{textcolor}")
writeln(self.fh, "\\pgftext[left,bottom,x=%fin,y=%fin,rotate=%f]{%s}\n" % (x, y, angle, s))

f = 1.0 / self.figure.dpi
text_args = []
if angle == 0 or mtext.get_rotation_mode() == "anchor":
# if text anchoring can be supported, get the original coordinates
# and add alignment information
x, y = mtext.get_transform().transform_point(mtext.get_position())
text_args.append("x=%fin" % (x * f))
text_args.append("y=%fin" % (y * f))

halign = {"left": "left", "right": "right", "center": ""}
valign = {"top": "top", "bottom": "bottom",
"baseline": "base", "center": ""}
text_args.append(halign[mtext.get_ha()])
text_args.append(valign[mtext.get_va()])
else:
# if not, use the text layout provided by matplotlib
text_args.append("x=%fin" % (x * f))
text_args.append("y=%fin" % (y * f))
text_args.append("left")
text_args.append("bottom")

if angle != 0:
text_args.append("rotate=%f" % angle)

writeln(self.fh, r"\pgftext[%s]{%s}" % (",".join(text_args), s))
writeln(self.fh, r"\end{pgfscope}")

def get_text_width_height_descent(self, s, prop, ismath):
Expand Down Expand Up @@ -861,53 +884,6 @@ def print_png(self, fname_or_fh, *args, **kwargs):
else:
raise ValueError("filename must be a path or a file-like object")

def _render_texts_pgf(self, fh):
# TODO: currently unused code path

# alignment anchors
valign = {"top": "top", "bottom": "bottom", "baseline": "base", "center": ""}
halign = {"left": "left", "right": "right", "center": ""}
# alignment anchors for 90deg. rotated labels
rvalign = {"top": "left", "bottom": "right", "baseline": "right", "center": ""}
rhalign = {"left": "top", "right": "bottom", "center": ""}

# TODO: matplotlib does not hide unused tick labels yet, workaround
for tick in self.figure.findobj(mpl.axis.Tick):
tick.label1.set_visible(tick.label1On)
tick.label2.set_visible(tick.label2On)
# TODO: strange, first legend label is always "None", workaround
for legend in self.figure.findobj(mpl.legend.Legend):
labels = legend.findobj(mpl.text.Text)
labels[0].set_visible(False)
# TODO: strange, legend child labels are duplicated,
# find a list of unique text objects as workaround
texts = self.figure.findobj(match=Text, include_self=False)
texts = list(set(texts))

# draw text elements
for text in texts:
s = text.get_text()
if not s or not text.get_visible():
continue

s = common_texification(s)

fontsize = text.get_fontsize()
angle = text.get_rotation()
transform = text.get_transform()
x, y = transform.transform_point(text.get_position())
x = x * 1.0 / self.figure.dpi
y = y * 1.0 / self.figure.dpi
# TODO: positioning behavior unknown for rotated elements
# right now only the alignment for 90deg rotations is correct
if angle == 90.:
align = rvalign[text.get_va()] + "," + rhalign[text.get_ha()]
else:
align = valign[text.get_va()] + "," + halign[text.get_ha()]

s = ur"{\fontsize{%f}{%f}\selectfont %s}" % (fontsize, fontsize*1.2, s)
writeln(fh, ur"\pgftext[%s,x=%fin,y=%fin,rotate=%f]{%s}" % (align,x,y,angle,s))

def get_renderer(self):
return RendererPgf(self.figure, None)

Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/backends/backend_ps.py
Expand Up @@ -649,7 +649,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,

self._path_collection_id += 1

def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!'):
def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
"""
draw a Text instance
"""
Expand Down Expand Up @@ -684,7 +684,7 @@ def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!'):
self._pswriter.write(ps)
self.textcnt += 1

def draw_text(self, gc, x, y, s, prop, angle, ismath):
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
"""
draw a Text instance
"""
Expand Down
52 changes: 41 additions & 11 deletions lib/matplotlib/backends/backend_svg.py
Expand Up @@ -815,7 +815,7 @@ def draw_image(self, gc, x, y, im, dx=None, dy=None, transform=None):
def _adjust_char_id(self, char_id):
return char_id.replace(u"%20", u"_")

def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath):
def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None):
"""
draw the text by converting them to paths using textpath module.

Expand Down Expand Up @@ -940,7 +940,7 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath):

writer.end('g')

def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath):
def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None):
writer = self.writer

color = rgb2hex(gc.get_rgb())
Expand All @@ -953,7 +953,8 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath):
if not ismath:
font = self._get_font(prop)
font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
y -= font.get_descent() / 64.0
descent = font.get_descent() / 64.0
y -= descent

fontsize = prop.get_size_in_points()

Expand All @@ -967,11 +968,40 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath):
style[u'font-style'] = prop.get_style().lower()
attrib[u'style'] = generate_css(style)

attrib[u'transform'] = generate_transform([
(u'translate', (x, y)),
(u'rotate', (-angle,))])
if angle == 0 or mtext.get_rotation_mode() == "anchor":
# If text anchoring can be supported, get the original
# coordinates and add alignment information.

# Get anchor coordinates.
transform = mtext.get_transform()
ax, ay = transform.transform_point(mtext.get_position())
ay = self.height - ay

# Don't do vertical anchor alignment. Most applications do not
# support 'alignment-baseline' yet. Apply the vertical layout
# to the anchor point manually for now.
angle_rad = angle * np.pi / 180.
dir_vert = np.array([np.sin(angle_rad), np.cos(angle_rad)])
y += descent # Undo inappropriate text descent handling
v_offset = np.dot(dir_vert, [(x - ax), (y - ay)])
ax = ax + (v_offset - descent) * dir_vert[0]
ay = ay + (v_offset - descent) * dir_vert[1]

ha_mpl_to_svg = {'left': 'start', 'right': 'end',
'center': 'middle'}
style[u'text-anchor'] = ha_mpl_to_svg[mtext.get_ha()]

attrib[u'x'] = str(ax)
attrib[u'y'] = str(ay)
attrib[u'style'] = generate_css(style)
attrib[u'transform'] = u"rotate(%f, %f, %f)" % (-angle, ax, ay)
writer.element(u'text', s, attrib=attrib)
else:
attrib[u'transform'] = generate_transform([
(u'translate', (x, y)),
(u'rotate', (-angle,))])

writer.element(u'text', s, attrib=attrib)
writer.element(u'text', s, attrib=attrib)

if rcParams['svg.fonttype'] == 'svgfont':
fontset = self._fonts.setdefault(font.fname, set())
Expand Down Expand Up @@ -1053,10 +1083,10 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath):

writer.end(u'g')

def draw_tex(self, gc, x, y, s, prop, angle):
def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX")

def draw_text(self, gc, x, y, s, prop, angle, ismath):
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
clipid = self._get_clip(gc)
if clipid is not None:
# Cannot apply clip-path directly to the text, because
Expand All @@ -1065,9 +1095,9 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath):
u'g', attrib={u'clip-path': u'url(#%s)' % clipid})

if rcParams['svg.fonttype'] == 'path':
self._draw_text_as_path(gc, x, y, s, prop, angle, ismath)
self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext)
else:
self._draw_text_as_text(gc, x, y, s, prop, angle, ismath)
self._draw_text_as_text(gc, x, y, s, prop, angle, ismath, mtext)

if clipid is not None:
self.writer.end(u'g')
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/backends/backend_template.py
Expand Up @@ -104,7 +104,7 @@ def draw_path(self, gc, path, transform, rgbFace=None):
def draw_image(self, gc, x, y, im):
pass

def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
pass

def flipy(self):
Expand Down