How can I rotate arrowheads in 3D quiver in Matplotlib? - python

I'm trying to replicate the following plot using Python and Matplotlib.
However, the best I have been able to produce is the following:
The main issue here is the not in-plane arrows heads, even if I am not satisfied with the quality of the plot in general. I've searched for a solution to use a 2D quiver in a 3D plot, but I haven't found any useful information about how to do that. Is there another way to achieve in-plane arrowheads?
import numpy as np
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
params = {
'font.family' : 'serif',
'mathtext.fontset': 'stix',
'axes.labelsize': 13,
'legend.fontsize': 8,
'xtick.labelsize': 13,
'ytick.labelsize': 13,
'text.usetex': True,
'figure.figsize': [10, 5]
}
plt.rcParams.update(params)
plt.close('all')
x_ax = np.linspace(-10, 10, 24)
y_ax = np.linspace(-10, 10, 24)
x, y = np.meshgrid(x_ax, y_ax, indexing='ij')
r = np.sqrt(x**2 + y**2)
j_x = -y/r*(- np.exp(-np.abs(r)) + np.exp(-np.abs(r)/2) )*2
j_y = +x/r*(- np.exp(-np.abs(r)) + np.exp(-np.abs(r)/2) )*2
#c = np.arctan2(x, -y)
c = np.sqrt(j_x**2 + j_y**2)
c = (c.ravel() - c.min()) / c.ptp()
c = np.concatenate((c, np.repeat(c, 2)))
c = cm.jet(c)
#c = plt.cm.hsv(c)
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.quiver(x, y, 0, j_x, j_y, 0, colors=c, length=1.2, pivot='middle')
t = np.linspace(-10, 10, 200)
psi = 1 - np.exp(-np.abs(t))
b = np.exp(-t**2)
j_abs = np.abs(t)*np.exp(-t**2)*2
#j_abs = (- np.exp(-np.abs(t)) + np.exp(-np.abs(t)/2) )*2
ax.plot(t, psi, zs=0, zdir='y', label=r"$|\psi|$")
ax.plot(t, b, zs=0, zdir='y', label=r"$|\vec B|$")
ax.plot(t, j_abs, zs=0, zdir='y', label=r"$|\vec j|$")
ax.legend()
ax.set_proj_type('ortho')
ax.set_axis_off()
ax.set_zlim([-0.2, 1.4])
ax.view_init(elev=45, azim=90)
ax.dist=5
fig.savefig("vortex.pdf", bbox_inches="tight")

Maybe mplot3d is not the right tool here, because this is not a truly 3-dimensional plot, but just a combination of two 2-dimensional plots. Consider this approach:
Plot the arrows for the bottom plane in two dimensions, as they would look from above the center, and save the plot as a square image.
Create another image as a projection of the first one, viewed from the desired perspective. E.g. with warpPerspective() from OpenCV.
Make a new plot containing the three lineplots, inserting the image from 2. with plt.imshow().
I guess this is roughly how the original plot above was made. It will take care of effects such as the arrowheads being in the plane, and arrows in the foreground being larger than those in the background.

Related

3D surface plot with section plan in Matplotlib

Basically I have a surface plot consisting of a set of time series, and I would like to add a section plan at a specific height, to better understand the period of the year when values are higher than the selected threshold.
From this:
where the plan is shown but not as a section
To This:
Any suggestion?
Plying with alpha and camera elevation did not solve the issue
the plan still seems to be in front of the figure, not as a section
Drawing in 3 steps
As others pointed out, matplotlib's 3D capabilities are somewhat limited. To hide objects behind other objects, it uses the painter's algorithm. So, the objects are simply drawn back to front, and no objects are put partly before and partly behind some plane. Matplotlib calculates some average depth of each object to define the order. You can overwrite this order via ax.computed_zorder = False, as the automatic calculation is not always what is wished.
You could draw the "layers" yourself:
the 3D surface
then the plane
then the part of the 3D surface that should be visible on top
An example:
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
from scipy.ndimage.filters import gaussian_filter
x = np.linspace(-10, 10, 51)
y = np.linspace(-10, 10, 51)
X, Y = np.meshgrid(x, y)
np.random.seed(20220201)
Z = np.random.rand(*X.shape) ** 5
Z[X ** 2 + Y ** 2 > 30] = 0
Z = gaussian_filter(Z, sigma=2) * 100
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.computed_zorder = False
ax.plot_surface(X, Y, Z, cmap='turbo')
special_z = 16
ax.plot_surface(X, Y, np.full_like(Z, special_z), color='blue', alpha=0.4)
ax.plot_surface(X, Y, np.where(Z >= special_z, Z, np.nan), cmap='turbo', vmin=0)
plt.show()
Drawing layer by layer
An alternative way could be to draw the surface one layer at a time.
The example at the left shows the surface divided into 30 layers, the example at the right stops at a given height, visualizing the intersection.
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
from scipy.ndimage.filters import gaussian_filter
x = np.linspace(-10, 10, 51)
y = np.linspace(-10, 10, 51)
X, Y = np.meshgrid(x, y)
np.random.seed(20220201)
Z = np.random.rand(*X.shape) ** 5
Z[X ** 2 + Y ** 2 > 30] = 0
Z = gaussian_filter(Z, sigma=2) * 100
fig = plt.figure()
for which in ['left', 'right']:
ax = fig.add_subplot(121 + (which == 'right'), projection="3d")
ax.computed_zorder = False
layers = np.linspace(Z.min(), Z.max(), 32)[1:-1]
colors = plt.get_cmap('turbo', len(layers)).colors
special_z = 16
plane_drawn = False
for layer, color in zip(layers, colors):
if layer >= special_z and not plane_drawn:
ax.plot_surface(X, Y, np.full_like(Z, special_z), color='blue', alpha=0.5, zorder=2)
plane_drawn = True
ax.contour(X, Y, Z, levels=[layer], offset=layer, colors=[color])
if plane_drawn and which == 'right':
break
plt.show()

The Matplotlib Result is Different From WolfarmAlpha

I want to plot some equation in Matplotlib. But it has different result from Wolframalpha.
This is the equation:
y = 10yt + y^2t + 20
The plot result in wolframalpha is:
But when I want to plot it in the matplotlib with these code
# Creating vectors X and Y
x = np.linspace(-2, 2, 100)
# Assuming α is 10
y = ((10*y*x)+((y**2)*x)+20)
# Create the plot
fig = plt.figure(figsize = (10, 5))
plt.plot(x, y)
The result is:
Any suggestion to modify to code so it has similar plot result as wolframalpha? Thank you
As #Him has suggested in the comments, y = ((10*y*x)+((y**2)*x)+20) won't describe a relationship, so much as make an assignment, so the fact that y appears on both sides of the equation makes this difficult.
It's not trivial to express y cleanly in terms of x, but it's relatively easy to express x in terms of y, and then graph that relationship, like so:
import numpy as np
import matplotlib.pyplot as plt
y = np.linspace(-40, 40, 2000)
x = (y-20)*(((10*y)+(y**2))**-1)
fig, ax = plt.subplots()
ax.plot(x, y, linestyle = 'None', marker = '.')
ax.set_xlim(left = -4, right = 4)
ax.grid()
ax.set_xlabel('x')
ax.set_ylabel('y')
Which produces the following result:
If you tried to plot this with a line instead of points, you'll get a big discontinuity as the asymptotic limbs try to join up
So you'd have to define the same function and evaluate it in three different ranges and plot them all so you don't get any crossovers.
import numpy as np
import matplotlib.pyplot as plt
y1 = np.linspace(-40, -10, 2000)
y2 = np.linspace(-10, 0, 2000)
y3 = np.linspace(0, 40, 2000)
x = lambda y: (y-20)*(((10*y)+(y**2))**-1)
y = np.hstack([y1, y2, y3])
fig, ax = plt.subplots()
ax.plot(x(y), y, linestyle = '-', color = 'b')
ax.set_xlim(left = -4, right = 4)
ax.grid()
ax.set_xlabel('x')
ax.set_ylabel('y')
Which produces this result, that you were after:

Matplotlib - rotating text on log scale where angles are incorrectly rounded

