Choose to plot the line over the "edge" of the graph instead (and loop around), instead of across the graph

Viewed 325

So, I have a plot of the dihedral angle of a bond. The y-axis is only from 0-360, the x-axis is the frame (think timestep). I need the plot to "loop" back around to zero if the value goes above 360, and to plot the shortest distance between two points (if need be going over the edge of the graph and "looping" back around instead of across the graph).
two plots: d5 and d3

The plot of d3 looks okay, but in reality needs to jump over the edge of the graph instead of across it.

the plot of d5 has a significant problem, for a small rotation there is a massive jump only because it happens to go just below zero degrees.

I would like for both these plots to plot towards the bottom (towards zero) and reappear at the top of the plot, effectively choosing the shortest distance between data points. I do not want solutions involving translation of the plots to remove these artefacts (it works, I've done it, but you loose information on the true value of the angle). Solutions that can plot "below zero" (so a y-axis from 300 to 360|0 to 200 to 300) are also great. Solutions using other libraries are perfectly fine. If needed I can provide the dataset.

Example of what I'd like it to do (green line) Example of what I'd like it to do (green line)

I have tried to find similar solutions to no avail. The questions regarding periodic boundaries use numpy dataset mask to hide certain jumps, but they have continuous functions (where as mine are "jumpy").

Thank you for any help, I'd really appreciate it.

Datasets(made them a little smaller than on the graph, keeping only the skips):

D3:

x = [41.0, 43.0, 45.0, 47.0, 49.0, 51.0, 53.0, 55.0, 57.0, 59.0, 61.0, 63.0, 65.0, 67.0, 69.0, 71.0, 73.0, 75.0, 77.0, 79.0, 81.0, 83.0, 85.0, 87.0, 89.0, 91.0, 93.0, 95.0, 97.0, 99.0, 101.0, 103.0, 105.0, 107.0, 109.0, 111.0, 113.0, 115.0, 117.0, 119.0, 121.0, 123.0, 125.0, 127.0, 129.0, 131.0, 133.0, 135.0, 137.0, 139.0, 141.0, 143.0, 145.0, 147.0, 149.0, 151.0, 153.0, 155.0, 157.0, 159.0]

y = [45.6501, 37.4855, 40.4035, 51.4948, 55.8648, 48.9723, 60.4494, 42.7136, 20.6929, 36.7847, 44.4601, 54.04, 52.4895, 45.1991, 46.8203, 44.5827, 65.8803, 53.5398, 69.5158, 46.5372, 37.1557, 43.9031, 39.9325, 35.5248, 34.3531, 57.8377, 37.9208, 26.6508, 27.2333, 49.3798, 47.8627, 54.2795, 50.0892, 40.9849, 37.4014, 300.7947, 299.4254, 288.5113, 313.2906, 319.0095, 291.0726, 308.075, 298.451, 311.1485, 320.4832, 303.9229, 310.4584, 325.6287, 307.7328, 301.5581, 308.7813, 308.6791, 305.1343, 307.5148, 334.6374, 310.476, 315.6943, 326.0586, 298.6766, 305.6225]

Minimum working example:

import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot(x, y, linewidth = 1.2, label = 'd3')
ax.set_yticks([t for t in range(0,390,30)])
ax.set_xticks([t for t in range(50,200,50)])
ax.legend(loc='lower right',prop={'size': 14})
plt.show()
2 Answers

Using basic Python, as indicated by your lists, and not higher libraries like numpy, you can separate the two parts of the plot with basic functions. However, coming to think about your specific problem, you might prefer a polar plot:

from matplotlib import pyplot as plt

#two subplots with two different approaches
fig, (ax1, ax2) = plt.subplots(2, figsize=(5, 10))

#first approach - separating the list at the jump point
ymin = 0
ymax = 360

#pseudo-threshold calculation, just the first index in your list with a value above the threshold
threshold = 100
breakpoint = next(a[0] for a in enumerate(y) if a[1] > threshold)

#separating the lists at this breakpoint, creating intermediate point
y1 = y[:breakpoint] + [ymin]
y2 = [ymax] + y[breakpoint:]
x12 = 0.5 * (x[breakpoint-1] + x[breakpoint])
x1 = x[:breakpoint] + [x12]
x2 = [x12] + x[breakpoint:]

#plotting of the upper subplot
ax1.plot(x1, y1, c="r", label="jump")
ax1.plot(x2, y2, c="r")
ax1.legend()
ax1.set_ylim(ymin, ymax)


#second approach - a polar plot    
#convert deg into rad, here with numpy
import numpy as np
angle = np.deg2rad(y)

#plot the second subplot using polar coordinates
ax2 = plt.subplot(212, projection='polar')
ax2.plot(angle, x, c="r", label = "same jump")

#making it look nicer with clockwise rotation and 0 degree at the top
ax2.set_theta_direction(-1)
ax2.set_theta_zero_location('N')
ax2.set_rlabel_position(180)
ax2.set_ylim(0.9 * x[0], 1.1 * x[-1])
ax2.legend(loc=(-0.07,0.97))


plt.show()

which gives you both views for comparison: enter image description here

I have been thinking about the other problem - how to detect the transitions. Usually, peak detection of signals is difficult but in your case, the condition is rather simple: If the change between two data points is more than 180°, then you should plot it over the border. Here now my suggestion using numpy in a rather awkward way with a loop. I have been asking for suggestions of how to improve the segment extraction, alas, no enlightening answers came. At least the code works.

import numpy as np
from matplotlib import pyplot as plt


fig, ax = plt.subplots(figsize=(10, 5))

ymin = 0
ymax = 360
colour = "blue"

xarr, yarr = np.asarray([x, y])

#create index array for all points with jumps by more than 180 degrees
ind = list(np.where(np.abs(np.diff(yarr, append=yarr[-1])) > np.mean([ymin, ymax])))[0]

#if ind is not empty extract segment
if ind.size:
    #found breakpoints 
    for i, j in enumerate(ind):
        #first segment
        if i==0:
            #first trace
            xcurr = np.copy(xarr[:j+2])
            ycurr = np.copy(yarr[:j+2])
            xcurr[-1] = np.mean(xcurr[-2:]) 
            ycurr[-1] = (ymin, ymax)[ycurr[-1]<np.mean([ymin, ymax])]
        #all following segments  
        else:
            xcurr = np.copy(xarr[ind[i-1]:j+2])
            ycurr = np.copy(yarr[ind[i-1]:j+2])
            xcurr[0] = np.mean(xcurr[:2]) 
            ycurr[0] = (ymin, ymax)[ycurr[0]<np.mean([ymin, ymax])]
            xcurr[-1] = np.mean(xcurr[-2:]) 
            ycurr[-1] = (ymin, ymax)[ycurr[-1]<np.mean([ymin, ymax])]
        
        plt.plot(xcurr, ycurr, c=colour)
            
    #last segment
    xcurr = np.copy(xarr[j:])
    ycurr = np.copy(yarr[j:])
    xcurr[0] = np.mean(xcurr[:2]) 
    ycurr[0] = (ymin, ymax)[ycurr[0]<np.mean([ymin, ymax])]
   
else:
    #ind was empty - plot entire array
    xcurr = np.copy(xarr)
    ycurr = np.copy(yarr)

plt.plot(xcurr, ycurr, c=colour)

plt.ylim(ymin, ymax)
plt.yticks(np.linspace(ymin, ymax, 13))
plt.show()

Sample output with the test data below for multiple jumps

enter image description here

Test data:

x = [41.0, 43.0, 45.0, 47.0, 49.0, 51.0, 53.0, 55.0, 57.0, 59.0, 61.0, 63.0, 65.0, 67.0, 69.0, 71.0, 73.0, 75.0, 77.0, 79.0, 81.0, 83.0, 85.0, 87.0, 89.0, 91.0, 93.0, 95.0, 97.0, 99.0, 101.0, 103.0, 105.0, 107.0, 109.0, 111.0, 113.0, 115.0, 117.0, 119.0, 121.0, 123.0, 125.0, 127.0, 129.0, 131.0, 133.0, 135.0, 137.0, 139.0, 141.0, 143.0, 145.0, 147.0, 149.0, 151.0, 153.0, 155.0, 157.0, 159.0]
y = [45.6501, 37.4855, 40.4035, 31.4948, 155.8648, 148.9723, 180.4494, 142.7136, 220.6929, 236.7847, 244.4601, 254.04, 252.4895, 245.1991, 246.8203, 244.5827, 265.8803, 253.5398, 269.5158, 246.5372, 237.1557, 243.9031, 239.9325, 235.5248, 234.3531, 257.8377, 37.9208, 26.6508, 27.2333, 349.3798, 347.8627, 354.2795, 350.0892, 340.9849, 337.4014, 300.7947, 99.4254, 88.5113, 13.2906, 19.0095, 191.0726, 208.075, 198.451, 111.1485, 120.4832, 103.9229, 110.4584, 125.6287, 107.7328, 101.5581, 108.7813, 108.6791, 105.1343, 107.5148, 134.6374, 110.476, 15.6943, 26.0586, 98.6766, 105.6225]
Related