Search code examples
pythonmatplotlibcheminformatics

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


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()

Solution

  • 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