I am trying to have text rotate onto a plot which is shown on log scale. When I compute the angles (based on the solution in this answer) the angles are getting incorrectly rounded to 0 or 90 degrees. This is because the angles are computed on a linear scale first, and then transformed. This calculation in linear space is the cause of the trouble. Even in a situation where I know the gradient, (either in a linear or logarithmic scale), I am not sure how I can put this onto the graph correctly.
MWE
import matplotlib as mpl
rc_fonts = {
"text.usetex": True,
'text.latex.preview': True,
"font.size": 50,
'mathtext.default': 'regular',
'axes.titlesize': 55,
"axes.labelsize": 55,
"legend.fontsize": 50,
"xtick.labelsize": 50,
"ytick.labelsize": 50,
'figure.titlesize': 55,
'figure.figsize': (10, 6.5), # 15, 9.3
'text.latex.preamble': [
r"""\usepackage{lmodern,amsmath,amssymb,bm,physics,mathtools,nicefrac,letltxmacro,fixcmex}
"""],
"font.family": "serif",
"font.serif": "computer modern roman",
}
mpl.rcParams.update(rc_fonts)
import matplotlib.pylab as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes, InsetPosition, mark_inset
import numpy as np
x = np.linspace(0, 20, 100)
y = np.exp(x**2)
g = 2*x*y # Gradient.
lg = 2 * x # Gradient on a log scale.
plt.clf()
plt.plot(x, y)
plt.yscale('log')
for x in [0,2,4,7,18]:
angle_data = np.rad2deg(np.arctan2(2 * x * np.exp(x**2), 1))
y = np.exp(x**2)
angle_screen = plt.gca().transData.transform_angles(np.array((angle_data,)), np.array([x, y]).reshape((1, 2)))[0]
plt.gca().text(x, y, r'A', rotation_mode='anchor', rotation=angle_screen, horizontalalignment='center')
plt.ylim(1e0, 1e180)
plt.xlim(-1, 20)
plt.xlabel(r'$x$')
plt.title(r'$\exp(x^2)$', y=1.05)
plt.savefig('logscale.pdf', format='pdf', bbox_inches='tight')
A few ideas?
I had tried to use the fact that for very large functions I can calculate the difference from 90 degrees using arctan(x) ~ pi/2 - arctan(1/x), and the former angle uses the low angle approximation so is just 1/x. However, after plugging this into transform_angles this is rounded incorrectly.
A slight hack of a solution
If I guess the aspect ratio of the figure (c0.6) and then also adjust for the difference in scales (x in [0:20] while log10(y) is in [0:180], giving a difference of 9 in scale), then I can get the following, although I don't think this is particularly sustainable, especially if I want to tweak something later.
# The 9 comes from tha fact that x is in [0:20], log10(y) is in [0, 180]. The factor of 0.6 is roughly the aspect ratio of the main plot shape.
plt.gca().text(x, y, r'A', rotation_mode='anchor', rotation=np.rad2deg(np.arctan(0.6 * x/9.0)), horizontalalignment='center')
I updated the solution to the original question with a class RotationAwareAnnotation2, which will be better suited here. It would first transform the points into screen coordinates, and then apply the rotation.
This this case it would look as follows.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.text as mtext
import matplotlib.transforms as mtransforms
class RotationAwareAnnotation2(mtext.Annotation):
def __init__(self, s, xy, p, pa=None, ax=None, **kwargs):
self.ax = ax or plt.gca()
self.p = p
if not pa:
self.pa = xy
kwargs.update(rotation_mode=kwargs.get("rotation_mode", "anchor"))
mtext.Annotation.__init__(self, s, xy, **kwargs)
self.set_transform(mtransforms.IdentityTransform())
if 'clip_on' in kwargs:
self.set_clip_path(self.ax.patch)
self.ax._add_text(self)
def calc_angle(self):
p = self.ax.transData.transform_point(self.p)
pa = self.ax.transData.transform_point(self.pa)
ang = np.arctan2(p[1]-pa[1], p[0]-pa[0])
return np.rad2deg(ang)
def _get_rotation(self):
return self.calc_angle()
def _set_rotation(self, rotation):
pass
_rotation = property(_get_rotation, _set_rotation)
x = np.linspace(0, 20, 100)
f = lambda x: np.exp(x**2)
y = f(x)
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set(yscale = 'log', ylim=(1e0, 1e180), xlim=(-1, 20), xlabel=r'$x$')
annots= []
for xi in [0,2,4,7,18]:
an = RotationAwareAnnotation2("A", xy=(xi,f(xi)), p=(xi+.01,f(xi+.01)), ax=ax,
xytext=(-1,1), textcoords="offset points",
ha="center", va="baseline", fontsize=40)
annots.append(an)
ax.set_title(r'$\exp(x^2)$', y=1.05)
fig.savefig('logscale.pdf', format='pdf', bbox_inches='tight')
plt.show()

Matplotlib: plotting line chart set line type, scale and customized colors

