Search code examples
pythonmatplotlibtextrectangles

Creating a text inside a rectangle disappearing when zooming out


I want to create an rectangle with a text inside for a gant chart with matplotlib. But if one is zooming in and out the text should never be bigger than the rectangle. So as soon as the text (with a fixed font size) is bigger than the rectangle it should disappear and as sson as it is smaller again (zooming in) it should appear again.

This is an example of a simple text with the problem that the text does not stay inside the rectangle when zooming (is not disappearing as soon as it becoems to big).

from matplotlib import pyplot as plt, patches
plt.rcParams["figure.figsize"] = [7.00, 3.50]
plt.rcParams["figure.autolayout"] = True
fig = plt.figure()
ax = fig.add_subplot(111)
rectangle = patches.Rectangle((0, 0), 3, 3, edgecolor='orange',
facecolor="green", linewidth=7)
ax.add_patch(rectangle)
rx, ry = rectangle.get_xy()
cx = rx + rectangle.get_width()/2.0
cy = ry + rectangle.get_height()/2.0
ax.annotate("Rectangle", (cx, cy), color='black', weight='bold', fontsize=10, ha='center', va='center')
plt.xlim([-5, 5])
plt.ylim([-5, 5])
plt.show()

Text should disappear now

Text should disappear now


Solution

  • To monitor the zoom state of your plot, you can connect to the "xlim_changed" and "ylim_changed" events of the Axes class. Apart from the zoom state, you might also want to factor in changes of the figure size, which you can monitor via the "resize_event" of the figure canvas.

    Below, I adjusted your code so that

    • the text is hidden, as soon as the ratio of your x limits wrt. figure height or the ratio of your y limits wrt. figure width are more than double the ratio of your original values (2·10/7 for the width and 2·10/3.5 for the height, where 10 is the distance between your lower limit -5 and upper limit 5, and 2 is a value that in your given example seems to produce a good result) – it is this ratio that is important, so that you react to actual changes of the rectangle's screen size as opposed to its size in image coordinates;
    • the text is shown again, as soon as the ratios are less than double the original ratios.
    from matplotlib import pyplot as plt, patches
    
    plt.rcParams["figure.figsize"] = [7.00, 3.50]
    plt.rcParams["figure.autolayout"] = True
    fig = plt.figure()
    ax = fig.add_subplot(111)
    rectangle = patches.Rectangle((0, 0), 3, 3, edgecolor="orange",
                                  facecolor="green", linewidth=7)
    ax.add_patch(rectangle)
    rx, ry = rectangle.get_xy()
    cx = rx + rectangle.get_width()/2.0
    cy = ry + rectangle.get_height()/2.0
    annotation = ax.annotate("Rectangle", (cx, cy), color="black", weight="bold",
                             fontsize=10, ha="center", va="center")
    
    def on_size_change(*unused_events):
        xlim, ylim, figsize = ax.get_xlim(), ax.get_ylim(), fig.get_size_inches()
        x_ratio = (xlim[1] - xlim[0]) / figsize[0]  # xlim dist. over fig. width
        y_ratio = (ylim[1] - ylim[0]) / figsize[1]  # ylim dist. over fig. height
        visible = x_ratio <= 20 / 7. and y_ratio <= 20 / 3.5
        annotation.set_visible(visible)
    
    ax.callbacks.connect("xlim_changed", on_size_change)
    ax.callbacks.connect("ylim_changed", on_size_change)
    fig.canvas.mpl_connect("resize_event", on_size_change)
    
    plt.xlim([-5, 5])
    plt.ylim([-5, 5])
    plt.show()
    

    Alternatively, you might want to make the showing and hiding of your text directly depend on the rectangle's screen size (show text as long as both width and height of the rectangle are greater than the width and height of the text). In this case, you might want to adjust the on_size_change() function as follows:

    def on_size_change(*unused_events):
        annotation.set_visible(True)  # Text must be visible to get correct size
        rct_box = rectangle.get_window_extent()
        txt_box = annotation.get_window_extent()
        visible = rct_box.width > txt_box.width and rct_box.height > txt_box.height
        annotation.set_visible(visible)
    

    This might not be perfect, yet, but I hope it gives you an idea on how to proceed.