Limited decoration margin in "constrained" layout?

94 Views Asked by At

I want to understand the decoration space limits of the "constrained" layout engine of Matplotlib. In my use case, I have to add a lot of decoration (such as different axes ticks and labels) to the right of the plot and I am running into limits that I cannot find documented.

The little test below shows an "x" being moved more and more to the right. Somewhere around x_pos=1.3, constrained starts to move the "x" out of the visible area. Another observation is that a tiny bit of window resize fixes this, i.e. it brings the "x" back to the visible.

Do you have any advises how to tame the beast?

from matplotlib import pyplot as plt


TEST_DECORATION = dict(s="x", horizontalalignment="center", verticalalignment="center")

def decorated_plot(x_pos: float):
    """Create (empty) plot w/ decoration at defined horizontal axes position."""
    fig = plt.figure(layout="constrained")
    ax = fig.add_subplot()
    ax.text(x=x_pos, y=0.5, transform=ax.transAxes, **TEST_DECORATION)
    ax.set_title(f"x = {x_pos}")
    plt.show()


def main():
    # explore the behavior for different values...
    for x_pos in [1.1, 1.2, 1.25, 1.3, 1.32, 1.4]:
        decorated_plot(x_pos)


if __name__ == "__main__":
    main()
3

There are 3 best solutions below

1
RuthC On BEST ANSWER

I think the problem comes from defining your "x" position relative to the size of the axes. Each time constrained layout runs, the size of the axes changes and so the location of the "x" relative to the axes also changes. I'm not sure but I think constrained layout looks at where artists currently are in the figure, rather than directly knowing that the "x" should be a certain fraction of the axes size out. You can get a more robust result by putting it a fixed distance from the side of the axes, if that suits your use case. annotate gives a lot more options than text for controlling the position. For example, here I put the "x" a certain multiple of the font size from the right of the axes:

from matplotlib import pyplot as plt


TEST_DECORATION = dict(horizontalalignment="center", verticalalignment="center")

def decorated_plot(x_pos: float):
    """
    Create (empty) plot w/ decoration at defined distance in font sizes
    from the right of the axes.
    """
    fig = plt.figure(layout="constrained")
    ax = fig.add_subplot()
    ax.annotate("x", (1, 0.5), xycoords=ax.transAxes, xytext=(x_pos, 0),
                textcoords='offset fontsize', **TEST_DECORATION)
    ax.set_title(f"x = {x_pos}")
    plt.show()


def main():
    # explore the behavior for different values...
    for x_pos in range(5, 41, 5):
        decorated_plot(x_pos)


if __name__ == "__main__":
    main()

Sample outputs:

enter image description here

enter image description here

0
RuthC On

When you resize the window the figure is drawn again. Each time the figure is drawn, the constrained layout algorithm runs a couple of times. So by resizing you give the algorithm more chances to figure out where everything should be. So one option may be to make the layout engine run before plt.show() with

fig.get_layout_engine().execute(fig)
1
RuthC On

Another option might be to set up a second axes just to contain the annotations. Use set_axis_off to make its frame, ticks, etc invisible.

I do not think this gives you the exact “x_pos”, but I do not know how much that matters for your use case.

from matplotlib import pyplot as plt


TEST_DECORATION = dict(s="x", horizontalalignment="center", verticalalignment="center")

def decorated_plot(x_pos: float):
    """Create (empty) plot w/ decoration at defined horizontal axes position."""
    fig, (ax_main, ax_txt) = plt.subplots(ncols=2, layout="constrained", width_ratios=[1, x_pos-1])
    ax_txt.set_axis_off()
    ax_txt.text(x=1, y=0.5, transform=ax_txt.transAxes, **TEST_DECORATION)
    ax_main.set_title(f"x = {x_pos}")
    plt.show()


def main():
    # explore the behavior for different values...
    for x_pos in [1.1, 1.2, 1.25, 1.3, 1.32, 1.4, 1.5, 1.6, 1.8]:
        decorated_plot(x_pos)


if __name__ == "__main__":
    main()

enter image description here

enter image description here