How to measure a text element in matplotlib

32 Views Asked by At

I need to lay out a table full of text boxes using matplotlib. It should be obvious how to do this: create a gridspec for the table members, fill in each element of the grid, take the maximum heights and widths of the elements in the grid, change the appropriate height and widths of the grid columns and rows. Easy peasy, right?

Wrong.

Everything works except the measurements of the items themselves. Matplotlib consistently returns the wrong size for each item. I believe that I have been able to track this down to not even being able to measure the size of a text path correctly:

import numpy as np

import matplotlib.pyplot as plt
import matplotlib.patches as mpatch
import matplotlib.text as mtext
import matplotlib.path as mpath
import matplotlib.patches as mpatches

fig, ax = plt.subplots(1, 1)
ax.set_axis_off()

text = '!?' * 16
size=36

## Buildand measure hidden text path
text_path=mtext.TextPath(
    (0.0, 0.0), 
    text,
    prop={'size' : size}
)

vertices = text_path.vertices
code = text_path.codes

min_x, min_y = np.min(
    text_path.vertices[text_path.codes != mpath.Path.CLOSEPOLY], axis=0)
max_x, max_y = np.max(
    text_path.vertices[text_path.codes != mpath.Path.CLOSEPOLY], axis=0)

## Transform measurement to graph units
transData = ax.transData.inverted()
((local_min_x, local_min_y),
 (local_max_x, local_max_y)) = transData.transform(
    ((min_x, min_y), (max_x, max_y)))

## Draw a box which should enclose the path
x_offset = (local_max_x - local_max_y) / 2
y_offset = (local_max_y - local_min_y) / 2
local_min_x = 0.5 - x_offset
local_min_y = 0.5 - y_offset
local_max_x = 0.5 + x_offset
local_max_y = 0.5 + y_offset

path_data = [
    (mpath.Path.MOVETO, (local_min_x, local_min_y)),
    (mpath.Path.LINETO, (local_max_x, local_min_y)),
    (mpath.Path.LINETO, (local_max_x, local_max_y)),
    (mpath.Path.LINETO, (local_min_x, local_max_y)),
    (mpath.Path.LINETO, (local_min_x, local_min_y)),
    (mpath.Path.CLOSEPOLY, (local_min_x, local_min_y)),
]

codes, verts = zip(*path_data)
path = mpath.Path(verts, codes)
patch = mpatches.PathPatch(
    path, 
    facecolor='white',
    edgecolor='red',
    linewidth=3)
ax.add_patch(patch)
    
## Draw the text itself
item_textbox = ax.text(
    0.5, 0.5, 
    text,
    bbox=dict(boxstyle='square',
              fc='white', 
              ec='white',
              alpha=0.0),
    transform=ax.transAxes,
    size=size, 
    horizontalalignment="center", 
    verticalalignment="center",
    alpha=1.0)

plt.show()

Run this under Python 3.8

Expect: the red box to be the exact height and width of the text

Observe: the red box is the right height, but is most definitely not the right width.

1

There are 1 best solutions below

0
JWLM On

There doesn't seem to be any way to do this directly, but there's a way to do it indirectly: instead of using a text box, use TextPath, transform it to Axis coordinates, and then use the differences between min and max on each coordinate. (See https://matplotlib.org/stable/gallery/text_labels_and_annotations/demo_text_path.html#sphx-glr-gallery-text-labels-and-annotations-demo-text-path-py for a sample implementation. This implementation has a significant bug -- it uses vertices and codes directly, which break in the case of a clipped text path.)