Plotting energy levels in stacks, on top of one another - python

I have a code that gives me different energy levels. The code and the output is shown here:
from numpy import*
from matplotlib.pyplot import*
N=[0,1,2,3,4]
s=0.5
hw=1
def Energy(n):
if n%2==0:
if n==0:
j=0.5
E=[(n+1.5)*hw-0.1*(j-0.5)*0.5-0.0225*(j+0.5)*(j-0.5)]
return(E)
else:
l=list(range(0,n+2,2))
j1=[abs(l+s) for l in l ]
j2=[l-s for l in l]
j2=list(filter(lambda x:x>0,j2))
E1=[(n+1.5)*hw-0.1*(j-0.5)*0.5-0.0225*(j+0.5)*(j-0.5) for j in j1]
E2=[(n+1.5)*hw+0.1*(j+0.5)*0.5-0.0225*(j+0.5)*(j-0.5) for j in j2]
return(E1+E2)
else:
l=list(range(1,n+2,2))
j1=[abs(l+s) for l in l]
j2=[abs(l-s) for l in l]
E1=[(n+1.5)*hw-0.1*(j-0.5)*0.5-0.0225*(j+0.5)*(j-0.5) for j in j1]
E2=[(n+1.5)*hw+0.1*(j+0.5)*0.5-0.0225*(j+0.5)*(j-0.5) for j in j2]
return(E1+E2)
E=[]
for n in N:
E.extend(Energy(n))
E.sort()
print(E)
orbital=[r'$1s_{1/2}$',r'$1p_{3/2}$',r'$1p_{1/2}$',r'$1d_{5/2}$',r'$2s_{1/2}$',r'$1d_{3/2}$',r'$1f_{7/2}$',r'$2p_{3/2}$',r'$1f_{5/2}$',r'$2p_{1/2}$',r'$1g_{9/2}$',r'$2d_{5/2}$',r'$1g_{7/2}$',r'$3s_{1/2}$',r'$2d_{3/2}$']
x = arange(len(E))
fig, ax =subplots()
ax.scatter(x, E, s=900, marker="_", linewidth=2, zorder=3)
ax.grid(axis='y')
for xi,yi,tx in zip(x,E,orbital):
ax.annotate(tx, xy=(xi,yi), xytext=(7,-3), size=5,
ha="center",va='top', textcoords="offset points")
ax.margins(0.1)
ylabel('energy >>')
title('Nuclear Energy levels')
The output is a graph containing the energy levels but spread out parallel to the x axis
What I actually need is the levels to not be spread across. I want them in a stack, one on the top of another. I tried modifying this code for that, but to no avail. Can someone help with this?

Instead of
x = arange(len(E))
(just before fig, ax =subplots()) use
x = [1] * len(E)
to have the same x-coordinate for all your levels:
You will probably want to increase the parameter s=, too, in your ax.scatter() method, for example to 90000:
ax.scatter(x, E, s=90000, marker="_", linewidth=2, zorder=3)
and change position of annotations — slightly change parameters xy=, xytext= in your code
for xi,yi,tx in zip(x,E,orbital):
ax.annotate(tx, xy=(xi,yi), xytext=(7,-3), size=5,
ha="center",va='top', textcoords="offset points")
to (for example):
for xi, yi, tx in zip(x, E, orbital):
ax.annotate(tx, xy=(.65*xi, yi), xytext=(7, 3), size=5,
ha="center", va='top', textcoords="offset points")
and change the overall image size to increase gaps between levels — in your
fig, ax = subplots()
use the figsize= parameter:
fig, ax = subplots(figsize=(6.5, 12))
Finally, you may remove ticks from x-axis and add minor ticks to y-axis:
import matplotlib as mpl # this line is better to put near the start of your code
ax.set_xticks([])
ax.yaxis.set_minor_locator(mpl.ticker.MaxNLocator(50))

Related

The axes labels do not align with the matrix when using matshow

