pythonmatplotlibcharts

Superimpose plot with background (image) chart


I am trying to use an existing graph as a background for new data that I want to plot on top of the graph. I have been able to do so when using a graph with all information contained within the axes and using the extent parameter of plt.imshow because then I just have to scale the image.

I would like to scale and shift the background graph. Replotting the background is not an option in the real use case.

Here is what I tried so far :

  1. Generation of a background graph (reproducible example)
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([0, 5, 10], [8, 5, 12])
ax.set_xlim(0, 20)
ax.set_ylim(0, 15)
ax.set_title('Background graph')
fig.show()
fig.savefig('bg_graph.png')

background_graph

  1. Use plt.imshow() to add the background graph and then superimpose my data.
bg_img = plt.imread('bg_graph.png')
fig, ax = plt.subplots()
ax.imshow(bg_img, extent=[0,50,0,50])
ax.scatter([4.9, 5.2], [7, 4.9])
fig.show()
fig.savefig('result.png')

superimposed_graph

I have made a mockup of the expected result using Excel : expected_result

Is there a method to stretch a new graph onto existing axis (from an image) in order to plot new pieces of data ? I assume that the coordinates of the axis in the image are known or can be guessed through trial an error. One way to rephrase this is to say that I would like to stretch the new plot to the image and not the other way around.


Solution

  • We can follow this answer to a related question and adapt it to your needs (see code comments for explanations):

    import matplotlib.pyplot as plt
    
    bg_img = plt.imread('stackoverflow/bg.png')  # TODO: Adjust as necessary
    bg_width, bg_xlim, bg_ylim = 6, (0, 20), (0, 15)
    
    # Create a figure with the same aspect ratio and scale as the image.
    # This provides the axes in which we will plot our new data
    figsize = (bg_width, bg_width * bg_img.shape[0] / bg_img.shape[1])
    fig, axes = plt.subplots(nrows=1, ncols=1, figsize=figsize)
    axes.patch.set_alpha(0.0)  # Make new figure's area transparent
    axes.set_xlim(*bg_xlim)  # Adjust limits to background's limits
    axes.set_ylim(*bg_ylim)
    axes.scatter([4.9, 5.2], [7, 4.9], color='red')  # Plot our new data
    # Optionally, turn off axes, as we already have them from
    # the background and they will not match perfectly:
    plt.axis('off')
        
    background_ax = plt.axes([0, 0, 1, 1])  # Create dummy subplot for background
    background_ax.set_zorder(-1)  # Set background subplot behind the other
    background_ax.imshow(bg_img, aspect='auto')  # Show background image
    plt.axis('off')  # Turn off axes that surround the background
    

    For me, using the background image that you shared and loading it as bg.png results in the following plot: plotting result with new data superimposed on background

    What if adjusting the whitespace is necessary?

    Luckily, the layout of the whitespace in your background image seems to match Matplotlib's defaults. If that was not the case, however, we could use subplots_adjust() on the foreground plot, together with a bit of trial and error, to make the axes of the foreground plot and background image align as perfectly as possible. In this case, I would initially leave the axes of the foreground plot turned on (and thus comment out the first plt.axis('off') in the code above) to make adjustments easier.

    To demonstrate this, I created a version of your background image with additional green padding (called bg_padded.png in the code below), which looks as follows: background image with additional green padding

    I then adjusted the code from above as follows:

    import matplotlib.pyplot as plt
    
    bg_img = plt.imread('stackoverflow/bg_padded.png')  # TODO: Adjust as necessary
    bg_width, bg_xlim, bg_ylim = 7.5, (0, 20), (0, 15)
    
    # Create a figure with the same aspect ratio and scale as the image.
    # This provides the axes in which we will plot our new data
    figsize = (bg_width, bg_width * bg_img.shape[0] / bg_img.shape[1])
    fig, axes = plt.subplots(nrows=1, ncols=1, figsize=figsize)
    # Adjust padding of foreground plot to padding of background image
    plt.subplots_adjust(left=.2, right=.82, top=.805, bottom=0.19)
    axes.patch.set_alpha(0.0)  # Make new figure's area transparent
    axes.set_xlim(*bg_xlim)  # Adjust limits to background's limits
    axes.set_ylim(*bg_ylim)
    axes.scatter([4.9, 5.2], [7, 4.9], color='red')  # Plot our new data
    # Optionally, turn off axes, as we already have them from
    # the background and they will not match perfectly:
    # plt.axis('off')
        
    background_ax = plt.axes([0, 0, 1, 1])  # Create dummy subplot for background
    background_ax.set_zorder(-1)  # Set background subplot behind the other
    background_ax.imshow(bg_img, aspect='auto')  # Show background image
    plt.axis('off')  # Turn off axes that surround the background
    

    Changes are:

    This time, I also left the first plt.axis('off') commented out, as mentioned above, to see and to show how well the axes of the background image and the foreground plot match. The result looks as follows:

    plotting result with new data superimposed on padded background