Skip to content

Instantly share code, notes, and snippets.

@dneuman
Last active February 7, 2024 06:10
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dneuman/90af7551c258733954e3b1d1c17698fe to your computer and use it in GitHub Desktop.
Save dneuman/90af7551c258733954e3b1d1c17698fe to your computer and use it in GitHub Desktop.
Text Wrapping in Matplotlib
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Wrapping arbitrary text to a defined box is not supported at least up to
Matplotlib 3.1. This is because the wrap width is hardwired to the Figure box.
A way around this is to override the _get_wrap_line_width method, either with
a new function, or by subclassing the matplotlib.text.Text class. Both
methods are shown below.
Note that this is very simple, and is not tested with rotations.
"""
import matplotlib.pyplot as plt
import matplotlib.text as mtext
fig = plt.figure(1, clear=True)
ax = fig.add_subplot(111)
text = ('Lorem ipsum dolor sit amet, consectetur adipiscing elit, '
'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ')
# METHOD 1: OVERRIDE METHOD
# Create a text box using fancy boxes.
txt = ax.text(.2, .8, text, ha='left', va='top', wrap=True,
bbox=dict(boxstyle='square', fc='w', ec='r'))
# Override the object's method for getting available width
# Note width is in pixels.
txt._get_wrap_line_width = lambda : 600.
# METHOD 2: SUBCLASS AND CHANGE METHOD
class WrapText(mtext.Text):
"""
WrapText(x, y, s, width, widthcoords, **kwargs)
x, y : position (default transData)
text : string
width : box width
widthcoords: coordinate system (default screen pixels)
**kwargs : sent to matplotlib.text.Text
Return : matplotlib.text.Text artist
"""
def __init__(self,
x=0, y=0, text='',
width=0,
widthcoords=None,
**kwargs):
mtext.Text.__init__(self,
x=x, y=y, text=text,
wrap=True,
clip_on=False,
**kwargs)
if not widthcoords:
self.width = width
else:
self.width = widthcoords.transform_point((width,0))[0]
def _get_wrap_line_width(self):
return self.width
# Create artist object.
wtxt = WrapText(.8, .4, text, width=.1, va='top', widthcoords=ax.transAxes,
bbox=dict(boxstyle='square', fc='w', ec='b'))
# Add artist to the axes
ax.add_artist(wtxt)
plt.show()
@dneuman
Copy link
Author

dneuman commented Jun 11, 2019

Figure_1

@StefRe
Copy link

StefRe commented Jan 28, 2021

Very nice, especially the second method. However, to work correctly, you must subtract the start from the endpoint to get the width:

        else:
            a = widthcoords.transform_point([(0,0),(width,0)])
            self.width = a[1][0]-a[0][0]

as you can easily verify with width=1 and widthcoords=ax.transAxes

wtxt = WrapText(.0, .4, text, width=1, va='top', widthcoords=ax.transAxes,
                bbox=dict(boxstyle='square', fc='w', ec='b'))

@dneuman
Copy link
Author

dneuman commented Jan 28, 2021

Thanks.

However, to work correctly, you must subtract the start from the endpoint to get the width:

Note that the start point is 0, so subtracting it is unnecessary.

@StefRe
Copy link

StefRe commented Jan 28, 2021

Note that the start point is 0, so subtracting it is unnecessary.

It's 0 in data coordinates but not in display coordinates, so it matters a lot. See the following example:

import matplotlib.pyplot as plt
import matplotlib.text as mtext

fig,ax = plt.subplots(ncols=2, figsize=(10,5))

class WrapText(mtext.Text):
    """
    WrapText(x, y, s, width, widthcoords, **kwargs)
    x, y       : position (default transData)
    text       : string
    width      : box width
    widthcoords: coordinate system (default screen pixels)
    **kwargs   : sent to matplotlib.text.Text
    Return     : matplotlib.text.Text artist
    """
    def __init__(self,
                 x=0, y=0, text='',
                 width=0,
                 widthcoords=None,
                 **kwargs):
        mtext.Text.__init__(self,
                 x=x, y=y, text=text,
                 wrap=True,
                 clip_on=False,
                 **kwargs)
        if not widthcoords:
            self.width = width
        else:
            if fixed:
                a = widthcoords.transform_point([(0,0),(width,0)])
                self.width = a[1][0]-a[0][0]
            else:
                self.width = widthcoords.transform_point((width,0))[0]

    def _get_wrap_line_width(self):
        return self.width

for fixed in (0,1):
    text =  ' '.join(['_' if fixed else 'x'] * 90)
    ax[fixed].set_title('Fixed - correct' if fixed else 'Original - incorrect')
    ax[fixed].set_xlim(0,2)
    ax[fixed].add_artist(WrapText(0, .8, 'full ' + text, width=1, widthcoords=ax[fixed].transAxes))
    ax[fixed].add_artist(WrapText(0, .6, 'full ' + text, width=2, widthcoords=ax[fixed].transData))
    ax[fixed].add_artist(WrapText(0, .3, 'left half ' + text, width=.5, widthcoords=ax[fixed].transAxes))
    ax[fixed].add_artist(WrapText(1, .1, 'right half' + text, width=.5, widthcoords=ax[fixed].transAxes))

correct

@m-wells
Copy link

m-wells commented Mar 12, 2021

@dneuman and @StefRe excellent!

@m-wells
Copy link

m-wells commented Mar 15, 2021

In order to get this to work properly with constrained_layout=True one needs to change clip_on=True.

import matplotlib.pyplot as plt
import matplotlib.text as mtext

class WrapText(mtext.Text):
    # WrapText(x, y, s, width, widthcoords, **kwargs)
    # x, y       : position (default transData)
    # text       : string
    # width      : box width
    # widthcoords: coordinate system (default screen pixels)
    # **kwargs   : sent to matplotlib.text.Text
    # Return     : matplotlib.text.Text artist
    def __init__(self,
                 x=0, y=0, text='',
                 width=0,
                 widthcoords=None,
                 **kwargs):
        mtext.Text.__init__(self,
                 x=x, y=y, text=text,
                 wrap=True,
                 clip_on=True,
                 **kwargs)
        if not widthcoords:
            self.width = width
        else:
            a = widthcoords.transform_point([(0,0),(width,0)])
            self.width = a[1][0]-a[0][0]

    def _get_wrap_line_width(self):
        return self.width

################################################
# Test Code ######################################
fig = plt.figure(constrained_layout=True)
gs = fig.add_gridspec(2, 4, width_ratios = [2,2,2,1])
ax11 = fig.add_subplot(gs[0,0])
ax21 = fig.add_subplot(gs[1,0])
ax12 = fig.add_subplot(gs[0,1])
ax22 = fig.add_subplot(gs[1,1])
ax13 = fig.add_subplot(gs[0,2])
ax23 = fig.add_subplot(gs[1,2])
ax4 = fig.add_subplot(gs[:,-1])
t = "some words "*5
wtxt = WrapText(0,1, t, width=1, va='top', widthcoords=ax4.transAxes)
ax4.add_artist(wtxt)
ax4.axis("off")
plt.show()

clip_on=False

clip_off

clip_on=True

clipon

@LiorA1
Copy link

LiorA1 commented Aug 18, 2021

Any Idea to how integrate it with:

def render_mpl_table(data, col_width=3.0, row_height=0.625, font_size=14,
                     header_color='#40466e', row_colors=['#f1f1f2', 'w'], edge_color='w',
                     bbox=[0, 0, 1, 1], header_columns=0,
                     ax=None, **kwargs):

    if ax is None:
        size = (np.array(data.shape[::-1]) + np.array([0, 1])) * np.array([col_width, row_height])
        fig, ax = plt.subplots(figsize=size)
        ax.axis('off')
    
    mpl_table = ax.table(cellText=data.values, bbox=bbox, colLabels=data.columns, **kwargs)

    mpl_table.auto_set_font_size(False)
    mpl_table.set_fontsize(font_size)
    

    for k, cell in mpl_table._cells.items():
        cell.set_edgecolor(edge_color)
        if k[0] == 0 or k[1] < header_columns:
            cell.set_text_props(weight='bold', color='w', wrap=True)
            cell.set_facecolor(header_color)
        else:
            cell.set_facecolor(row_colors[k[0] % len(row_colors)])
    return ax.get_figure(), ax

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment