Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use subplots to zoom into timeseries or how I can draw lines outside of axis borders

I would like to generate a plot like this with matplotlib

timescale http://www.imagenetz.de/img.php?file=37d8879009.jpg&pid=

Currently I just do the 3 subplots with matplotlib and add the red lines in inkscape. I found out that I can create the dashed rectangles with Rectangle.

ax.add_patch(Rectangle((25, -1.4), 3, 1.3, edgecolor='red',
                       fill=False, linestyle='dashed'))

I haven't found anything yet to draw the lines connecting the plots. Is there a function that can draw across axis borders?

like image 739
Max Linke Avatar asked Jun 29 '14 14:06

Max Linke


People also ask

How do I zoom out on a Matplotlib plot?

Press the right mouse button to zoom, dragging it to a new position. The x axis will be zoomed in proportionately to the rightward movement and zoomed out proportionately to the leftward movement. The same is true for the y axis and up/down motions.

How do you zoomed a portion of image and insert in the same plot in Matplotlib?

MatPlotLib with Python Create x and y points, using numpy. To zoom a part of an image, we can make data for x and y points, in that range. Plot x and y points (Step 1), using the plot() method with lw=2, color red and label. Use the legend() method to place text for the plot, Main curve.

What does the Pyplot method subplots return?

pyplot. subplots method provides a way to plot multiple plots on a single figure. Given the number of rows and columns , it returns a tuple ( fig , ax ), giving a single figure fig with an array of axes ax .

What is sharey true?

What is sharey true? Controls sharing of properties among x ( sharex ) or y ( sharey ) axes: True or 'all': x- or y-axis will be shared among all subplots. False or 'none': each subplot x- or y-axis will be independent.


2 Answers

Short answer

We can exploit plt.annotate() to draw lines outside axis borders in figure coordinates.

Long answer

First define a helper function:

from matplotlib.patches import Rectangle

def zoomingBox(ax1, roi, ax2, color='red', linewidth=2, roiKwargs={}, arrowKwargs={}):
    '''
    **Notes (for reasons unknown to me)**
    1. Sometimes the zorder of the axes need to be adjusted manually...
    2. The figure fraction is accurate only with qt backend but not inline...
    '''
    roiKwargs = dict([('fill',False), ('linestyle','dashed'), ('color',color), ('linewidth',linewidth)] + roiKwargs.items())
    ax1.add_patch(Rectangle([roi[0],roi[2]], roi[1]-roi[0], roi[3]-roi[2], **roiKwargs))
    arrowKwargs = dict([('arrowstyle','-'), ('color',color), ('linewidth',linewidth)] + arrowKwargs.items())
    srcCorners = [[roi[0],roi[2]], [roi[0],roi[3]], [roi[1],roi[2]], [roi[1],roi[3]]]
    dstCorners = ax2.get_position().corners()
    srcBB = ax1.get_position()
    dstBB = ax2.get_position()
    if (dstBB.min[0]>srcBB.max[0] and dstBB.max[1]<srcBB.min[1]) or (dstBB.max[0]<srcBB.min[0] and dstBB.min[1]>srcBB.max[1]):
        src = [0, 3]; dst = [0, 3]
    elif (dstBB.max[0]<srcBB.min[0] and dstBB.max[1]<srcBB.min[1]) or (dstBB.min[0]>srcBB.max[0] and dstBB.min[1]>srcBB.max[1]):
        src = [1, 2]; dst = [1, 2]
    elif dstBB.max[1] < srcBB.min[1]:
        src = [0, 2]; dst = [1, 3]
    elif dstBB.min[1] > srcBB.max[1]:
        src = [1, 3]; dst = [0, 2]
    elif dstBB.max[0] < srcBB.min[0]:
        src = [0, 1]; dst = [2, 3]
    elif dstBB.min[0] > srcBB.max[0]:
        src = [2, 3]; dst = [0, 1]
    for k in range(2):
        ax1.annotate('', xy=dstCorners[dst[k]], xytext=srcCorners[src[k]], xycoords='figure fraction', textcoords='data', arrowprops=arrowKwargs)

Then we can do this:

import matplotlib.pyplot as plt

axs = plt.subplots(2, 2)[1]
axs[1,1].plot(rand(100))
zoomingBox(axs[1,1], [40,60,0.1,0.9], axs[0,0])
zoomingBox(axs[1,1], [10,30,0.1,0.9], axs[1,0], color='orange')

enter image description here

like image 90
herrlich10 Avatar answered Sep 21 '22 21:09

herrlich10


Based on herrilich10's answer, here is full implementation. The figure now can have subplots only in a row (or in a column) [see Figure 1 below], or some subplots around the original one [see Figure 2 below].

I also fixed 2 bugs: TypeError: can only concatenate list (not "dict_items") to list and UnboundLocalError: local variable 'dst' referenced before assignment. The codes have been tested in Python 3.7.6 and matplotlib 3.1.3 with qt/pdf backend.

Remark: the variable roi is different from that in herrilich10's answer. I use the default order from matplotlib [xmin, ymin, xmax, ymax] to pass values to this variable. [see class matplotlib.transforms.Bbox]

Here's how to define the helper function:

from matplotlib.patches import Rectangle
    
def zoom_outside(srcax, roi, dstax, color="red", linewidth=2, roiKwargs={}, arrowKwargs={}):
    '''Create a zoomed subplot outside the original subplot
    
    srcax: matplotlib.axes
        Source axis where locates the original chart
    dstax: matplotlib.axes
        Destination axis in which the zoomed chart will be plotted
    roi: list
        Region Of Interest is a rectangle defined by [xmin, ymin, xmax, ymax],
        all coordinates are expressed in the coordinate system of data
    roiKwargs: dict (optional)
        Properties for matplotlib.patches.Rectangle given by keywords
    arrowKwargs: dict (optional)
        Properties used to draw a FancyArrowPatch arrow in annotation
    '''
    roiKwargs = dict([("fill", False), ("linestyle", "dashed"),
                      ("color", color), ("linewidth", linewidth)]
                     + list(roiKwargs.items()))
    arrowKwargs = dict([("arrowstyle", "-"), ("color", color),
                        ("linewidth", linewidth)]
                       + list(arrowKwargs.items()))
    # draw a rectangle on original chart
    srcax.add_patch(Rectangle([roi[0], roi[1]], roi[2]-roi[0], roi[3]-roi[1], 
                            **roiKwargs))
    # get coordinates of corners
    srcCorners = [[roi[0], roi[1]], [roi[0], roi[3]],
                  [roi[2], roi[1]], [roi[2], roi[3]]]
    dstCorners = dstax.get_position().corners()
    srcBB = srcax.get_position()
    dstBB = dstax.get_position()
    # find corners to be linked
    if srcBB.max[0] <= dstBB.min[0]: # right side
        if srcBB.min[1] < dstBB.min[1]: # upper
            corners = [1, 2]
        elif srcBB.min[1] == dstBB.min[1]: # middle
            corners = [0, 1]
        else:
            corners = [0, 3] # lower
    elif srcBB.min[0] >= dstBB.max[0]: # left side
        if srcBB.min[1] < dstBB.min[1]:  # upper
           corners = [0, 3]
        elif srcBB.min[1] == dstBB.min[1]: # middle
            corners = [2, 3]
        else:
            corners = [1, 2]  # lower
    elif srcBB.min[0] == dstBB.min[0]: # top side or bottom side
        if srcBB.min[1] < dstBB.min[1]:  # upper
            corners = [0, 2]
        else:
            corners = [1, 3] # lower
    else:
        RuntimeWarning("Cannot find a proper way to link the original chart to "
                       "the zoomed chart! The lines between the region of "
                       "interest and the zoomed chart wiil not be plotted.")
        return
    # plot 2 lines to link the region of interest and the zoomed chart
    for k in range(2):
        srcax.annotate('', xy=srcCorners[corners[k]], xycoords="data",
            xytext=dstCorners[corners[k]], textcoords="figure fraction",
            arrowprops=arrowKwargs)

Here's how to use it:

from matplotlib import pyplot as plt

# prepare something to plot
x = range(100)
y = [-100, -50, 0, 50, 100] * int(len(x)/5)

# create a figure
fig, axes = plt.subplots(3, 3)
plt.subplots_adjust(wspace=0.2, hspace=0.2)

# plot the main chart
axes[1, 1].plot(x, y)

# plot zoomed charts
zoom_outside(srcax=axes[1, 1], roi=[0, 80, 20, 100], dstax=axes[0, 0], color="C1")
zoom_outside(axes[1, 1], [40, 80, 60, 100], axes[0, 1], "C2")
zoom_outside(axes[1, 1], [80, 80, 100, 100], axes[0, 2], "C3")
zoom_outside(axes[1, 1], [0, -20, 20, 20], axes[1, 0], "C4")
zoom_outside(axes[1, 1], [80, -20, 100, 20], axes[1, 2], "C5")
zoom_outside(axes[1, 1], [0, -100, 20, -80], axes[2, 0], "C6")
zoom_outside(axes[1, 1], [40, -100, 60, -80], axes[2, 1], "C7")
zoom_outside(axes[1, 1], [80, -100, 100, -80], axes[2, 2], "C8")

plt.show()

Here's some demonstrations:

Figure 1

Figure 2

Enjoy it !

like image 34
yoursbh Avatar answered Sep 17 '22 21:09

yoursbh