I used the following code to create the attached plot:
fig, ax = plt.subplots()
fig.set_figheight(50)
fig.set_figwidth(50)
ax.matshow(power_final_for_plotting, cmap='GnBu', origin='upper')
ax.set_xticks(time_periods)
ax.set_xticklabels(time_periods)
ax.set_yticks(sig_wave_height)
ax.set_yticklabels(sig_wave_height)
for i in range(len(time_periods)):
for j in range(len(amplitude)):
c = round(power_final_for_plotting[j, i],3)
ax.text(i, j, str(c), va='center', ha='center', size=27)
plt.tight_layout()
Here time_period and sig_wave_height are lists of integers. The axis labels do not align properly in this case (Check the top right of the image to see the labels as they are). How can I fix this? The labels are really small in this case:
The current scale definition, for example, gives the necessary amount of x-axis as a numerical value, but the scale name is displayed up to 15 because it is a numerical value. We need 30 tick points and tick names for each column. Since no data is provided, I have created sample data as appropriate. Also, the small font of the scale is due to the size of 50 inches.
import numpy as np
import matplotlib.pyplot as plt
power_final_for_plotting = np.random.rand(450).reshape(15,30)
time_periods = np.arange(0,15,0.5)
sig_wave_height = np.arange(0.5,8.0,0.5)
fig, ax = plt.subplots()
fig.set_figheight(6)
fig.set_figwidth(12)
ax.matshow(power_final_for_plotting, cmap='GnBu', origin='upper')
ax.set_xticks(range(len(time_periods)))
ax.set_xticklabels([str(x) for x in time_periods])
ax.set_yticks(range(len(sig_wave_height)))
ax.set_yticklabels([str(x) for x in sig_wave_height])
for i in range(len(time_periods)):
for j in range(len(sig_wave_height)):
c = round(power_final_for_plotting[j, i],3)
ax.text(i, j, str(c), va='center', ha='center', size=9)
plt.tight_layout()
#print(ax.get_xticklabels())
plt.show()

How to add values at the end of Radar Chart points?

so I am plotting a Radar Chart and I need to add values at the end of each point of the chart. Is there a way? Main area of code and df enlisted below.
"""PLOT GRAPH 1"""
categories=list(df)[0:]
N = len(categories)
categories=list(df)[0:]
N = len(categories)
values=df.iloc[1].values.flatten().tolist()
values += values[:1]
for_tick=df.iloc[0].values.flatten().tolist()
for_tick += for_tick[:1]
angles = [n / float(N) * 2 * pi for n in range(N)]
angles += angles[:1]
f, ax = plt.subplots(1,1,figsize=(8,6))
ax = plt.subplot(111, polar=True)
plt.xticks(angles[:-1], wrapped_labels , size=10)
ax.set_rlabel_position(0)
plt.yticks([0,25,50,75,100], color="grey", size=5)
plt.ylim(0,100)
plt.title('Suppliers hit in the test out of all supliers (' +number_of_suppliers+') (in %)')
ax.plot(angles, values, color='#ffe600', linewidth=1, linestyle='solid')
ax.fill(angles, values, color='#ffe600', alpha=0.3)
plt.savefig(r'C:\Radar\firmy.png',dpi=100)
plt.show()
So I managed to find a more manual way, as there were only five values to label, of how to do it, adding this line of code. If I find more intuitive and automatic way, I will update you :)
ax.annotate(labels[0], xy=(angles[0],values[0]), textcoords='data',size=5,ha="center", va="center")
ax.annotate(labels[1], xy=(angles[1],values[1]), textcoords='data',size=5,ha="center", va="center")
ax.annotate(labels[2], xy=(angles[2],values[2]), textcoords='data',size=5,ha="center", va="center")
ax.annotate(labels[3], xy=(angles[3],values[3]), textcoords='data',size=5,ha="center", va="center")
ax.annotate(labels[4], xy=(angles[4],values[4]), textcoords='data',size=5,ha="center", va="center")

two (or more) graphs in one plot with different x-axis AND y-axis scales in python

I want 3 graphs on one axes object, for example:
#example x- and y-data
x_values1=[1,2,3,4,5]
y_values1=[1,2,3,4,5]
x_values2=[-1000,-800,-600,-400,-200]
y_values2=[10,20,39,40,50]
x_values3=[150,200,250,300,350]
y_values3=[10,20,30,40,50]
#make axes
fig=plt.figure()
ax=fig.add_subplot(111)
now I want to add all three data sets to ax. But they shouldn't share any x- or y-axis (since then because of the diffenrent scales one would be way smaller thant the other. I need something like ax.twinx(), ax.twiny(), but both the x- and y-axis need to be independent.
I want to do this, because I want to put the two attached plots (and a third one, that is similar to the second one) in one plot ("put them on top of each other").
Plot1
Plot2
I then would put the x/y-labels (and/or ticks, limits) of the second plot on the right/top and the x/y-limits of another plot in the bottom/left. I dont need x/y-labels of the 3. plot.
How do I do this?
The idea would be to create three subplots at the same position. In order to make sure, they will be recognized as different plots, their properties need to differ - and the easiest way to achieve this is simply to provide a different label, ax=fig.add_subplot(111, label="1").
The rest is simply adjusting all the axes parameters, such that the resulting plot looks appealing.
It's a little bit of work to set all the parameters, but the following should do what you need.
import matplotlib.pyplot as plt
x_values1=[1,2,3,4,5]
y_values1=[1,2,2,4,1]
x_values2=[-1000,-800,-600,-400,-200]
y_values2=[10,20,39,40,50]
x_values3=[150,200,250,300,350]
y_values3=[10,20,30,40,50]
fig=plt.figure()
ax=fig.add_subplot(111, label="1")
ax2=fig.add_subplot(111, label="2", frame_on=False)
ax3=fig.add_subplot(111, label="3", frame_on=False)
ax.plot(x_values1, y_values1, color="C0")
ax.set_xlabel("x label 1", color="C0")
ax.set_ylabel("y label 1", color="C0")
ax.tick_params(axis='x', colors="C0")
ax.tick_params(axis='y', colors="C0")
ax2.scatter(x_values2, y_values2, color="C1")
ax2.xaxis.tick_top()
ax2.yaxis.tick_right()
ax2.set_xlabel('x label 2', color="C1")
ax2.set_ylabel('y label 2', color="C1")
ax2.xaxis.set_label_position('top')
ax2.yaxis.set_label_position('right')
ax2.tick_params(axis='x', colors="C1")
ax2.tick_params(axis='y', colors="C1")
ax3.plot(x_values3, y_values3, color="C3")
ax3.set_xticks([])
ax3.set_yticks([])
plt.show()
You could also standardize the data so it shares the same limits and then plot the limits of the desired second scale "manually".
This function standardizes the data to the limits of the first set of points:
def standardize(data):
for a in range(2):
span = max(data[0][a]) - min(data[0][a])
min_ = min(data[0][a])
for idx in range(len(data)):
standardize = (max(data[idx][a]) - min(data[idx][a]))/span
data[idx][a] = [i/standardize + min_ - min([i/standardize
for i in data[idx][a]]) for i in data[idx][a]]
return data
Then, plotting the data is easy:
import matplotlib.pyplot as plt
data = [[[1,2,3,4,5],[1,2,2,4,1]], [[-1000,-800,-600,-400,-200], [10,20,39,40,50]], [[150,200,250,300,350], [10,20,30,40,50]]]
limits = [(min(data[1][a]), max(data[1][a])) for a in range(2)]
norm_data = standardize(data)
fig, ax = plt.subplots()
for x, y in norm_data:
ax.plot(x, y)
ax2, ax3 = ax.twinx(), ax.twiny()
ax2.set_ylim(limits[1])
ax3.set_xlim(limits[0])
plt.show()
Since all data points have the limits of the first set of points, we can just plot them on the same axis. Then, using the limits of the desired second x and y axis we can set the limits for these two.
In this example, you can plot multiple lines in each x-y-axis, and legend each line.
import numpy as np
import matplotlib.pyplot as plt
X1 = np.arange(10)
X1 = np.stack([X1, X1])
Y1 = np.random.randint(1, 10, (2, 10))
X2 = np.arange(0, 1000, 200)
X2 = np.stack([X2, X2])
Y2 = np.random.randint(100, 200, (2, 5))
x_label_names = ['XXX', 'xxx']
y_label_names = ['YYY', 'yyy']
X1_legend_names = ['X1_legend1', 'X1_legend2']
X2_legend_names = ['X2_legend1', 'X2_legend2']
def plot_by_two_xaxis(X1, Y1, X2, Y2, x_label_names: list, y_label_names: list, X1_legend_names: list, X2_legend_names: list):
fig = plt.figure()
ax1s = []
ax2s = []
lines = []
j = 0
for i in range(len(X1)):
j += 1
ax1s.append(fig.add_subplot(111, label=f"{j}", frame_on=(j == 1)))
for i in range(len(X2)):
j += 1
ax2s.append(fig.add_subplot(111, label=f"{j}", frame_on=(j == 1)))
k = 0
for i in range(len(X1)):
lines.append(ax1s[i].plot(X1[i], Y1[i], color=f"C{k}")[0])
if i == 0:
ax1s[i].set_xlabel(x_label_names[0], color=f"C{k}")
ax1s[i].set_ylabel(y_label_names[0], color=f"C{k}")
ax1s[i].tick_params(axis='x', colors=f"C{k}")
ax1s[i].tick_params(axis='y', colors=f"C{k}")
else:
ax1s[i].set_xticks([])
ax1s[i].set_yticks([])
k += 1
for i in range(len(X1)):
lines.append(ax2s[i].plot(X2[i], Y2[i], color=f"C{k}")[0])
if i == 0:
ax2s[i].xaxis.tick_top()
ax2s[i].yaxis.tick_right()
ax2s[i].set_xlabel(x_label_names[1], color=f"C{k}")
ax2s[i].set_ylabel(y_label_names[1], color=f"C{k}")
ax2s[i].xaxis.set_label_position('top')
ax2s[i].yaxis.set_label_position('right')
ax2s[i].tick_params(axis='x', colors=f"C{k}")
ax2s[i].tick_params(axis='y', colors=f"C{k}")
else:
ax2s[i].set_xticks([])
ax2s[i].set_yticks([])
k += 1
ax1s[0].legend(lines, X1_legend_names + X2_legend_names)
plt.show()
plot_by_two_xaxis(X1, Y1, X2, Y2, x_label_names,
y_label_names, X1_legend_names, X2_legend_names)

