0

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 Answer 1

0

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.)

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.