bar plot in 3d following a given line - python

I want to draw a bar plot in 3d. I know how to do that using the following code:
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(111, projection='3d')
nbins = 50
# for c, z in zip(['r', 'g', 'b', 'y'], [30, 20, 10, 0]):
ys = np.random.normal(loc=10, scale=10, size=2000)
hist, bins = np.histogram(ys, bins=nbins)
xs = (bins[:-1] + bins[1:])/2
ax.bar(xs, hist, zs=30, zdir='y', color='r', ec='r', alpha=0.8)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
plt.show()
This will render something like this: https://i.stack.imgur.com/KK2If.png
However, my goal is to make the bar plot follows a line that I give as parameter. For example here, the parameter zdir='y' makes the plot have its current direction. Ideally I want to pass a parameter that makes the plot follows a given line for example y=2x+1.
Could someone help arrive at the desired result?

One way to achieve that is by using Poly3DCollection: the idea is to compute the coordinates and orientation of each bar, then add it to the plot.
The position and orientation of each bar can be computed starting from a rectangle in 3D space and applying the appropriate transformation matrix.
If you are going to change the curve, you will also need to change the bar width.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from matplotlib.patches import Rectangle
################
# Generates data
################
nbins = 50
ys = np.random.normal(loc=10, scale=10, size=2000)
hist, bins = np.histogram(ys, bins=nbins)
xs = (bins[:-1] + bins[1:])/2
#################################################
# Create a single bar and a transformation matrix
#################################################
# rectangle of width=height=1, centered at x,y=0
# covering the z range [0, height]
rect = np.array([
[-0.5, 0, 0, 1],
[0.5, 0, 0, 1],
[0.5, 0, 1, 1],
[-0.5, 0, 1, 1],
])
def translate(x, y, z):
d = np.eye(4, dtype=float)
d[:, -1] = [x, y, z, 1]
return d
def scale(sx, sy, sz):
d = np.eye(4, dtype=float)
d[np.diag_indices(4)] = [sx, sy, sz, 1]
return d
def rotate(t):
d = np.eye(4, dtype=float)
d[:2, :2] = np.array([
[np.cos(t), -np.sin(t)],
[np.sin(t), np.cos(t)]])
return d
def transformation_matrix(t, x, y, z, w, h):
return translate(x, y, z) # rotate(t) # scale(w, 1, h)
def apply_transform(t, x, y, z, w, h):
"""Apply the transformation matrix to the rectangle"""
verts = transformation_matrix(t, x, y, z, w, h) # rect.T
return verts.T
#################
# Create the plot
#################
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
curve = lambda x: 2 * x + 1
# curve = lambda x: np.sin(0.05 * x)
xstep = abs(xs[0] - xs[1])
# NOTE: chose an appropriate bar width
width = xstep * 1.5
ys = curve(xs)
# previous bar coordinates
xp = np.roll(xs, 1)
yp = np.roll(ys, 1)
xp[0] = xs[0] - xstep
yp[0] = curve(xp[0])
# compute the orientation of the bars
theta = np.arctan2((ys - yp), (xs - xp))
# customize the appearance of the bar
facecolor = "tab:red"
edgecolor = "k"
linewidth = 0
# loop to add each bar
for x, y, t, h in zip(xs, ys, theta, hist):
verts_matrix = apply_transform(t, x, y, 0, width, h)
x, y, z = verts_matrix[:, 0], verts_matrix[:, 1], verts_matrix[:, 2]
verts = [list(zip(x, y, z))]
c = Poly3DCollection(verts, facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth)
ax.add_collection3d(c)
# eventually show a legend
ax.legend([Rectangle((0, 0), 1, 1, facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth)], ["Bar Plot"])
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.set_xlim(xs.min(), xs.max())
ax.set_ylim(ys.min(), ys.max())
ax.set_zlim(0, 100)
plt.show()
EDIT to explain what is going on:
Consider a generic rectangle with 4 vertices: bottom left, bottom right, top right, top left. For simplicity, let's fix width=height=1. Then we consider a reference system x,y,z and we draw this rectangle. The coordinates of vertices are: bottom left (-0.5, 0, 0), bottom right (0.5, 0, 0), top right (0.5, 0, 1) and top left (-0.5, 0, 1). Note that this rectangle is centered around the zero in the x direction. If we move it to x=2, then it will be centered at that location. You can see the above coordinates in rect: why does this variable has a fourth column filled with ones? That's a mathematical trick to be able to apply a translation matrix to the vertices.
Let's talk about transformation matrices (wikipedia has a nice page about it). Consider again our generic rectangle: we can scale it, rotate it and translate it to get a new rectangle in the position and orientation we want.
So, the code above defines a function for each transformation, translate, scale, rotate. Turns out that we can multiply together multiple transformation matrices to get an overall transformation: that's what transformation_matrix does, it combines the aforementioned transformations into a single matrix.
Finally, I used apply_transform to apply the transformation matrix to the generic rectangle: this will compute the coordinates of the vertices of the new rectangle, in the specified position/orientation with the specified size (width, height).