Preventing plot joining when values "wrap" in matplotlib plots

I'm plotting right ascension ephemerides for planets, which have the property that they are cyclical: they hit a maximum value, 24, and then start again at 0. When I plot these using matplotlib, the "jump" from 24 to zero is joined so that I get horizontal lines running across my figure:
How can I eliminate these lines? Is there an approach in matplotlib, or perhaps a way to split the lists at between the points where the jump occurs.
Code to generate above figure:
from __future__ import division
import ephem
import matplotlib
import matplotlib.pyplot
import math
fig, ax = matplotlib.pyplot.subplots()
ax.set(xlim=[0, 24])
ax.set(ylim=[min(date_range), max(date_range)])
ax.plot([12*ep.ra/math.pi for ep in [ephem.Jupiter(base_date + d) for d in date_range]], date_range,
ls='-', color='g', lw=2)
ax.plot([12*ep.ra/math.pi for ep in [ephem.Venus(base_date + d) for d in date_range]], date_range,
ls='-', color='r', lw=1)
ax.plot([12*ep.ra/math.pi for ep in [ephem.Sun(base_date + d) for d in date_range]], date_range,
ls='-', color='y', lw=3)
Here is a generator function that finds the contiguous regions of 'wrapped' data:
import numpy as np
def unlink_wrap(dat, lims=[-np.pi, np.pi], thresh = 0.95):
"""
Iterate over contiguous regions of `dat` (i.e. where it does not
jump from near one limit to the other).
This function returns an iterator object that yields slice
objects, which index the contiguous portions of `dat`.
This function implicitly assumes that all points in `dat` fall
within `lims`.
"""
jump = np.nonzero(np.abs(np.diff(dat)) > ((lims[1] - lims[0]) * thresh))[0]
lasti = 0
for ind in jump:
yield slice(lasti, ind + 1)
lasti = ind + 1
yield slice(lasti, len(dat))
An example usage would be,
x = np.arange(0, 100, .1)
y = x.copy()
lims = [0, 24]
x = (x % lims[1])
fig, ax = matplotlib.pyplot.subplots()
for slc in unlink_wrap(x, lims):
ax.plot(x[slc], y[slc], 'b-', linewidth=2)
ax.plot(x, y, 'r-', zorder=-10)
ax.set_xlim(lims)
Which gives the figure below. Note that the blue lines (which utilize unlink_wrap) are broken and the standard-plotted red lines are shown for reference.

