1

I have a need to plot lines and a shaded region. The shaded region extends beyond the edge of the plot and I want to keep the plot limits as they were before the shaded region was plotted. I also need any autoscale changes to take into account the current axes so the limits should never shrink, and allow any lines plotted in the future to adjust the axes as they normally would had I not specified the xlim. The following code produces an image that shows what I get and what I expect. I have tried auto=True,False,None and they don't do what I expect for layered plots.

I really appreciate any help with this as I've been stuck at it for hours.

import matplotlib.pyplot as plt
import numpy as np

def shade(X, power1, power2, offset, color):
    '''
    This function plots a shaded region that is larger than the lines
    It should not adjust the axes limits as I do not want to display anything larger than the lines
    It needs to take into account previous axes limits so the axes limits never shrink
    It needs to leave the axes autoscaling in a way that will allow future line plots (done by plotter) to adjust the axes limits so nothing is cut off
    '''
    xlims = plt.xlim() #get the xlims to be reimposed after the shaded region is plotted
    xmin = min(X)
    xmax = max(X)
    X_enlarged = np.linspace(xmin, xmax * 2, 100)
    plt.fill_between(X_enlarged, X_enlarged ** power1 + offset, X_enlarged ** power2 + offset, alpha=0.2, color=color)
    #reimpose the xlims    
    plt.xlim(xlims, auto=None)  # this line is the key to how the xlims are adjusted

def plotter(xmax, offset, color):
    power1 = 2
    power2 = 2.5
    X1 = np.linspace(0, xmax, 100)
    Y1 = X1 ** power1
    Y2 = X1 ** power2
    plt.plot(X1, Y1 + offset, color=color)
    plt.plot(X1, Y2 + offset, color=color)
    shade(X1, power1, power2, offset, color=color)


plt.subplot(231)
plotter(xmax=10, offset=0, color='red')
plt.subplot(232)
plotter(xmax=15, offset=1000, color='blue')
plt.subplot(233)
plotter(xmax=12, offset=2000, color='green')
plt.subplot(234)
plotter(xmax=10, offset=0, color='red')
plotter(xmax=15, offset=1000, color='blue')
plotter(xmax=12, offset=2000, color='green')
plt.title('What I get\nwhen combined')
plt.subplot(236)
plotter(xmax=10, offset=0, color='red')
plotter(xmax=15, offset=1000, color='blue')
plotter(xmax=12, offset=2000, color='green')
plt.xlim(-1, 16)
plt.title('What I want\nwhen combined')
plt.tight_layout()
plt.show()

code_output

Matthew Reid
  • 332
  • 2
  • 9

1 Answers1

3

So I managed to solve my own problem. This matplotlib example was quite helpful.

Below is my full solution. I'll provide what I learnt in the hope that this helps someone else one day.

Why didn't my first code work as expected?

When you plot something in matplotlib, the way most people plot something is using plt.plot or plt.fill_between or one of the many other functions. These functions are just an API to make matplotlib easy to use and they automate a lot of work in the background including the process of adding the line or polygon to the line collection or poly collection. There are lots of collections that matplotlib uses. Unfortunately, when you plot something in this way, you don't have full control over the properties of that object when it is automatically added to the collection. In terms of autoscaling, what we do have control over is the global autoscaling rules for all objects. So using something like plt.xlim(auto=False) will set the autoscaling to not be updated when subsequent plots are added to the plot. Similarly using auto=True (which is default) will update the autoscaling and auto=None leaves it unchanged from whatever it was. kwargs like scalex and scaley seem to do a similar thing and work globally. What I needed is for the shaded region (the polygon) to be left off the list when scaling limits were calculated, but for the global autoscaling rules to be unchanged so that if anything was plotted later the scale would adjust to include it. None of the autoscaling keywords afforded me this level of control on each object I add to the plot.

How does the solution below work?

The variable "polygon" is created in the way that PolyCollection wants to receive a polygon. It is effectively a list of coordinates from left to right along the bottom of the polygon then from right to left across the top. By using add_collection I have the ability to use the kwarg autolim=False. This changes how the polygon is added to the PolyCollection and ensures that it is left off the list of objects which are considered when autoscaling looks at all the objects on the plot. Importantly, the autoscaling properties are not changed for the whole plot so when I add something else (like the sine wave line) it is considered when autoscaling recalculates the limits.

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import PolyCollection


def shade(X, power1, power2, offset, color):
    x = np.linspace(min(X), max(X) * 2, 100)
    y1 = x ** power1 + offset
    y2 = x ** power2 + offset
    polygon = np.column_stack([np.hstack([x, x[::-1]]), np.hstack([y1, y2[::-1]])])  # this is equivalent to fill as it makes a polygon
    col = PolyCollection([polygon], color=color, alpha=0.2)
    plt.gca().add_collection(col, autolim=False)


def plotter(xmax, offset, color):
    power1 = 2
    power2 = 2.5
    X1 = np.linspace(0, xmax, 100)
    Y1 = X1 ** power1
    Y2 = X1 ** power2
    plt.plot(X1, Y1 + offset, color=color)
    plt.plot(X1, Y2 + offset, color=color)
    shade(X1, power1, power2, offset, color=color)


final_lineX = np.linspace(0, 22, 100)
final_lineY = np.sin(final_lineX) * 500

plt.subplot(231)
plotter(xmax=10, offset=0, color='red')
plt.subplot(232)
plotter(xmax=15, offset=1000, color='blue')
plt.subplot(233)
plotter(xmax=12, offset=2000, color='green')
plt.subplot(223)
plotter(xmax=10, offset=0, color='red')
plotter(xmax=15, offset=1000, color='blue')
plotter(xmax=12, offset=2000, color='green')
plt.title('combined')
plt.subplot(224)
plotter(xmax=10, offset=0, color='red')
plotter(xmax=15, offset=1000, color='blue')
plotter(xmax=12, offset=2000, color='green')
plt.plot(final_lineX, final_lineY)
plt.title('combined\nwith extra line')
plt.subplots_adjust(left=0.11, bottom=0.07, right=0.95, top=0.95, wspace=0.41, hspace=0.44)
plt.show()

solution_output

Matthew Reid
  • 332
  • 2
  • 9