Related

Converting 1D distribution into matplotlib gradient

I have a 1D distribution (x values vs probability, shown below) and I would like to convert that to a 2D plot like the one shown below in which the color gradient is based on the values probabilities.
Currently, my code just plot in a qualitative manner because I am manually defining the array v1 and the color list. I tried my best to crack this and understand how to do it, but I failed. Does anyone have a suggestion?
def gradient_image(ax, extent, direction=0.3, cmap_range=(0, 1), **kwargs):
"""
Draw a gradient image based on a colormap.
Parameters
----------
ax : Axes
The axes to draw on.
extent
The extent of the image as (xmin, xmax, ymin, ymax).
By default, this is in Axes coordinates but may be
changed using the *transform* keyword argument.
direction : float
The direction of the gradient. This is a number in
range 0 (=vertical) to 1 (=horizontal).
cmap_range : float, float
The fraction (cmin, cmax) of the colormap that should be
used for the gradient, where the complete colormap is (0, 1).
**kwargs
Other parameters are passed on to `.Axes.imshow()`.
In particular useful is *cmap*.
"""
phi = direction * np.pi / 2
v = np.array([np.cos(phi), np.sin(phi)])
X = np.array([[v # [1, 0], v # [1, 1]],
[v # [0, 0], v # [0, 1]]])
a, b = cmap_range
X = a + (b - a) / X.max() * X
im = ax.imshow(X, extent=extent, interpolation='bicubic',
vmin=0, vmax=1, **kwargs)
return im
v1 = [0, 0.15, 0.5, 0.85, 1.0] # | Those two lines here
b = ["white","lightblue", "dodgerblue","lightblue", "white"] # | were the best I could do
bl = list(zip(v1,b))
blue_grad=LinearSegmentedColormap.from_list('custom',bl, N=256)
xmin, xmax = xlim = 0, 4
ymin, ymax = ylim = -300, 300
fig, ax = plt.subplots()
ax.set(xlim=xlim, ylim=ylim, autoscale_on=False)
gradient_image(ax, direction=1, extent=(0 , 2, -300, 300), cmap=blue_grad, cmap_range=(0., 1), alpha=0.5)
Here is a minimal example with a gaussian distribution (code for generating the gaussian distribution was adapted from this):
import matplotlib.pyplot as plt
import numpy as np
import scipy.stats as stats
mu=0 #Create gaussian distribution
sigma=1
x = np.linspace(mu - 3*sigma, mu + 3*sigma, 100)
y=stats.norm.pdf(x, mu, sigma)
y_expand=np.expand_dims(y, axis=0) #expanding dimensions to use imshow
plt.plot(x, stats.norm.pdf(x, mu, sigma),color='k',lw=3,ls='--')# plot distribution
extent = np.min(x), np.max(x), np.min(y), np.max(y)
plt.imshow(y_expand,interpolation=None,aspect='auto',cmap='Blues',extent=extent) #plot imshow
plt.colorbar()
plt.show()

Changing the origin of a 3d plot

I am trying to graph a 3d plot of the function f(x,y,z) = cos(x) + cos(y) + cos(z). I found a code off here that I copied and pasted and it looked like this.
def fun(x, y, z):
return cos(x) + cos(y) + cos(z)
x, y, z = pi*np.mgrid[-1:1:31j, -1:1:31j, -1:1:31j]
vol = fun(x, y, z)
iso_val=0.0
verts, faces, _, _ = measure.marching_cubes(vol, iso_val, spacing=(0.1, 0.1, 0.1))
fig = plt.figure(figsize = (10,6))
ax = fig.add_subplot(111, projection='3d')
p = ax.plot_trisurf(verts[:, 0], verts[:,1], faces, verts[:, 2],
cmap='Spectral', lw=1)
fig.colorbar(p, ax=ax, pad = 0.1)
plt.show()
Then, I noticed that it the labels in xyz and I want the origin to be on the center of the plot and not on the corners (kind of like this)
I don't want to just mess with the labels of the graph, I want the array to reflect that the origin has been changed. I don't know how to do this, like, do I just subtract -1.5 on everything to center the whole plot?

Plot scaled and rotated bivariate distribution using matplotlib

I am trying to plot a bivariate gaussian distribution using matplotlib. I want to do this using the xy coordinates of two scatter points (Group A), (Group B).
I want to adjust the distribution by adjusting the COV matrix to account for each Groups velocity and their distance to an additional xy coordinate used as a reference point.
I've calculated the distance of each groups xy coordinate to that of the reference point. The distance is expressed as a radius, labelled [GrA_Rad],[GrB_Rad].
So the further they are away from the reference point the greater the radius. I've also calculated velocity labelled [GrA_Vel],[GrB_Vel]. The direction of each group is expressed as the orientation. This is labelled [GrA_Rotation],[GrB_Rotation]
Question on how I want the distribution to be adjusted for velocity and distance (radius):
I'm hoping to use SVD. Specifically, if I have the rotation angle of each scatter, this provides the direction. The velocity can be used to describe a scaling matrix [GrA_Scaling],[GrB_Scaling]. So this scaling matrix can be used to expand the radius in the x-direction and contract the radius in the y-direction. This expresses the COV matrix.
Finally, the distribution mean value is found by translating the groups location (x,y) by half the velocity.
Put simply: the radius is applied to each group's scatter point. The COV matrix is adjusted by the radius and velocity. So using the scaling matrix to expand the radius in x-direction and contract in y-direction. The direction is measured from the rotation angle. Then determine the distribution mean value by translating the groups location (x,y) by half the velocity.
Below is the df of these variables
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.animation as animation
d = ({
'Time' : [1,2,3,4,5,6,7,8],
'GrA_X' : [10,12,17,16,16,14,12,8],
'GrA_Y' : [10,12,13,7,6,7,8,8],
'GrB_X' : [5,8,13,16,19,15,13,5],
'GrB_Y' : [6,15,12,7,8,9,10,8],
'Reference_X' : [6,8,14,18,13,11,16,15],
'Reference_Y' : [10,12,8,12,15,12,10,8],
'GrA_Rad' : [8.3,8.25,8.2,8,8.15,8.15,8.2,8.3],
'GrB_Rad' : [8.3,8.25,8.3,8.4,8.6,8.4,8.3,8.65],
'GrA_Vel' : [0,2.8,5.1,6.1,1.0,2.2,2.2,4.0],
'GrB_Vel' : [0,9.5,5.8,5.8,3.16,4.12,2.2,8.2],
'GrA_Scaling' : [0,0.22,0.39,0.47,0.07,0.17,0.17,0.31],
'GrB_Scaling' : [0,0.53,0.2,0.2,0.06,0.1,0.03,0.4],
'GrA_Rotation' : [0,45,23.2,-26.56,-33.69,-36.86,-45,-135],
'GrB_Rotation' : [0,71.6,36.87,5.2,8.13,16.70,26.57,90],
})
df = pd.DataFrame(data = d)
I've made an animated plot of each xy coordinate.
GrA_X = [10,12,17,16,16,14,12,8]
GrA_Y = [10,12,13,7,6,7,8,8]
GrB_X = [5,8,13,16,19,15,13,5]
GrB_Y = [6,15,12,10,8,9,10,8]
Item_X = [6,8,14,18,13,11,16,15]
Item_Y = [10,12,8,12,15,12,10,8]
scatter_GrA = ax.scatter(GrA_X, GrA_Y)
scatter_GrB = ax.scatter(GrB_X, GrB_Y)
scatter_Item = ax.scatter(Item_X, Item_Y)
def animate(i) :
scatter_GrA.set_offsets([[GrA_X[0+i], GrA_Y[0+i]]])
scatter_GrB.set_offsets([[GrB_X[0+i], GrB_Y[0+i]]])
scatter_Item.set_offsets([[Item_X[0+i], Item_Y[0+i]]])
ani = animation.FuncAnimation(fig, animate, np.arange(0,9),
interval = 1000, blit = False)
Update
The question has been updated, and has gotten somewhat clearer. I've updated my code to match. Here's the latest output:
Aside from the styling, I think this matches what the OP described.
Here's the code that was used to produce the above plot:
dfake = ({
'GrA_X' : [15,15],
'GrA_Y' : [15,15],
'Reference_X' : [15,3],
'Reference_Y' : [15,15],
'GrA_Rad' : [15,25],
'GrA_Vel' : [0,10],
'GrA_Scaling' : [0,0.5],
'GrA_Rotation' : [0,45]
})
dffake = pd.DataFrame(dfake)
fig,axs = plt.subplots(1, 2, figsize=(16,8))
fig.subplots_adjust(0,0,1,1)
plotone(dffake, 'A', 0, xlim=(0,30), ylim=(0,30), fig=fig, ax=axs[0])
plotone(dffake, 'A', 1, xlim=(0,30), ylim=(0,30), fig=fig, ax=axs[1])
plt.show()
and the complete implementation of the plotone function that I used is in the code block below. If you just want to know about the math used to generate and transform the 2D gaussian PDF, check out the mvpdf function (and the rot and getcov functions it depends on):
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.stats as sts
def rot(theta):
theta = np.deg2rad(theta)
return np.array([
[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)]
])
def getcov(radius=1, scale=1, theta=0):
cov = np.array([
[radius*(scale + 1), 0],
[0, radius/(scale + 1)]
])
r = rot(theta)
return r # cov # r.T
def mvpdf(x, y, xlim, ylim, radius=1, velocity=0, scale=0, theta=0):
"""Creates a grid of data that represents the PDF of a multivariate gaussian.
x, y: The center of the returned PDF
(xy)lim: The extent of the returned PDF
radius: The PDF will be dilated by this factor
scale: The PDF be stretched by a factor of (scale + 1) in the x direction, and squashed by a factor of 1/(scale + 1) in the y direction
theta: The PDF will be rotated by this many degrees
returns: X, Y, PDF. X and Y hold the coordinates of the PDF.
"""
# create the coordinate grids
X,Y = np.meshgrid(np.linspace(*xlim), np.linspace(*ylim))
# stack them into the format expected by the multivariate pdf
XY = np.stack([X, Y], 2)
# displace xy by half the velocity
x,y = rot(theta) # (velocity/2, 0) + (x, y)
# get the covariance matrix with the appropriate transforms
cov = getcov(radius=radius, scale=scale, theta=theta)
# generate the data grid that represents the PDF
PDF = sts.multivariate_normal([x, y], cov).pdf(XY)
return X, Y, PDF
def plotmv(x, y, xlim=None, ylim=None, radius=1, velocity=0, scale=0, theta=0, xref=None, yref=None, fig=None, ax=None):
"""Plot an xy point with an appropriately tranformed 2D gaussian around it.
Also plots other related data like the reference point.
"""
if xlim is None: xlim = (x - 5, x + 5)
if ylim is None: ylim = (y - 5, y + 5)
if fig is None:
fig = plt.figure(figsize=(8,8))
ax = fig.gca()
elif ax is None:
ax = fig.gca()
# plot the xy point
ax.plot(x, y, '.', c='C0', ms=20)
if not (xref is None or yref is None):
# plot the reference point, if supplied
ax.plot(xref, yref, '.', c='w', ms=12)
# plot the arrow leading from the xy point
if velocity > 0:
ax.arrow(x, y, *rot(theta) # (velocity, 0),
width=.4, length_includes_head=True, ec='C0', fc='C0')
# fetch the PDF of the 2D gaussian
X, Y, PDF = mvpdf(x, y, xlim=xlim, ylim=ylim, radius=radius, velocity=velocity, scale=scale, theta=theta)
# normalize PDF by shifting and scaling, so that the smallest value is 0 and the largest is 1
normPDF = PDF - PDF.min()
normPDF = normPDF/normPDF.max()
# plot and label the contour lines of the 2D gaussian
cs = ax.contour(X, Y, normPDF, levels=6, colors='w', alpha=.5)
ax.clabel(cs, fmt='%.3f', fontsize=12)
# plot the filled contours of the 2D gaussian. Set levels high for smooth contours
cfs = ax.contourf(X, Y, normPDF, levels=50, cmap='viridis', vmin=-.9, vmax=1)
# create the colorbar and ensure that it goes from 0 -> 1
cbar = fig.colorbar(cfs, ax=ax)
cbar.set_ticks([0, .2, .4, .6, .8, 1])
# add some labels
ax.grid()
ax.set_xlabel('X distance (M)')
ax.set_ylabel('Y distance (M)')
# ensure that x vs y scaling doesn't disrupt the transforms applied to the 2D gaussian
ax.set_aspect('equal', 'box')
return fig, ax
def fetchone(df, l, i, **kwargs):
"""Fetch all the needed data for one xy point
"""
keytups = (
('x', 'Gr%s_X'%l),
('y', 'Gr%s_Y'%l),
('radius', 'Gr%s_Rad'%l),
('velocity', 'Gr%s_Vel'%l),
('scale', 'Gr%s_Scaling'%l),
('theta', 'Gr%s_Rotation'%l),
('xref', 'Reference_X'),
('yref', 'Reference_Y')
)
ret = {k:df.loc[i, l] for k,l in keytups}
# add in any overrides
ret.update(kwargs)
return ret
def plotone(df, l, i, xlim=None, ylim=None, fig=None, ax=None, **kwargs):
"""Plot exactly one point from the dataset
"""
# look up all the data to plot one datapoint
xydata = fetchone(df, l, i, **kwargs)
# do the plot
return plotmv(xlim=xlim, ylim=ylim, fig=fig, ax=ax, **xydata)
Old answer -2
I've adjusted my answer to match the example the OP posted:
Here's the code that produced the above image:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.stats as sts
def rot(theta):
theta = np.deg2rad(theta)
return np.array([
[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)]
])
def getcov(radius=1, scale=1, theta=0):
cov = np.array([
[radius*(scale + 1), 0],
[0, radius/(scale + 1)]
])
r = rot(theta)
return r # cov # r.T
def datalimits(*data, pad=.15):
dmin,dmax = min(d.min() for d in data), max(d.max() for d in data)
spad = pad*(dmax - dmin)
return dmin - spad, dmax + spad
d = ({
'Time' : [1,2,3,4,5,6,7,8],
'GrA_X' : [10,12,17,16,16,14,12,8],
'GrA_Y' : [10,12,13,7,6,7,8,8],
'GrB_X' : [5,8,13,16,19,15,13,5],
'GrB_Y' : [6,15,12,7,8,9,10,8],
'Reference_X' : [6,8,14,18,13,11,16,15],
'Reference_Y' : [10,12,8,12,15,12,10,8],
'GrA_Rad' : [8.3,8.25,8.2,8,8.15,8.15,8.2,8.3],
'GrB_Rad' : [8.3,8.25,8.3,8.4,8.6,8.4,8.3,8.65],
'GrA_Vel' : [0,2.8,5.1,6.1,1.0,2.2,2.2,4.0],
'GrB_Vel' : [0,9.5,5.8,5.8,3.16,4.12,2.2,8.2],
'GrA_Scaling' : [0,0.22,0.39,0.47,0.07,0.17,0.17,0.31],
'GrB_Scaling' : [0,0.53,0.2,0.2,0.06,0.1,0.03,0.4],
'GrA_Rotation' : [0,45,23.2,-26.56,-33.69,-36.86,-45,-135],
'GrB_Rotation' : [0,71.6,36.87,5.2,8.13,16.70,26.57,90],
})
df = pd.DataFrame(data=d)
limitpad = .5
clevels = 5
cflevels = 50
xmin,xmax = datalimits(df['GrA_X'], df['GrB_X'], pad=limitpad)
ymin,ymax = datalimits(df['GrA_Y'], df['GrB_Y'], pad=limitpad)
X,Y = np.meshgrid(np.linspace(xmin, xmax), np.linspace(ymin, ymax))
fig = plt.figure(figsize=(10,6))
ax = plt.gca()
Zs = []
for l,color in zip('AB', ('red', 'yellow')):
# plot all of the points from a single group
ax.plot(df['Gr%s_X'%l], df['Gr%s_Y'%l], '.', c=color, ms=15, label=l)
Zrows = []
for _,row in df.iterrows():
x,y = row['Gr%s_X'%l], row['Gr%s_Y'%l]
cov = getcov(radius=row['Gr%s_Rad'%l], scale=row['Gr%s_Scaling'%l], theta=row['Gr%s_Rotation'%l])
mnorm = sts.multivariate_normal([x, y], cov)
Z = mnorm.pdf(np.stack([X, Y], 2))
Zrows.append(Z)
Zs.append(np.sum(Zrows, axis=0))
# plot the reference points
# create Z from the difference of the sums of the 2D Gaussians from group A and group B
Z = Zs[0] - Zs[1]
# normalize Z by shifting and scaling, so that the smallest value is 0 and the largest is 1
normZ = Z - Z.min()
normZ = normZ/normZ.max()
# plot and label the contour lines
cs = ax.contour(X, Y, normZ, levels=clevels, colors='w', alpha=.5)
ax.clabel(cs, fmt='%2.1f', colors='w')#, fontsize=14)
# plot the filled contours. Set levels high for smooth contours
cfs = ax.contourf(X, Y, normZ, levels=cflevels, cmap='viridis', vmin=0, vmax=1)
# create the colorbar and ensure that it goes from 0 -> 1
cbar = fig.colorbar(cfs, ax=ax)
cbar.set_ticks([0, .2, .4, .6, .8, 1])
ax.set_aspect('equal', 'box')
Old answer -1
It's a little hard to tell exactly what you're after. It is possible to scale and rotate a multivariate gaussian distribution via its covariance matrix. Here's an example of how to do so based on your data:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.stats as sts
def rot(theta):
theta = np.deg2rad(theta)
return np.array([
[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)]
])
def getcov(scale, theta):
cov = np.array([
[1*(scale + 1), 0],
[0, 1/(scale + 1)]
])
r = rot(theta)
return r # cov # r.T
d = ({
'Time' : [1,2,3,4,5,6,7,8],
'GrA_X' : [10,12,17,16,16,14,12,8],
'GrA_Y' : [10,12,13,7,6,7,8,8],
'GrB_X' : [5,8,13,16,19,15,13,5],
'GrB_Y' : [6,15,12,7,8,9,10,8],
'Reference_X' : [6,8,14,18,13,11,16,15],
'Reference_Y' : [10,12,8,12,15,12,10,8],
'GrA_Rad' : [8.3,8.25,8.2,8,8.15,8.15,8.2,8.3],
'GrB_Rad' : [8.3,8.25,8.3,8.4,8.6,8.4,8.3,8.65],
'GrA_Vel' : [0,2.8,5.1,6.1,1.0,2.2,2.2,4.0],
'GrB_Vel' : [0,9.5,5.8,5.8,3.16,4.12,2.2,8.2],
'GrA_Scaling' : [0,0.22,0.39,0.47,0.07,0.17,0.17,0.31],
'GrB_Scaling' : [0,0.53,0.2,0.2,0.06,0.1,0.03,0.4],
'GrA_Rotation' : [0,45,23.2,-26.56,-33.69,-36.86,-45,-135],
'GrB_Rotation' : [0,71.6,36.87,5.2,8.13,16.70,26.57,90],
})
df = pd.DataFrame(data=d)
xmin,xmax = min(df['GrA_X'].min(), df['GrB_X'].min()), max(df['GrA_X'].max(), df['GrB_X'].max())
ymin,ymax = min(df['GrA_Y'].min(), df['GrB_Y'].min()), max(df['GrA_Y'].max(), df['GrB_Y'].max())
X,Y = np.meshgrid(
np.linspace(xmin - (xmax - xmin)*.1, xmax + (xmax - xmin)*.1),
np.linspace(ymin - (ymax - ymin)*.1, ymax + (ymax - ymin)*.1)
)
fig,axs = plt.subplots(df.shape[0], sharex=True, figsize=(4, 4*df.shape[0]))
fig.subplots_adjust(0,0,1,1,0,-.82)
for (_,row),ax in zip(df.iterrows(), axs):
for c in 'AB':
x,y = row['Gr%s_X'%c], row['Gr%s_Y'%c]
cov = getcov(scale=row['Gr%s_Scaling'%c], theta=row['Gr%s_Rotation'%c])
mnorm = sts.multivariate_normal([x, y], cov)
Z = mnorm.pdf(np.stack([X, Y], 2))
ax.contour(X, Y, Z)
ax.plot(row['Gr%s_X'%c], row['Gr%s_Y'%c], 'x')
ax.set_aspect('equal', 'box')
This outputs:

How to draw a line with matplotlib?

I cannot find a way to draw an arbitrary line with matplotlib Python library. It allows to draw horizontal and vertical lines (with matplotlib.pyplot.axhline and matplotlib.pyplot.axvline, for example), but i do not see how to draw a line through two given points (x1, y1) and (x2, y2). Is there a way? Is there a simple way?
This will draw a line that passes through the points (-1, 1) and (12, 4), and another one that passes through the points (1, 3) et (10, 2)
x1 are the x coordinates of the points for the first line, y1 are the y coordinates for the same -- the elements in x1 and y1 must be in sequence.
x2 and y2 are the same for the other line.
import matplotlib.pyplot as plt
x1, y1 = [-1, 12], [1, 4]
x2, y2 = [1, 10], [3, 2]
plt.plot(x1, y1, x2, y2, marker = 'o')
plt.show()
I suggest you spend some time reading / studying the basic tutorials found on the very rich matplotlib website to familiarize yourself with the library.
What if I don't want line segments?
[edit]:
As shown by #thomaskeefe, starting with matplotlib 3.3, this is now builtin as a convenience: plt.axline((x1, y1), (x2, y2)), rendering the following obsolete.
There are no direct ways to have lines extend to infinity... matplotlib will either resize/rescale the plot so that the furthest point will be on the boundary and the other inside, drawing line segments in effect; or you must choose points outside of the boundary of the surface you want to set visible, and set limits for the x and y axis.
As follows:
import matplotlib.pyplot as plt
x1, y1 = [-1, 12], [1, 10]
x2, y2 = [-1, 10], [3, -1]
plt.xlim(0, 8), plt.ylim(-2, 8)
plt.plot(x1, y1, x2, y2, marker = 'o')
plt.show()
As of matplotlib 3.3, you can do this with plt.axline((x1, y1), (x2, y2)).
I was checking how ax.axvline does work, and I've written a small function that resembles part of its idea:
import matplotlib.pyplot as plt
import matplotlib.lines as mlines
def newline(p1, p2):
ax = plt.gca()
xmin, xmax = ax.get_xbound()
if(p2[0] == p1[0]):
xmin = xmax = p1[0]
ymin, ymax = ax.get_ybound()
else:
ymax = p1[1]+(p2[1]-p1[1])/(p2[0]-p1[0])*(xmax-p1[0])
ymin = p1[1]+(p2[1]-p1[1])/(p2[0]-p1[0])*(xmin-p1[0])
l = mlines.Line2D([xmin,xmax], [ymin,ymax])
ax.add_line(l)
return l
So, if you run the following code you will realize how does it work. The line will span the full range of your plot (independently on how big it is), and the creation of the line doesn't rely on any data point within the axis, but only in two fixed points that you need to specify.
import numpy as np
x = np.linspace(0,10)
y = x**2
p1 = [1,20]
p2 = [6,70]
plt.plot(x, y)
newline(p1,p2)
plt.show()
Just want to mention another option here.
You can compute the coefficients using numpy.polyfit(), and feed the coefficients to numpy.poly1d(). This function can construct polynomials using the coefficients, you can find more examples here
https://docs.scipy.org/doc/numpy-1.13.0/reference/generated/numpy.poly1d.html
Let's say, given two data points (-0.3, -0.5) and (0.8, 0.8)
import numpy as np
import matplotlib.pyplot as plt
# compute coefficients
coefficients = np.polyfit([-0.3, 0.8], [-0.5, 0.8], 1)
# create a polynomial object with the coefficients
polynomial = np.poly1d(coefficients)
# for the line to extend beyond the two points,
# create the linespace using the min and max of the x_lim
# I'm using -1 and 1 here
x_axis = np.linspace(-1, 1)
# compute the y for each x using the polynomial
y_axis = polynomial(x_axis)
fig = plt.figure()
axes = fig.add_axes([0.1, 0.1, 1, 1])
axes.set_xlim(-1, 1)
axes.set_ylim(-1, 1)
axes.plot(x_axis, y_axis)
axes.plot(-0.3, -0.5, 0.8, 0.8, marker='o', color='red')
Hope it helps.
In case somebody lands here trying to plot many segments in one go, here is a way. Say the segments are defined by two 2-d arrays of same length, e.g. a and b. We want to plot segments between each a[i] and b[i]. In that case:
Solution 1
ab_pairs = np.c_[a, b]
plt_args = ab_pairs.reshape(-1, 2, 2).swapaxes(1, 2).reshape(-1, 2)
ax.plot(*plt_args, ...)
Example:
np.random.seed(0)
n = 32
a = np.random.uniform(0, 1, (n, 2))
b = np.random.uniform(0, 1, (n, 2))
fig, ax = plt.subplots(figsize=(3, 3))
ab_pairs = np.c_[a, b]
ab_args = ab_pairs.reshape(-1, 2, 2).swapaxes(1, 2).reshape(-1, 2)
# segments
ax.plot(*ab_args, c='k')
# identify points: a in blue, b in red
ax.plot(*a.T, 'bo')
ax.plot(*b.T, 'ro')
plt.show()
Solution 2
The above creates many matplotlib.lines.Line2D. If you'd like a single line, we can do it by interleaving NaN between pairs:
ax.plot(*np.c_[a, b, a*np.nan].reshape(-1, 2).T, ...)
Example:
# same init as example above, then
fig, ax = plt.subplots(figsize=(3, 3))
# segments (all at once)
ax.plot(*np.c_[a, b, a*np.nan].reshape(-1, 2).T, 'k')
# identify points: a in blue, b in red
ax.plot(*a.T, 'bo')
ax.plot(*b.T, 'ro')
plt.show()
(Same figure as above).
Based on #Alejandro's answer:
if you want to add a line to an existing Axes (e.g. a scatter plot), and
all you know is the slope and intercept of the desired line (e.g. a regression line), and
you want it to cover the entire visible X range (already computed), and
you want to use the object-oriented interface (not pyplot).
Then you can do this (existing Axes in ax):
# e.g. slope, intercept, r_value, p_value, std_err = scipy.stats.linregress(xs, ys)
xmin, xmax = ax.get_xbound()
ymin = (xmin * slope) + intercept
ymax = (xmax * slope) + intercept
l = matplotlib.lines.Line2D([xmin, xmax], [ymin, ymax])
ax.add_line(l)

How to make four-way logarithmic plot in Matplotlib?

Four-way logarithmic plot is a very often used graph for vibration control and earthquake protection. I am quite interesting in how this plot can be plotted in Matplotlib instead of adding axes in Inkscape. A sample of Four-way logarithmic plot is here.
A quick and dirty Python code can generate main part of the figure, but I cannot add the two axes onto the figure. http://matplotlib.org/examples/axes_grid/demo_curvelinear_grid.html provides an example of adding axes, but I fails to make it working. Anyone has similar experience on adding axes to Matplotlib figure?
from pylab import *
from mpl_toolkits.axisartist.grid_helper_curvelinear import GridHelperCurveLinear
from mpl_toolkits.axisartist import Subplot
beta=logspace(-1,1,500)
Rd={}
for zeta in [0.01,0.1,0.2,0.7,1]:
Rd[zeta]=beta/sqrt((1-beta*beta)**2+(2*beta*zeta)**2)
loglog(beta,Rd[zeta])
ylim([0.1,10])
xlim([0.1,10])
grid('on',which='minor')
Update: Thank you all! I use Inkscape to modify the figure above. I think the result is just fine. However, I am still looking for methods to draw this figure in Matplotlib.
Here is a partial solution. I am still working on how to do all of this in a natural loglog() plot rather than scaling the data. (To complete this example you would have to define custom tick-lables so that they display 10**x rather than x.)
%matplotlib inline # I am doing this in an IPython notebook.
from matplotlib import pyplot as plt
import numpy as np
from numpy import log10
# Generate the data
beta = np.logspace(-1, 1, 500)[:, None]
zeta = np.array([0.01,0.1,0.2,0.7,1])[None, :]
Rd = beta/np.sqrt((1 - beta*beta)**2 + (2*beta*zeta)**2)
def draw(beta=beta, Rd=Rd):
plt.plot(log10(beta), log10(Rd))
plt.ylim([log10(0.1), log10(10)])
plt.xlim([log10(0.1), log10(10)])
plt.grid('on',which='minor')
ax = plt.gca()
ax.set_aspect(1)
from mpl_toolkits.axisartist import GridHelperCurveLinear
from matplotlib.transforms import Affine2D
from mpl_toolkits.axisartist import SubplotHost
from mpl_toolkits.axisartist import Subplot
#tr = Affine2D().rotate(-np.pi/2)
#inv_tr = Affine2D().rotate(np.pi/2)
class Transform(object):
"""Provides transforms to go to and from rotated grid.
Parameters
----------
ilim : (xmin, xmax, ymin, ymax)
The limits of the displayed axes (in physical units)
olim : (xmin, xmax, ymin, ymax)
The limits of the rotated axes (in physical units)
"""
def __init__(self, ilim, olim):
# Convert each to a 3x3 matrix and compute the transform
# [x1, y1, 1] = A*[x0, y0, 1]
x0, x1, y0, y1 = np.log10(ilim)
I = np.array([[x0, x0, x1],
[y0, y1, y1],
[ 1, 1, 1]])
x0, x1, y0, y1 = np.log10(olim)
x_mid = (x0 + x1)/2
y_mid = (y0 + y1)/2
O = np.array([[ x0, x_mid, x1],
[y_mid, y1, y_mid],
[ 1, 1, 1]])
self.A = np.dot(O, np.linalg.inv(I))
self.Ainv = np.linalg.inv(self.A)
def tr(self, x, y):
"""From "curved" (rotated) coords to rectlinear coords"""
x, y = map(np.asarray, (x, y))
return np.dot(self.A, np.asarray([x, y, 1]))[:2]
def inv_tr(self, x, y):
"""From rectlinear coords to "curved" (rotated) coords"""
x, y = map(np.asarray, (x, y))
return np.dot(self.Ainv, np.asarray([x, y, 1]))[:2]
ilim = (0.1, 10)
olim = (0.01, 100)
tr = Transform(ilim + ilim, olim + olim)
grid_helper = GridHelperCurveLinear((tr.tr, tr.inv_tr))
fig = plt.gcf()
ax0 = Subplot(fig, 1, 1, 1)
ax1 = Subplot(fig, 1, 1, 1, grid_helper=grid_helper, frameon=False)
ax1.set_xlim(*np.log10(olim))
ax1.set_ylim(*np.log10(olim))
ax1.axis["left"] = ax1.new_floating_axis(0, 0.)
ax1.axis["bottom"] = ax1.new_floating_axis(1, 0.0)
fig.add_subplot(ax0)
fig.add_subplot(ax1)
ax0.grid('on', which='both')
ax1.grid('on', which='both')
plt.plot(log10(beta), log10(Rd))
plt.ylim(np.log10(ilim))
plt.xlim(np.log10(ilim))
This seems to be a bit tricker than it should. There are ways to center the spines (axis lines), and ways to rotate them, but those do not work together. Adding a normal axis on a line (a la mpl demos) results in a curved axis (because it is logarithmic). Here is a [poor] example of how to draw -- as in, like you would with Inkscape something to look like an additional pair of axis spines with the example data.
import matplotlib.pyplot as plt
import numpy as np
#data
b = np.logspace(-1, 1, 500)
Rd = {}
for zeta in [0.01, 0.1, 0.2, 0.7, 1]:
Rd[zeta] = b / np.sqrt((1 - b * b) ** 2 + (2 * b * zeta) ** 2)
#plot
fig = plt.figure()
ax1 = fig.add_subplot(111)
for z in Rd:
ax1.loglog(b, Rd[z])
ax1.set_xlim([0.1, 10])
ax1.set_ylim([0.1, 10])
ax1.set_aspect(1.)
#draw lines to look like diagonal spines (axes)
xmin, xmax = ax1.get_xlim() # xlim == ylim
a = np.log10(xmin)
b = np.log10(xmax)
span = b - a
period_points = 3 # number of points/ticks per decade
npts = (span * period_points) + 1 # +1 for even powers of 10
x1 = np.logspace(a, b, num=npts)
x2 = np.logspace(b, a, num=npts)
ax1.plot(x1, x1, color='k', marker='x', ms='9')
ax1.plot(x1, x2, color='k', marker='x', ms='9')
#NOTE: v1.2.1 lacks 'TICKUP' and similar - these may be
# a better choice in v1.3x and beyond
ax1.text(0.97, 0.9,
"axis label: A",
size='large',
horizontalalignment='right',
verticalalignment='top',
rotation=45,
transform=ax1.transAxes,
#bbox={'facecolor': 'white', 'alpha': 0.5, 'pad': 10},
)
ax1.text(0.03, 0.9,
"axis label: B",
size='large',
horizontalalignment='left',
verticalalignment='top',
rotation=-45,
transform=ax1.transAxes,
#bbox={'facecolor': 'white', 'alpha': 0.5, 'pad': 10},
)
plt.savefig("example.pdf")

Categories