How to space overlapping annotations

I want to annotate the bars in a graph with some text but if the bars are close together and have comparable height, the annotations are above ea. other and thus hard to read (the coordinates for the annotations were taken from the bar position and height).
Is there a way to shift one of them if there is a collision?
Edit: The bars are very thin and very close sometimes so just aligning vertically doesn't solve the problem...
A picture might clarify things:
I've written a quick solution, which checks each annotation position against default bounding boxes for all the other annotations. If there is a collision it changes its position to the next available collision free place. It also puts in nice arrows.
For a fairly extreme example, it will produce this (none of the numbers overlap):
Instead of this:
Here is the code:
import numpy as np
import matplotlib.pyplot as plt
from numpy.random import *
def get_text_positions(x_data, y_data, txt_width, txt_height):
a = zip(y_data, x_data)
text_positions = y_data.copy()
for index, (y, x) in enumerate(a):
local_text_positions = [i for i in a if i[0] > (y - txt_height)
and (abs(i[1] - x) < txt_width * 2) and i != (y,x)]
if local_text_positions:
sorted_ltp = sorted(local_text_positions)
if abs(sorted_ltp[0][0] - y) < txt_height: #True == collision
differ = np.diff(sorted_ltp, axis=0)
a[index] = (sorted_ltp[-1][0] + txt_height, a[index][1])
text_positions[index] = sorted_ltp[-1][0] + txt_height
for k, (j, m) in enumerate(differ):
#j is the vertical distance between words
if j > txt_height * 2: #if True then room to fit a word in
a[index] = (sorted_ltp[k][0] + txt_height, a[index][1])
text_positions[index] = sorted_ltp[k][0] + txt_height
break
return text_positions
def text_plotter(x_data, y_data, text_positions, axis,txt_width,txt_height):
for x,y,t in zip(x_data, y_data, text_positions):
axis.text(x - txt_width, 1.01*t, '%d'%int(y),rotation=0, color='blue')
if y != t:
axis.arrow(x, t,0,y-t, color='red',alpha=0.3, width=txt_width*0.1,
head_width=txt_width, head_length=txt_height*0.5,
zorder=0,length_includes_head=True)
Here is the code producing these plots, showing the usage:
#random test data:
x_data = random_sample(100)
y_data = random_integers(10,50,(100))
#GOOD PLOT:
fig2 = plt.figure()
ax2 = fig2.add_subplot(111)
ax2.bar(x_data, y_data,width=0.00001)
#set the bbox for the text. Increase txt_width for wider text.
txt_height = 0.04*(plt.ylim()[1] - plt.ylim()[0])
txt_width = 0.02*(plt.xlim()[1] - plt.xlim()[0])
#Get the corrected text positions, then write the text.
text_positions = get_text_positions(x_data, y_data, txt_width, txt_height)
text_plotter(x_data, y_data, text_positions, ax2, txt_width, txt_height)
plt.ylim(0,max(text_positions)+2*txt_height)
plt.xlim(-0.1,1.1)
#BAD PLOT:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.bar(x_data, y_data, width=0.0001)
#write the text:
for x,y in zip(x_data, y_data):
ax.text(x - txt_width, 1.01*y, '%d'%int(y),rotation=0)
plt.ylim(0,max(text_positions)+2*txt_height)
plt.xlim(-0.1,1.1)
plt.show()
Another option using my library adjustText, written specially for this purpose (https://github.com/Phlya/adjustText). I think it's probably significantly slower that the accepted answer (it slows down considerably with a lot of bars), but much more general and configurable.
from adjustText import adjust_text
np.random.seed(2017)
x_data = np.random.random_sample(100)
y_data = np.random.random_integers(10,50,(100))
f, ax = plt.subplots(dpi=300)
bars = ax.bar(x_data, y_data, width=0.001, facecolor='k')
texts = []
for x, y in zip(x_data, y_data):
texts.append(plt.text(x, y, y, horizontalalignment='center', color='b'))
adjust_text(texts, add_objects=bars, autoalign='y', expand_objects=(0.1, 1),
only_move={'points':'', 'text':'y', 'objects':'y'}, force_text=0.75, force_objects=0.1,
arrowprops=dict(arrowstyle="simple, head_width=0.25, tail_width=0.05", color='r', lw=0.5, alpha=0.5))
plt.show()
If we allow autoalignment along x axis, it gets even better (I just need to resolve a small issue that it doesn't like putting labels above the points and not a bit to the side...).
np.random.seed(2017)
x_data = np.random.random_sample(100)
y_data = np.random.random_integers(10,50,(100))
f, ax = plt.subplots(dpi=300)
bars = ax.bar(x_data, y_data, width=0.001, facecolor='k')
texts = []
for x, y in zip(x_data, y_data):
texts.append(plt.text(x, y, y, horizontalalignment='center', size=7, color='b'))
adjust_text(texts, add_objects=bars, autoalign='xy', expand_objects=(0.1, 1),
only_move={'points':'', 'text':'y', 'objects':'y'}, force_text=0.75, force_objects=0.1,
arrowprops=dict(arrowstyle="simple, head_width=0.25, tail_width=0.05", color='r', lw=0.5, alpha=0.5))
plt.show()
(I had to adjust some parameters here, of course)
One option is to rotate the text/annotation, which is set by the rotation keyword/property. In the following example, I rotate the text 90 degrees to guarantee that it wont collide with the neighboring text. I also set the va (short for verticalalignment) keyword, so that the text is presented above the bar (above the point that I use to define the text):
import matplotlib.pyplot as plt
data = [10, 8, 8, 5]
fig = plt.figure()
ax = fig.add_subplot(111)
ax.bar(range(4),data)
ax.set_ylim(0,12)
# extra .4 is because it's half the default width (.8):
ax.text(1.4,8,"2nd bar",rotation=90,va='bottom')
ax.text(2.4,8,"3nd bar",rotation=90,va='bottom')
plt.show()
The result is the following figure:
Determining programmatically if there are collisions between various annotations is a trickier process. This might be worth a separate question: Matplotlib text dimensions.
Just thought I would provide an alternative solution that I just created textalloc that makes sure that text-boxes avoids overlap with both each other and lines when possible, and is fast.
For this example you could use something like this:
import textalloc as ta
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(2017)
x_data = np.random.random_sample(100)
y_data = np.random.random_integers(10,50,(100))
f, ax = plt.subplots(dpi=200)
bars = ax.bar(x_data, y_data, width=0.002, facecolor='k')
ta.allocate_text(f,ax,x_data,y_data,
[str(yy) for yy in list(y_data)],
x_lines=[np.array([xx,xx]) for xx in list(x_data)],
y_lines=[np.array([0,yy]) for yy in list(y_data)],
textsize=8,
margin=0.004,
min_distance=0.005,
linewidth=0.7,
textcolor="b")
plt.show()
This results in this

Categories