Skip to content

[ENH]: Separate autoscale_x_left and autoscale_x_right (and likewise for y) #30585

Open

Description

@anntzer

Problem

Currently, one can control separately whether autoscaling is applied to the x-axis and to the y-axis, but not whether it is only applied to the left or the right of the x-axis (or likewise to the bottom or the top of the y-axis). Yet this would be useful, e.g. in the case where the left limit is forced to zero whereas the right limit should follow autoscaling.

Concretely, I often have the following pattern:

ax = <create axes or fetch them from axes array>
ax.set(title="the title", xlabel="the label", xlim=(lo, hi), ...)
ax.plot(...)

yet if I want to force the left x-limit to 0 while leaving the right one autoscaled, I have to put the set_xlim() call after all the plotting, which splits axes configuration in two places. (One can also put the entire ax.set(...) call at the bottom, but this can be less clear especially when there are multiple axes being set up, as ax.set(title=...) also serves as self-commenting code to introduce what this axes will be about.)

Proposed solution

Make it possible to separately control whether the left and the right x-limits are autoscaled, and ideally make ax.set_xlim(0, None) only turn off autoscaling on the left x-limit (via the relevant deprecation process).

Note that one question is how to compute the autoscaled right limit when the left limit is already forced. For simplicity, I believe this should behave as if the left limit wasn't forced, i.e. the right limit should be equal to the maximum value plus a margin equal to 0.05 (or whatever has been configured) times the entire x-span of the data; this way the behavior is as if ax.set_xlim(0, None) had been called after the data is plotted. An alternative would be to not use the entire x-span, but only the span of data from x=0 to the maximum value (this would differ if some x-values are negative).

Activity

  1. Alverok commented on Oct 2, 2025

    @Alverok

    Hello everyone,

    I'm Alverok and I was looking into this enhancement request (Issue #30585) as a potential first contribution to understand the Matplotlib codebase. I've performed some testing on the current version of Matplotlib (3.8.3 in my environment after a dev install, which is what the editable install would have given you).

    It appears that the behavior described in the "Proposed solution"—specifically, where the autoscaled limit uses a margin calculated from the entire data span even when one limit is fixed—might already be the current default behavior.

    Here's a test script I used to investigate:

    import matplotlib.pyplot as plt
    import numpy as np
    
    fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(6, 9))
    
    # --- Scenario 1: Data starts above forced limit (original poster's example type) ---
    # Data range: [2, 10], Span: 8. Expected 5% margin: 0.4.
    # Total autoscaled range from data: [1.6, 10.4] (if fully autoscaled)
    ax1.plot(np.linspace(2, 10, 100), np.sin(np.linspace(2, 10, 100)))
    ax1.set_xlim(left=0, right=None)
    ax1.set_title("Data: [2, 10], xlim(0, None)")
    
    # --- Scenario 2: Data with negative values (explicitly testing span calculation) ---
    # Data range: [-5, 10], Span: 15. Expected 5% margin: 0.75.
    # Total autoscaled range from data: [-5.75, 10.75] (if fully autoscaled)
    ax2.plot(np.linspace(-5, 10, 100), np.sin(np.linspace(-5, 10, 100)))
    ax2.set_xlim(left=0, right=None) # Left forced to 0
    ax2.set_title("Data: [-5, 10], xlim(0, None)")
    
    # --- Scenario 3: Fully autoscaled for direct comparison ---
    # Same data as Scenario 2, but no explicit xlim set.
    ax3.plot(np.linspace(-5, 10, 100), np.sin(np.linspace(-5, 10, 100)))
    ax3.set_title("Data: [-5, 10], Fully Autoscaled")
    
    plt.tight_layout()
    plt.show()

    Here is the resulting plot from the test script:

    Image

    As the plot shows:

    • For Scenario 1 (Data [2, 10], xlim(0, None)): The x-axis limits are [0, 10.4]. The right limit correctly includes a 5% margin (0.4) calculated from the data's span (10 - 2 = 8).
    • For Scenario 2 (Data [-5, 10], xlim(0, None)): The x-axis limits are [0, 10.75]. Here, the right limit correctly includes a 5% margin (0.75) calculated from the full data span (10 - (-5) = 15), even though the left limit is manually set to 0. This directly addresses the nuance mentioned in the issue: "this should behave as if the left limit wasn't forced, i.e. the right limit should be equal to the maximum value plus a margin... times the entire x-span of the data."
    • For Scenario 3 (Data [-5, 10], Fully Autoscaled): The x-axis limits are [-5.75, 10.75], showing the full autoscaling behavior for comparison.

    This observed behavior directly aligns with the "Proposed solution" described in the issue, particularly regarding how the None limit should be calculated using the full data range for margins.

    Upon further investigation of the codebase, I traced the logic to lib/matplotlib/axis.py, within the Axis._set_lim method. The relevant section that handles None limits is:

    Image

    This code explicitly retrieves old0 and old1 (which are the fully autoscaled limits determined by Axes.autoscale_view() during the drawing process) and substitutes them if None was passed. This effectively re-uses the full autoscale calculation for the None side.

    Given these findings, it seems the desired behavior outlined in this enhancement request is already present in the current version of Matplotlib. Am I misunderstanding a more subtle aspect of the request, or is it possible this has been implemented since the issue was opened?

    Thanks for your time and guidance!

  2. rcomer commented on Oct 2, 2025

    @rcomer
    Member

    Hi @Alverok thank you for your willingness to work on this. I think the feature request is about what happens if you set the limits before calling plot. In that case you do not get the desired result.

    As an aside, the most recent release of Matplotlib is v3.10.6. If you installed from an up-to-date git checkout you should get a version that starts 3.11.0.dev. So something seems to have gone wrong there.

  3. Alverok commented on Oct 2, 2025

    @Alverok

    Hi, @rcomer, thank you so much for the clarification and the heads-up about my environment! I've synced my repository to the latest main branch, corrected my setup, and I'm now on the correct development version - 3.11.0.dev1400+g652012d5b.

    With the updated environment, I can now confirm the behavior you described when reversing the order of operations. I ran a test script where one plot sets the limits before plotting, and the other sets them after.

    # new_test_plot.py
    import matplotlib.pyplot as plt
    import numpy as np
    
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(6, 6))
    
    # --- Scenario 1: set_xlim BEFORE plot ---
    # This demonstrates the issue.
    ax1.set_xlim(left=0, right=None) # Set limit on an empty plot
    ax1.plot(np.linspace(2, 10, 100), np.sin(np.linspace(2, 10, 100)))
    ax1.set_title("Scenario 1: set_xlim() BEFORE plot()")
    
    
    # --- Scenario 2: set_xlim AFTER plot (for comparison) ---
    # This shows the currently working behavior.
    ax2.plot(np.linspace(2, 10, 100), np.sin(np.linspace(2, 10, 100)))
    ax2.set_xlim(left=0, right=None) # Set limit after data is present
    ax2.set_title("Scenario 2: set_xlim() AFTER plot() (Original Test)")
    
    plt.tight_layout()
    plt.show()

    This script produces the following plot:
    Image
    Image

    As you can see in Scenario 1, when set_xlim is called on the empty axes, the view is fixed to a default [0, 1] range, and the subsequently plotted data (from x=2 to x=10) is not visible. This confirms the issue perfectly.

    I'd be happy to try and investigate a solution. Do you have any pointers on where in the codebase one might start to implement the "sticky" None behavior for autoscaling?

    Thanks again for all your help!

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions