How to rotate the offset text in a 3D plot? - python

I'm trying to plot a 3D figure in Matplotlib with the scale in the offset text. For this purpose, I've used ImportanceOfBeingErnest's custom axis' major formatter in order to have this scale in a latex format:
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import numpy as np
class OOMFormatter(matplotlib.ticker.ScalarFormatter):
def __init__(self, order=0, fformat="%1.1f", offset=True, mathText=True):
self.oom = order
self.fformat = fformat
matplotlib.ticker.ScalarFormatter.__init__(self,useOffset=offset,useMathText=mathText)
def _set_order_of_magnitude(self):
self.orderOfMagnitude = self.oom
def _set_format(self, vmin=None, vmax=None):
self.format = self.fformat
if self._useMathText:
self.format = r'$\mathdefault{%s}$' % self.format
x = np.linspace(0, 22, 23)
y = np.linspace(-10, 10, 21)
X, Y = np.meshgrid(x, y)
V = -(np.cos(X/10)*np.cos(Y/10))**2*1e-4
fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
surf = ax.plot_surface(X, Y, V, cmap=cm.jet,
linewidth=0, antialiased=False)
ax.zaxis.set_major_formatter(OOMFormatter(int('{:.2e}'.format(np.min(V)).split('e')[1]), mathText=True))
ax.zaxis.set_rotate_label(False)
ax.set_xlabel(r'$x$ (cm)', size=20, labelpad=10)
ax.set_ylabel(r'$y$ (cm)', size=20, labelpad=10)
ax.set_zlabel(r'$A_z$', size=20, labelpad=10)
This results in the following figure:
Note that the scale (zaxis off-set text, x 10^{-4}) is rotationed 90 degrees. To solve this, I've tried to acess the element of the off-set text and set its rotation to 0:
ax.zaxis.get_offset_text().set_rotation(0)
ax.zaxis.get_offset_text().get_rotation()
>>> 0
Which was of no use, since the off-set text didn't rotate an inch. I've then tried to print the text object when running the plot function:
surf = ax.plot_surface(X, Y, V, cmap=cm.jet,
linewidth=0, antialiased=False)
.
.
.
print(ax.zaxis.get_offset_text())
>>>Text(1, 0, '')
This made me think that perhaps the off-set text wasn't stored inside this variable, but when I run the same command without calling the plot function it returns exactly what I expected it to return:
print(ax.zaxis.get_offset_text())
>>>Text(-0.1039369506424546, 0.050310729257045626, '$\\times\\mathdefault{10^{−4}}\\mathdefault{}$')
What am I doing wrong?

I have to say, this is an excellent and intriguing question and I scratched my head a while over it…
You can access the offset text with ot = ax.zaxis.get_offset_text(). It is easy to hide it ot.set_visible(False), but for some unknown reason it does not work to rotate ot.set_rotation(90). I tried to print the text value with print(ot.get_text()), but this outputs nothing, unless the plot was already drawn. Only after the plot is drawn, it returns '$\\times\\mathdefault{10^{-4}}\\mathdefault{}$'. This tells me that this is likely the source of the problem. Whatever you apply to the offset text, it gets overwritten in a final step of the graph generation and this fails.
I came to the conclusion that the best approach is to hide the offset and annotate yourself the graph. You can do it programmatically using the following snippet:
ax.zaxis.get_offset_text().set_visible(False)
exponent = int('{:.2e}'.format(np.min(V)).split('e')[1])
ax.text(ax.get_xlim()[1]*1.1, ax.get_ylim()[1], ax.get_zlim()[1],
'$\\times\\mathdefault{10^{%d}}\\mathdefault{}$' % exponent)
Result:

Related

Using matlotlib: why do imshow and contourf not plot together? (contourf "overrides" imshow)

I am trying to plot some meteorological data onto a map and I would like to add an image of a plane using imshow. Plotting i) the trajectory, ii) some contour-data and iii) the image, works fine. But as soon as I add a contourf-plot (see below) the image dissapears!
Any ideas how to fix this?
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import cartopy.crs as crs
import cartopy.feature as cfeature
def plot_test():
#DEFINE DATA
x,y = np.meshgrid(np.linspace(0,90,100),np.linspace(0,90,100))
z = x**3 + y**3
#BEGIN FIGURE (IN THIS CASE A MAP, IM PLOTTING METEOROLOGICAL DATA)
fig = plt.figure(figsize = (6,6))
ax1 = plt.axes(projection=crs.PlateCarree(central_longitude=0))
ax1.set_extent([0,90,0,90], crs=crs.PlateCarree())
ax1.coastlines(resolution='auto', color='k')
#EXAMPLE DATA PLOTTED AS CONTOURF
v_max = int(z.max())
v_min = int(z.min())
qcs = ax1.contourf(x, y, z, cmap = "Blues", vmin = v_min, vmax = v_max)
sm = plt.cm.ScalarMappable(cmap="Blues",norm=qcs.norm)
sm._A = []
cbar = plt.colorbar(sm, ax=ax1,orientation="vertical")
cbar.ax.set_ylabel("some contourf data", rotation=90, fontsize = 15)
#PLOT IMAGE OF A PLANE (THIS IS NOT SHOWING UP ON THE PLOT!)
x0 = 50
y0 = 40
img=plt.imread("plane2.png")
ax1.imshow(img,extent=[x0,x0 - 10, y0, y0-10], label = "plane")
plt.show()
without contourf (code from above with lines 14-20 commented out):
with contourf:
Thank you 1000 times #JohanC (see comments). I simply had to place the z-order:
ax1.imshow(img, ...., zorder=3)
which made the plane show up!

Fixed colorange for making plots using matplotlibs

I am currently trying to visualize some dataset with matplotlib, but seem to have some problems with the color range.
The way I am currently making my plots is a following:
def make_plot_store_data(name,interweaved,static):
Y = np.array(range(0,static.shape[1]))
X = np.array(range(0,static.shape[0]))
X,Y = np.meshgrid(X, Y)
plt.pcolormesh(X,Y,np.log10(static.T),cmap=cm.jet)
plt.xlabel('Frames')
plt.ylabel('Frequency(Hz)')
plt.title('Power spectrum of ' + name)
plt.colorbar()
plt.savefig(plot+"/"+name+"_plot_static_conv.png")
plt.close()
Problems with plotting it this way is that the colorbar automatically adjust to the dataset, which is not wanted.
I want a constant colorbar/fixed color bar such that i avoid representation such this:
In which darkred = >18
and here it is:
is darkred = >2.5
I am trying to do pattern recognition, and I think this is somehow messing up with my result.
You can set the min and max color levels using vmin and vmax:
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(0,100,0.2)
y = np.arange(0,40,0.2)
X, Y = np.meshgrid(x,y)
Z = np.random.random((y.shape[0],x.shape[0]))*np.sin(0.01*X*Y)
plt.pcolormesh(X,Y,Z, cmap=plt.cm.jet, vmin=0, vmax=1)
plt.colorbar()
plt.show()

Colorbar only shows one color in Python

Within my B.Sc. thesis I have to investigate the distribution of pore pressure and stresses around a fluid-injecting well. I try to solve this part by using Spyder, which seems to be the best interface for me because I have nearly zero experience in coding.
Although I worked through the most important Python and matplotlib documentation I cant find the solution for my problem.
First here's my code so far:
# -*- coding: utf-8 -*-
"""
Created on Wed Oct 01 10:26:29 2014
#author: Alexander
"""
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from math import *
from scipy.special import *
import matplotlib.pyplot as plt
import numpy as np
q = 6.0/1000
rhof = 1000
lameu = 11.2*10**9
lame = 8.4*10**9
pi
alpha = 0.8
G = 8.4*10**9
k = 4*10**(-14)
eta = 0.001
t = 10*365*24*3600
kappa = k/eta
print "kappa ist:",kappa
c = ((kappa*(lameu-lame)*(lame+2*G))/((alpha**2)*(lameu+2*G)))
print "c ist:",c
xmin = -10
xmax = 10
ymin = -10
ymax = 10
X = np.arange(xmin,xmax,0.5)
Y = np.arange(ymin,ymax,0.5)
x, y = np.meshgrid(X, Y)
r=np.sqrt(x**2+y**2)
P=(q/(rhof*4*pi*kappa))*(expn(1,(r**2)/(4*c*t)))
print x,y
print P
z = P/1000000
fig = plt.figure()
ax = fig.gca(projection='3d')
surf = ax.plot_surface(x, y, z, rstride=1, cstride=1, cmap=cm.jet, linewidth=0,
antialiased=True)
fig.colorbar(surf, shrink=0.5, aspect=5)
ax.set_xlim(xmin,xmax)
ax.set_ylim(ymin,ymax)
ax.set_title('Druckverteilung')
ax.set_xlabel('Distanz zu Well [m]')
ax.set_ylabel('Distanz zu Well [m]')
ax.set_zlabel('Druck in [MPa]')
plt.show()
I have two major questions:
After plotting, my colorbar only shows 1 color (blue) and I don't know why. I looked up for similar problems on this site, yet wasn't able to find a proper solution. How can I get this done?
Let's say I want to know the value of the pressure in my coordinates x=5m and y=2m from my injection point (x,y=0). Is there a code to "grab" this value?
I will try to plot some stresses (e.g. normal stress and shear stress) around a borehole. Can I avoid the error I encountered within the pressure in future plots by simply using your suggested proposition of
z[z == np.inf] = np.nan
and modifying the plot_surface command? I ask because I'm not sure if I will have a value within inf.
The color scale is all blue because one of you z values in inf. You can correct this first by setting the inf values in z to nan:
z[z == np.inf] = np.nan
And then telling plot_surface what range of values to plot using the vmin and vmax arguments:
surf = ax.plot_surface(x, y, z, rstride=1, cstride=1, cmap=cm.jet, linewidth=0,
antialiased=True, vmin=np.nanmin(z), vmax=np.nanmax(z))
You can easily create a function to calculate z for a give x and y like this:
def calcZ(x,y):
r=np.sqrt(x**2+y**2)
P=(q/(rhof*4*pi*kappa))*(expn(1,(r**2)/(4*c*t)))
z = P/1000000
return z
For your first question, if you add vmin=0.15, vmax=0.24 to your ax.plot_surface call, you will get a colored plot. I don't know why plot_surface doesn't do a better job of auto-selecting these color limits, but I guess it doesn't.
Regarding your second question, if you want to be able to grab arbitrary points (not necessarily the ones in your grid), you can use an interpolate function. For example, first create an interpolator function:
from scipy.interpolate import interp2d
intrp = interp2d(X, Y, z)
Then you can use that to calculate values wherever you like. You can get a single point:
a_single_point = intrp(2, 4)
Or a line of points from x=-3 to -1, along y=2:
vals = intrp(np.arange(-3, -1, .1), 2)
Good luck!

Normalizing Colormap Used by Facecolors in Matplotlib

First off, I'm trying to plot spherical harmonics in matplotlib as they are seen here in mayavi: http://docs.enthought.com/mayavi/mayavi/auto/example_spherical_harmonics.html
Here is where I'm at:
import matplotlib.pyplot as plt
from matplotlib import cm, colors
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
from scipy import special
# Create a sphere
r = 3
pi = np.pi
cos = np.cos
sin = np.sin
phi, theta = np.mgrid[0:pi:50j, 0:2*pi:50j]
x = r * sin(phi) * cos(theta)
y = r * sin(phi) * sin(theta)
z = r * cos(phi)
colorfunction=special.sph_harm(3,4,theta,phi).real
norm=colors.Normalize(vmin = np.min(colorfunction), vmax = np.max(colorfunction), clip = False)
print colorfunction
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.scatter(x, y, z)
ax.plot_surface(
x, y, z, rstride=1, cstride=1, norm=norm, cmap=cm.jet, facecolors=cm.jet(colorfunction))
plt.show()
The idea is to use colorfunction to colour the surface of the sphere according to the spherical harmonic. However, the output of this function is an array with negative numbers. What I need to do is 'normalize' this array so it behaves nicely with matplotlib's colormap. However, unlike the answer here, Color matplotlib plot_surface command with surface gradient , where the answer simply preforms a sloppy normalize by dividing by the largest element, I have negative elements so that just won't work. I'd ideally like to use matplotlib.colors.Normalize but it just isn't working on facecolors.
I know that the norm is applying to the cmap=cm.jet, because if I remove facecolors argument entirely I get a new colormap that behaves according to my norm function.
This is the crux of my issue, I cannot get my normalized colormap to apply to my facecolors. Any ideas?
This is the figure the above code currently generates. As you can see the negative values are cut-off entirely and the information is lost because the colormaps range is much larger than the actual values (so everything just looks blue).
Maybe this is too trivial, but:
ax.plot_surface(x, y, z, rstride=1, cstride=1, facecolors=cm.jet(norm(colorfunction)))
This normalizes colorfunction. Also, it is sufficient to define the normalization function by:
norm = colors.Normalize()
This will automatically scale the input between 0..1.
The result:
It seems that cmap and norm keywords apply to the case where one uses Z data to color the surface, so they are not useful here.

keeps text rotated in data coordinate system after resizing?

I'm trying to have a rotated text in matplotlib. unfortunately the rotation seems to be in the display coordinate system, and not in the data coordinate system. that is:
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_axes([0.15, 0.1, 0.8, 0.8])
t = np.arange(0.0, 1.0, 0.01)
line, = ax.plot(t, t, color='blue', lw=2)
ax.text (0.51,0.51,"test label", rotation=45)
plt.show()
will give a line that will be in a 45 deg in the data coordinate system, but the accompanied text will be in a 45 deg in the display coordinate system.
I'd like to have the text and data to be aligned even when resizing the figure.
I saw here that I can transform the rotation, but this will works only as long as the plot is not resized.
I tried writing ax.text (0.51,0.51,"test label", transform=ax.transData, rotation=45), but it seems to be the default anyway, and doesn't help for the rotation
Is there a way to have the rotation in the data coordinate system ?
EDIT:
I'm interested in being able to resize the figure after I draw it - this is because I usually draw something and then play with the figure before saving it
You may use the following class to create the text along the line. Instead of an angle it takes two points (p and pa) as input. The connection between those two points define the angle in data coordinates. If pa is not given, the connecting line between p and xy (the text coordinate) is used.
The angle is then updated automatically such that the text is always oriented along the line. This even works with logarithmic scales.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.text as mtext
import matplotlib.transforms as mtransforms
class RotationAwareAnnotation(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
self.calc_angle_data()
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_data(self):
ang = np.arctan2(self.p[1]-self.pa[1], self.p[0]-self.pa[0])
self.angle_data = np.rad2deg(ang)
def _get_rotation(self):
return self.ax.transData.transform_angles(np.array((self.angle_data,)),
np.array([self.pa[0], self.pa[1]]).reshape((1, 2)))[0]
def _set_rotation(self, rotation):
pass
_rotation = property(_get_rotation, _set_rotation)
Example usage:
fig, ax = plt.subplots()
t = np.arange(0.0, 1.0, 0.01)
line, = ax.plot(t, t, color='blue', lw=2)
ra = RotationAwareAnnotation("test label", xy=(.5,.5), p=(.6,.6), ax=ax,
xytext=(2,-1), textcoords="offset points", va="top")
plt.show()
Alternative for edge-cases
The above may fail in certain cases of text along a vertical line or on scales with highly dissimilar x- and y- units (example here). In that case, the following would be better suited. It calculates the angle in screen coordinates, instead of relying on an angle transformation.
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)
For usual cases, both result in the same output. I'm not sure if the second class has any drawbacks, so I'll leave both in here, choose whichever you seem more suitable.
Ok, starting off with code similar to your example:
%pylab inline
import numpy as np
fig = plt.figure()
ax = fig.add_axes([0.15, 0.1, 0.8, 0.8])
t = np.arange(0.0, 1.0, 0.01)
line, = ax.plot(t, t, color='blue', lw=2)
ax.text(0.51,0.51,"test label", rotation=45)
plt.show()
As you indicated, the text label is not rotated properly to be parallel with the line.
The dissociation in coordinate systems for the text object rotation relative to the line has been explained at this link as you indicated. The solution is to transform the text rotation angle from the plot to the screen coordinate system, and let's see if resizing the plot causes issues as you suggest:
for fig_size in [(3.0,3.0),(9.0,3.0),(3.0,9.0)]: #use different sizes, in inches
fig2 = plt.figure(figsize=fig_size)
ax = fig2.add_axes([0.15, 0.1, 0.8, 0.8])
text_plot_location = np.array([0.51,0.51]) #I'm using the same location for plotting text as you did above
trans_angle = gca().transData.transform_angles(np.array((45,)),text_plot_location.reshape((1,2)))[0]
line, = ax.plot(t, t, color='blue', lw=2)
ax.text(0.51,0.51,"test label", rotation=trans_angle)
plt.show()
Looks good to me, even with resizing. Now, if you make the line longer and the axis limits longer, then of course you'd have to adjust the text drawing to occur at the new center of the plot.

Categories