I'm trying to create the graph present in the image bellow. I'm new on matplotlib so I don't know how to create the axis without insert some data. For example, if a have x and y and plot(x,y), the graph will be create and the axis too, but the axis will be correlated with the data. In the graph attach, the curves are isolines, so the correlation of the data doesn't depend exactly of the axis (depend by a function that I have described in the code).
How can I proceed?
You can use plt.xlim and plt.ylim to set the limits on the axis. With plt.grid you can control how to show the gridlines. set_major_locator can set the position for the ticks and corresponding gridlines.
The position for the text could be calculated by first finding the y on the line that corresponds to x=40. If that y would be too low, we can calculate an x value on the line corresponding to y=-1500. For the rotation of the text, we can take the tangent of the line, which needs to be corrected by the aspect ratio of the plot. To have the rotation still valid when the size of the plot is changed, a fixed aspect ratio can be set.
Here is some demonstration code to get you started:
import matplotlib.pyplot as plt
from matplotlib.ticker import MultipleLocator, FormatStrFormatter
import numpy as np
fig, ax = plt.subplots()
aspect = 1 / 300
ax.set_aspect(aspect) # a fixed aspect ratio is needed so the text rotation stays equal to the line rotation
for iso in range(-5000, 16001, 1000):
x1 = 25 + iso / 100
x2 = -50
y1 = -1000
y2 = 7000 + iso
plt.plot([x1, x2], [y1, y2], c='crimson', lw=2 if iso % 5000 == 0 else 0.5)
if iso % 5000 == 0:
# xt,yt: x,y value for text; first try y position when x = 40; if too small find x position for y = -1500
xt = 40
yt = (y1 * (xt - x2) + y2 * (x1 - xt)) / (x1 - x2)
if yt < y1:
yt = -1500
xt = (x1 * (yt - y2) + x2 * (y1 - yt)) / (y1 - y2)
ax.text(xt, yt, iso if iso != 0 else "zero",
{'ha': 'center', 'va': 'center', 'bbox': {'fc': 'lightgoldenrodyellow', 'pad': 2, 'alpha': 0.7}},
rotation=np.arctan(aspect * (y1 - y2) / (x1 - x2)) / np.pi * 180)
ax.xaxis.set_major_locator(MultipleLocator(10))
ax.xaxis.set_minor_locator(MultipleLocator(5))
ax.xaxis.set_major_formatter(FormatStrFormatter('%d°C'))
ax.yaxis.set_major_locator(MultipleLocator(5000))
ax.yaxis.set_minor_locator(MultipleLocator(1000))
plt.grid(True, which='major', color='blue', linestyle='--', lw=0.5)
plt.grid(True, which='minor', color='blue', linestyle='--', lw=0.2)
plt.xlim(-50, 50)
plt.ylim(-5000, 20000)
plt.show()
Related
I have the following snippet of code to draw a best-fit line through a collections of points on a graph, and annotate it with the corresponding R2 value:
import matplotlib.pyplot as plt
import numpy as np
import scipy.stats
x = 50 * np.random.rand(20) + 50
y = 200 * np.random.rand(20)
plt.plot(x, y, 'o')
# k, n = np.polyfit(x, y, 1)
k, n, r, _, _ = scipy.stats.linregress(x, y)
line = plt.axline((0, n), slope=k, color='blue')
xy = line.get_xydata()
plt.annotate(
f'$R^2={r**2:.3f}$',
(xy[0] + xy[-1]) // 2,
xycoords='axes fraction',
ha='center', va='center_baseline',
rotation=k, rotation_mode='anchor',
)
plt.show()
I have tried various different (x,y) pairs, different xycoords and other keyword parameters in annotate but I haven't been able to get the annotation to properly appear where I want it. How do I get the text annotation to appear above the line with proper rotation, located either at the middle point of the line, or at either end?
1. Annotation coordinates
We cannot compute the coordinates using xydata here, as axline() just returns dummy xydata (probably due to the way matplotlib internally plots infinite lines):
print(line.get_xydata())
# array([[0., 0.],
# [1., 1.]])
Instead we can compute the text coordinates based on the xlim():
xmin, xmax = plt.xlim()
xtext = (xmin + xmax) // 2
ytext = k*xtext + n
Note that these are data coordinates, so they should be used with xycoords='data' instead of 'axes fraction'.
2. Annotation angle
We cannot compute the angle purely from the line points, as the angle will also depend on the axis limits and figure dimensions (e.g., imagine the required rotation angle in a 6x4 figure vs 2x8 figure).
Instead we should normalize the calculation to both scales to get the proper visual rotation:
rs = np.random.RandomState(0)
x = 50 * rs.rand(20) + 50
y = 200 * rs.rand(20)
plt.plot(x, y, 'o')
# save ax and fig scales
xmin, xmax = plt.xlim()
ymin, ymax = plt.ylim()
xfig, yfig = plt.gcf().get_size_inches()
k, n, r, _, _ = scipy.stats.linregress(x, y)
plt.axline((0, n), slope=k, color='blue')
# restore x and y limits after axline
plt.xlim(xmin, xmax)
plt.ylim(ymin, ymax)
# find text coordinates at midpoint of regression line
xtext = (xmin + xmax) // 2
ytext = k*xtext + n
# find run and rise of (xtext, ytext) vs (0, n)
dx = xtext
dy = ytext - n
# normalize to ax and fig scales
xnorm = dx * xfig / (xmax - xmin)
ynorm = dy * yfig / (ymax - ymin)
# find normalized annotation angle in radians
rotation = np.rad2deg(np.arctan2(ynorm, xnorm))
plt.annotate(
f'$R^2={r**2:.3f}$',
(xtext, ytext), xycoords='data',
ha='center', va='bottom',
rotation=rotation, rotation_mode='anchor',
)
Here I need to plot a frequency and a simple line with a slope of -5/3.
The problem is that the main plot is using plt.loglog() and when I want to show the line it gives me nothing or something strange.
Here are the pictures of it. I have plotted the right one and desired is the left one.
I already used np.linspace and some other things, but I was not able to solve the problem. Also, it is not clear at which points I have the first and the end of the frequency plot. That's another reason why I can not use 'np.linspace'. Can anyone help me?
Thanks a lot for your attention. I tried your code but I found out maybe there are better ways to do it with kind of my dataset. So I did this:
Change the class of dataset to np.array() and had np.log() function on it:
x = ... # type(x) = list
y = ... # type(y) = list
.
.
.
x = np.log(np.array(x))
y = np.log(np.array(y))
In this case I did not have to use plt.loglog() or np.log() and np.exp() in calculations anymore.
Locate the min, max, and mean for x and y:
ymin, ymax = ([y.min(), y.max()])
ymid = (ymin + ymax) / 2
xmin, xmax = ([x.min(), x.max()])
xmid = (xmin + xmax) / 2
Use np.linspace() for rest:
slope = - 5 / 3
x1 = np.linspace(xmin, xmax)
y1 = slope * (x1 - xmid) + ymid
ax.plot(x1, y1, 'r')
And got the result I wanted.
the result
Edited: the plot in log scale.
Because of that, it is better to use plt.loglog() kind of plots in frequency spectrums, I edited these things:
Changed back x and y to normal np.array()
x = array(...)
y = array(...)
find the middle of x and y to have the center of the line and used simple equation of a straight line and then ploted the line using np.exp():
ymin, ymax = log([y.min(), y.max()])
ymid = (ymin + ymax) / 2
xmin, xmax = log([x.min(), x.max()])
xmid = (xmin + xmax) / 2
slope = - 5 / 3
y1 = slope * (xmin - xmid) + ymid
y2 = slope * (xmax - xmid) + ymid
ax.plot(exp([xmin, xmax]), exp([y1, y2]), 'r')
plt.loglog()
the result
As you see now we have the plot in log scale.
Here is a possible approach. All calculations happen in logspace. The points are transformed back to linear space to be plotted. As matplotlib plots a line given two points always as straight, independent to the tranformation of the axes, only two points are needed.
Steps:
find the lowest and highest values of the curve (y0, y1); these define the extension of the sloped line
find a point near the center of the curve (mid_x, mid_y); this point serves as an anchor point to know where the sloped line should go
find the x values that correspond to a sloped line through (mid_x, mid_y) and go to y0 and y1
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(figsize=(8, 8))
# first create some toy data roughly resembling the example plot
x = np.linspace(10, 2000, 1000)
y = np.random.normal(2000 / x ** np.linspace(.7, .55, x.size), 100 / x ** .7)
ax.plot(x, y)
y0, y1 = np.log([y.min(), y.max()])
# mid_x, mid_y = np.log([x[x.size // 2], y[y.size // 2]])
mid_x, mid_y = (np.log(x.min()) + np.log(x.max())) / 2, (y0 + y1) / 2
slope = -5 / 3
x0 = mid_x + slope * (y0 - mid_y)
x1 = mid_x + slope * (y1 - mid_y)
ax.plot(np.exp([x0, x1]), np.exp([y0, y1]), color='crimson')
plt.loglog()
plt.show()
I have data with 5 parameters, which I want to plot on multiple y axes and have them connected. Please see the example picture.
Currently I tried with normalizing all the values and create dictionary to do a scatter plot where on x axis would be values 1, 2, 3, 4, 5 and on y axis 5 parameter values for each data point. But this way I will need to add axis lines and values later on in Photoshop.
Is there a better way to create such graph using matplotlib and python?
One idea is to create 4 extra y axes, and set explicit limits for them. All the y-values can be rescaled to be compatible with the host axis.
(The code can still be optimized to make better use of numpy.)
import matplotlib.pyplot as plt
import numpy as np
fig, host = plt.subplots()
N = 20
y1 = np.random.uniform(10, 50, N)
y2 = np.sin(np.random.uniform(0, np.pi, N))
y3 = np.random.binomial(300, 0.9, N)
y4 = np.random.pareto(10, N)
y5 = np.random.uniform(0, 800, N)
ys = [y1, y2, y3, y4, y5]
y0min = ys[0].min()
dy = ys[0].max() - y0min
zs = [ys[0]] + [(y - y.min()) / (y.max() - y.min()) * dy + y0min for y in ys[1:]]
ynames = ['P1', 'P2', 'P3', 'P4', 'P5']
axes = [host] + [host.twinx() for i in range(len(ys) - 1)]
for i, (ax, y) in enumerate(zip(axes, ys)):
ax.set_ylim(y.min(), y.max())
ax.spines['top'].set_visible(False)
ax.spines['bottom'].set_visible(False)
if ax != host:
ax.spines['left'].set_visible(False)
ax.yaxis.set_ticks_position('right')
ax.spines["right"].set_position(("axes", i / (len(ys) - 1)))
host.set_xlim(0, len(ys) - 1)
host.set_xticks(range(len(ys)))
host.set_xticklabels(ynames)
host.tick_params(axis='x', which='major', pad=7)
host.spines['right'].set_visible(False)
colors = plt.cm.tab20.colors
for j in range(len(ys[0])):
host.plot(range(len(ys)), [z[j] for z in zs], c=colors[j % len(colors)])
plt.show()
I have a [1,2,3,4,5] data points on x-axis and its respective value on y-axis like [10,15,10,10,20].
normally to find value point of y-axis by given x-axis data points
like y=f(x), I checked this and we can achieve this by interpolation using numpy.. But I didn't found how to interpolate x-axis by given y-axis value.. as per attached screen I want to find respective x axis value where line 12 crosses..so I am expecting result something like [1, 1.x, 2, 2.x, 3, 4, 4.x, 5, 5.x] on x-axis
If it's a smooth curve, you can use InterpolatedUnivariateSpline
import numpy as np
from scipy import interpolate
x = np.linspace(0, 20, 100)
y = np.sin(x + 0.1)
y0 = 0.3
spline = interpolate.InterpolatedUnivariateSpline(x, y - y0)
xp = spline.roots()
Here is the plot:
pl.plot(x, y)
pl.axhline(0.3, color="black", linestyle="dashed")
pl.vlines(xp, 0, 0.3, color="gray", linestyle="dotted")
if you want linear interpolate:
x = np.linspace(0, 20, 20)
y = np.sin(x + 0.1)
y0 = 0.3
y_offset = y - y0
pos = np.where((y_offset[1:] * y_offset[:-1]) <= 0)[0]
x1 = x[pos]
x2 = x[pos+1]
y1 = y[pos]
y2 = y[pos+1]
xp = (y0 - y1) / (y2 - y1) * (x2 - x1) + x1
If you change interp1d(x,y) for interp1d(y,x) you have expressed x as a function of y.
Note that if f(x) is not unique, you may get unexpected or undefined behavior.
Plotting the function:
(x1 - 3)^2 + (x2 - 2)^2
With constraints:
x1^2 - x2 - 3 <= 0
x2 - 1 <= 0
-x1 <= 0
The equation can also be found here.
I am trying to solve this graphically using matplotlib
but ended up with the above graph using the code below (the question i found that helped me with the code) which is missing the first condition.
import matplotlib.pyplot as plt
from numpy import arange
from pylab import meshgrid
# function to be plotted
def z_func(a, b):
return (a - 3) * (a - 3) + (b - 2) * (b - 2)
x1 = arange(15.0, 0, -0.1) # x1 >= 0 according to given conditions
x2 = arange(-15.0, 1, 0.1) # x2 <= 1 according to given conditions
X1,X2 = meshgrid(x1, x2)
Z = z_func(X1, X2)
fig = plt.figure()
ax = fig.gca(projection='3d')
surf = ax.plot_surface(X1, X2, Z, rstride=1, cstride=1, cmap=cm.RdBu,linewidth=0, antialiased=False)
ax.zaxis.set_major_locator(LinearLocator(10))
ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))
ax.set_xlabel('x-axis')
ax.set_ylabel('y-axis')
ax.set_zlabel('z-axis')
ax.view_init(elev=25, azim=-120)
fig.colorbar(surf, shrink=0.5, aspect=5)
plt.show()
in which way shall the above code be altered in order to also take the first condition into account?
thanks
You can filter the array to plot and set all values outside the condition to nan:
Z[X1**2 - X2 - 3 > 0] = np.nan
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.axes3d import Axes3D
import numpy as np
from pylab import meshgrid
# function to be plotted
def z_func(a, b):
return (a - 3) * (a - 3) + (b - 2) * (b - 2)
x1 = np.arange(15.0, 0, -0.1) # x1 >= 0 according to given conditions
x2 = np.arange(-15.0, 1, 0.1) # x2 <= 1 according to given conditions
X1,X2 = meshgrid(x1, x2)
Z = z_func(X1, X2)
# set all values outside condition to nan
Z[X1**2 - X2 - 3 > 0] = np.nan
fig = plt.figure()
ax = fig.gca(projection='3d')
surf = ax.plot_surface(X1, X2, Z, rstride=1, cstride=1,vmin=0, vmax=np.nanmax(Z),
cmap=plt.cm.RdBu,linewidth=0, antialiased=False)
ax.set_xlabel('x-axis')
ax.set_ylabel('y-axis')
ax.set_zlabel('z-axis')
ax.view_init(elev=25, azim=-120)
ax.set_ylim(0,4)
fig.colorbar(surf, shrink=0.5, aspect=5)
plt.show()