I am newbie with Python and Matplotlib, and I'm trying to do a line plot with the following code:
cov = np.array([164430.5, 84322.6, 83595.6])
p2 = np.array([92118.2, 30813.8, 36428.7])
p5 = np.array([16033.3, 15940.9, 16555.1])
s = np.array([315262.9, 176823.4, 182933.4])
ax = np.array([1, 8, 32])
plt.plot(ax, cov, 'r--', ax, p2, 'bs', ax, p5,'g^', ax, s, 'y*')
plt.show()
So far, I see the plot working well, but I do not know how can I do the following:
Join the symbols with a smooth line
Change the X axis to see only 1, 8 and 32 but also with the same distance between them.
Change the scale to a logaritmic.
Apply to each line, some customized colors in hexadecimal.
Could you guide me? Thanks a lot!
I am answering my own question to help someone else if they need it!:
Join the symbols with a smooth line
Change 'r--' for 'r-'.
Change the X axis to see only 1, 8 and 32 but also with the same distance between them.
Finally, I didn't this, but something approximately is using this: plt.xticks(ax, (1, 8, 32)) . The only thing I coudn't do is to keep them equidistant.
Change the scale to a logaritmic.
plt.yscale('log')
Apply to each line, some customized colors in hexadecimal.
color = '#FF7373'
I have also changed the way to write everything, because it was clearer for me. So here is all together:
import numpy as np
import matplotlib.pyplot as plt
cov = np.array([164430.5, 84322.6, 83595.6])
p2 = np.array([92118.2, 30813.8, 36428.7])
p5 = np.array([16033.3, 15940.9, 16555.1])
s = np.array([315262.9, 176823.4, 182933.4])
ax = np.array([1, 8, 32])
plt.xticks(ax, (1, 8, 32))
line1, = plt.plot(ax, cov, '*-', label='cov', color = '#4F81BD') # blue
line2, = plt.plot(ax, p2, 'o-', label='p2',color = '#C0504D') # red
line3, = plt.plot(ax, p5, '^-', label='p5', color = '#9BBB59') # green
line4, = plt.plot(ax, s, 's-', label='s', color = '#FF7373') # pink
plt.legend()
plt.grid(True)
plt.show()

Shade 'cells' in polar plot with matplotlib

I've got a bunch of regularly distributed points (θ = n*π/6, r=1...8), each having a value in [0, 1]. I can plot them with their values in matplotlib using
polar(thetas, rs, c=values)
But rather then having just a meagre little dot I'd like to shade the corresponding 'cell' (ie. everything until halfway to the adjacent points) with the colour corresponding to the point's value:
(Note that here my values are just [0, .5, 1], in really they will be everything between 0 and 1. Is there any straight-forward way of realising this (or something close enough) with matplotlib? Maybe it's easier to think about it as a 2D-histogram?
This can be done quite nicely by treating it as a polar stacked barchart:
import matplotlib.pyplot as plt
import numpy as np
from random import choice
fig = plt.figure()
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
for i in xrange(12*8):
color = choice(['navy','maroon','lightgreen'])
ax.bar(i * 2 * np.pi / 12, 1, width=2 * np.pi / 12, bottom=i / 12,
color=color, edgecolor = color)
plt.ylim(0,10)
ax.set_yticks([])
plt.show()
Produces:
Sure! Just use pcolormesh on a polar axes.
E.g.
import matplotlib.pyplot as plt
import numpy as np
# Generate some data...
# Note that all of these are _2D_ arrays, so that we can use meshgrid
# You'll need to "grid" your data to use pcolormesh if it's un-ordered points
theta, r = np.mgrid[0:2*np.pi:20j, 0:1:10j]
z = np.random.random(theta.size).reshape(theta.shape)
fig, (ax1, ax2) = plt.subplots(ncols=2, subplot_kw=dict(projection='polar'))
ax1.scatter(theta.flatten(), r.flatten(), c=z.flatten())
ax1.set_title('Scattered Points')
ax2.pcolormesh(theta, r, z)
ax2.set_title('Cells')
for ax in [ax1, ax2]:
ax.set_ylim([0, 1])
ax.set_yticklabels([])
plt.show()
If your data isn't already on a regular grid, then you'll need to grid it to use pcolormesh.
It looks like it's on a regular grid from your plot, though. In that case, gridding it is quite simple. If it's already ordered, it may be as simple as calling reshape. Otherwise, a simple loop or exploiting numpy.histogram2d with your z values as weights will do what you need.
Well, it's fairly unpolished overall, but here's a version that rounds out the sections.
from matplotlib.pylab import *
ax = subplot(111, projection='polar')
# starts grid and colors
th = array([pi/6 * n for n in range(13)]) # so n = 0..12, allowing for full wrapping
r = array(range(9)) # r = 0..8
c = array([[random_integers(0, 10)/10 for y in range(th.size)] for x in range(r.size)])
# The smoothing
TH = cbook.simple_linear_interpolation(th, 10)
# Properly padding out C so the colors go with the right sectors (can't remember the proper word for such segments of wedges)
# A much more elegant version could probably be created using stuff from itertools or functools
C = zeros((r.size, TH.size))
oldfill = 0
TH_ = TH.tolist()
for i in range(th.size):
fillto = TH_.index(th[i])
for j, x in enumerate(c[:,i]):
C[j, oldfill:fillto].fill(x)
oldfill = fillto
# The plotting
th, r = meshgrid(TH, r)
ax.pcolormesh(th, r, C)
show()

Categories

Resources