Consider a 3D bar plot with custom grid lines:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib.ticker import MultipleLocator
# This import registers the 3D projection, but is otherwise unused.
from mpl_toolkits.mplot3d import Axes3D # noqa: F401 unused import
fig = plt.figure(figsize=(20, 10))
ax = fig.add_subplot(111, projection='3d')
ax.xaxis.set_major_locator(MultipleLocator(1))
ax.yaxis.set_major_locator(MultipleLocator(1))
ax.zaxis.set_major_locator(MultipleLocator(2))
nx = 10
ny = 10
colors = cm.tab20(np.linspace(0, 1, nx))
width = depth = 0.1
for x in np.arange(nx):
for y in np.arange(ny):
ax.bar3d(x, y, 0, width, depth, x+y, shade=False, color = colors[x], edgecolor = 'black')
plt.show()
How can I place the bars so that the bars are centered where the grid lines cross each other in the xy plane?
I'm thinking about something like
ax.bar3d(x+0.5*depth, y+0.5*width, ...)
only it is not clear to me what the offset is that matplotlib uses. It should work for all depth and width values.
For 2D bar plots there is an argument for this, align = 'center', but it doesn't seem to work for 3D.
What looks to you as a shift in coordinates is really just the projection in combination with the margins of the axes. Hence even if the bars are correctly positionned in their center they look offset and that offset is dependent on the axes size, viewing angle etc.
The solution to this is in principle given in this Q&A:
Removing axes margins in 3D plot
You would center the bars by subtracting half of their width and add a patch to remove the margin of the zaxis. Then setting the lower z limit to 0 pins the bars to the grid and makes them look centered for any viewing angle.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib.ticker import MultipleLocator
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.axis3d import Axis
def _get_coord_info_new(self, renderer):
mins, maxs, cs, deltas, tc, highs = self._get_coord_info_old(renderer)
correction = deltas * [0,0,1.0/4]
mins += correction
maxs -= correction
return mins, maxs, cs, deltas, tc, highs
if not hasattr(Axis, "_get_coord_info_old"):
Axis._get_coord_info_old = Axis._get_coord_info
Axis._get_coord_info = _get_coord_info_new
fig = plt.figure(figsize=(20, 10))
ax = fig.add_subplot(111, projection='3d')
ax.xaxis.set_major_locator(MultipleLocator(1))
ax.yaxis.set_major_locator(MultipleLocator(1))
ax.zaxis.set_major_locator(MultipleLocator(2))
nx = 10
ny = 10
colors = cm.tab20(np.linspace(0, 1, nx))
width = depth = 0.1
for x in np.arange(nx):
for y in np.arange(ny):
ax.bar3d(x-width/2., y-depth/2., 0, width, depth, x+y, shade=False,
color = colors[x], edgecolor = 'black')
ax.set_zlim(0,None)
plt.show()
Related
I've been plotting a dataframe using the following code within a Jupyter Notebook: For comparision with older data only available on paper in the scale 0.005mm=1cm, I need to export and print the graph in the same scale: 0.005mm in the figure (both x and y-axis) have to be 1cm in the figure.
Is there any way how I can define a custom scale? For information, the x-range and y-range are not fixed, they will vary depending on the data I am loading into the dataframe.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.ticker as ticker
df = pd.DataFrame(np.array([[1.7, 0], [1.75, -0.012], [1.8, 0]]),
columns=['pos', 'val'])
# Plot results
sns.set()
plt.figure(figsize=(20,30))
plt.plot(df['pos'], df['val'])
ax = plt.axes()
ax.set_aspect('equal')
plt.xlabel('Position [mm]')
plt.ylabel('Höhe [mm]')
ax.xaxis.set_major_locator(ticker.MultipleLocator(0.005))
ax.yaxis.set_major_locator(ticker.MultipleLocator(0.005))
plt.show()
In a
comment
I suggested to use matplotlib.transforms — well I was wrong, the way
to go is to shamelessly steal from Matplotlib's Demo Fixed Size
Axes…
(the figure was resized by StackOverflow to fit in the post, but you
can check that the proportions are correct)
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import Divider, Size
from mpl_toolkits.axes_grid1.mpl_axes import Axes
cm = lambda d: d/2.54
x, y = [1700.0, 1725.0, 1750.0], [0.0, -12.0, 0.0] # μm
dx, dy = 50.0, 12.0
# take margins into account
xmin, xmax = min(x)-dx*0.05, max(x)+dx*0.05
ymin, ymax = min(y)-dy*0.05, max(y)+dy*0.05
dx, dy = xmax-xmin, ymax-ymin
# 5 μm data == 1 cm plot
scale = 5/1
xlen, ylen = dx/scale, dy/scale
# Now we know the extents of our data and the axes dimension,
# so we can set the Figure dimensions, taking borders into account
left, right = 2, 1
bot, top = 1.5, 1.5
fig = plt.figure(
figsize=(cm(left+xlen+right), cm(bot+ylen+top)),
dpi=118)
# change bg color to show so that one can measure the figure
# and the axes when pasted into SO and do their math…
fig.set_facecolor('xkcd:grey teal')
########## Below is stolen from Matplotlib Fixed Size Axes
########## (please don't ask me…)
# Origin and size of the x axis and y axis
h = [Size.Fixed(cm(left)), Size.Fixed(cm(xlen))]
v = [Size.Fixed(cm(bot)), Size.Fixed(cm(ylen))]
divider = Divider(fig, (0.0, 0.0, 1., 1.), h, v, aspect=False)
# NB: Axes is from mpl_toolkits.axes_grid1.mpl_axes
ax = Axes(fig, divider.get_position())
ax.set_axes_locator(divider.new_locator(nx=1, ny=1))
fig.add_axes(ax)
######### Above is stolen from Matplotlib Fixed Size Axes Demo
plt.plot(x,y)
plt.grid()
ax.set(xlim=(xmin, xmax), ylim=(ymin, ymax), yticks=range(-12,1,3),
xlabel='X/μm', ylabel='Y/μm',
title='X vs Y, 1 cm on plot equals 5 μm')
fig.suptitle('Figure dimensions: w = %.2f cm, h = %.2f cm.'%(
left+xlen+right, bot+ylen+top))
fig.savefig('Figure_1.png',
# https://stackoverflow.com/a/4805178/2749397, Joe Kington's
facecolor=fig.get_facecolor(), edgecolor='none')
1 inch = 2.54 cm, so 254/0.005 = 50800 dpi
plt.figure(figsize=(20,30), dpi=50800)
How can I set the colormap in relation to the radius of the figure?
And how can I close the ends of the cylinder (on the element, not the top and bottom bases)?
My script:
import numpy as np
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from math import sin, cos, pi
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
h, w = 60,30
znew = np.random.randint(low=90, high=110, size=(60,30))
theta = np.linspace(0,2*pi, h)
Z = np.linspace(0,1,w)
Z,theta = np.meshgrid(Z, theta)
R = 1
X = (R*np.cos(theta))*znew
Y = (R*np.sin(theta))*znew
ax1 = ax.plot_surface(X,Y,Z,linewidth = 0, cmap="coolwarm",
vmin= 80,vmax=130, shade = True, alpha = 0.75)
fig.colorbar(ax1, shrink=0.9, aspect=5)
plt.show()
First you need to use the facecolors keyword argument of plot_surface to draw your surface with arbitrary (non-Z-based) colours. You have to pass an explicit RGBA colour four each point, which means we need to sample a colormap object with the keys given by the radius at every point. Finally, this will break the mappable property of the resulting surface, so we will have to construct the colorbar by manually telling it to use our radii for colours:
import numpy as np
from matplotlib import pyplot as plt
import matplotlib.cm as cm
from matplotlib.colors import Normalize
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
h, w = 60,30
#znew = np.random.randint(low=90, high=110, size=(h,w))
theta = np.linspace(0,2*np.pi, h)
Z = np.linspace(0,1,w)
Z,theta = np.meshgrid(Z, theta)
znew = 100 + 10*np.cos(theta/2)*np.cos(2*Z*np.pi)
R = 1
X = (R*np.cos(theta))*znew
Y = (R*np.sin(theta))*znew
true_radius = np.sqrt(X**2 + Y**2)
norm = Normalize()
colors = norm(true_radius) # auto-adjust true radius into [0,1] for color mapping
cmap = cm.get_cmap("coolwarm")
ax.plot_surface(X, Y, Z, linewidth=0, facecolors=cmap(colors), shade=True, alpha=0.75)
# the surface is not mappable, we need to handle the colorbar manually
mappable = cm.ScalarMappable(cmap=cmap)
mappable.set_array(colors)
fig.colorbar(mappable, shrink=0.9, aspect=5)
plt.show()
Note that I changed the radii to something smooth for a less chaotic-looking result. The true_radius arary contains the actual radii in data units, which after normalization becomes colors (essentially colors = (true_radius - true_radius.min())/true_radius.ptp()).
The result:
Finally, note that I generated the radii such that the cylinder doesn't close seamlessly. This mimicks your random example input. There's nothing you can do about this as long as the radii are not 2π-periodic in theta. This has nothing to do with visualization, this is geometry.
I'm making plots using matplotlib colormap "seismic" and would like to have the white color centered on 0. When I run my script with no changes, white falls from 0 to -10. I tried then setting vmin=-50, vmax=50 but I completely lose the white in that case. Any suggestions on how to accomplish that?
from netCDF4 import Dataset as NetCDFFile
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.basemap import Basemap
nc = NetCDFFile('myfile.nc')
lat = nc.variables['lat'][:]
lon = nc.variables['lon'][:]
time = nc.variables['time'][:]
hgt = nc.variables['hgt'][:]
map = Basemap(llcrnrlon=180.,llcrnrlat=0.,urcrnrlon=320.,urcrnrlat=80.)
lons,lats = np.meshgrid(lon,lat)
x,y = map(lons,lats)
cs = map.contourf(x,y,hgt[0],cmap='seismic')
cbar = plt.colorbar(cs, orientation='horizontal', shrink=0.5,
cmap='seismic')
cbar.set_label('500mb Geopotential Height Anomalies(m)')
map.drawcoastlines()
map.drawparallels(np.arange(20,80,20),labels=[1,1,0,0], linewidth=0.5)
map.drawmeridians(np.arange(200,320,20),labels=[0,0,0,1], linewidth=0.5)
plt.show()`
Plot with defaults
Plot with vmin, vmax set
You can set the levels you want to show manually. As long as you have the same spacing of intervals to the left and to the right of zero this works nicely.
levels = [-50,-40,-30,-20,-10,10,20,30,40,50]
ax.contourf(X,Y,Z, levels)
Example:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-6.3,6.3)
y = np.linspace(-3.1,3.1)
X,Y = np.meshgrid(x,y)
Z = -np.cos(X)*np.cos(Y)*45
levels = [-50,-40,-30,-20,-10,10,20,30,40,50]
fig, ax = plt.subplots(figsize=(4,2))
cont = ax.contourf(X,Y,Z,levels, cmap="seismic")
fig.colorbar(cont, orientation="horizontal")
plt.show()
Or, if you want the colorbar to be proportional to the data,
fig.colorbar(cont, orientation="horizontal", spacing="proportional")
If levels are unequal, you need to specify vmin and vmax.
levels = [-50,-40,-30,-20,-10,10,30,50,80,100]
cont = ax.contourf(X,Y,Z,levels, cmap="seismic", vmin=-50, vmax=50)
The disadvantage is that you loose resolution, hence you may use a BoundaryNorm to select equally spaced colors for unequally spaced labels.
import matplotlib.pyplot as plt
import matplotlib.colors
import numpy as np
x = np.linspace(-6.3,6.3)
y = np.linspace(-3.1,3.1)
X,Y = np.meshgrid(x,y)
Z = -np.cos(X)*np.cos(Y)*45
levels = [-50,-40,-30,-20,-10,10,30,50,80,100]
norm = matplotlib.colors.BoundaryNorm(levels, len(levels)-1)
fig, ax = plt.subplots(figsize=(4,2))
cont = ax.contourf(X,Y,Z,levels,cmap=plt.get_cmap("seismic",len(levels)-1), norm=norm)
fig.colorbar(cont, orientation="horizontal")
plt.show()
To change the ticklabels on the colorbar so something other than the levels or in case they are too dence you may use the ticks argument.
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-6.3,6.3)
y = np.linspace(-3.1,3.1)
X,Y = np.meshgrid(x,y)
Z = -np.cos(X)*np.cos(Y)*45
levels = np.arange(-45,50,5)
levels = levels[levels!=0]
ticks=np.arange(-40,50,10)
fig, ax = plt.subplots(figsize=(4,2))
cont = ax.contourf(X,Y,Z,levels,cmap="seismic", spacing="proportional")
fig.colorbar(cont, orientation="horizontal", ticks=ticks, spacing="proportional")
plt.show()
I'm struggling with creating a quite complex 3d figure in python, specifically using iPython notebook. I can partition the content of the graph into two sections:
The (x,y) plane: Here a two-dimensional random walk is bobbing around, let's call it G(). I would like to plot part of this trajectory on the (x,y) plane. Say, 10% of all the data points of G(). As G() bobs around, it visits some (x,y) pairs more frequently than others. I would like to estimate this density of G() using a kernel estimation approach and draw it as contour lines on the (x,y) plane.
The (z) plane: Here, I would like to draw a mesh or (transparent) surface plot of the information theoretical surprise of a bivariate normal. Surprise is simply -log(p(i)) or the negative (base 2) logarithm of outcome i. Given the bivariate normal, each (x,y) pair has some probability p(x,y) and the surprise of this is simply -log(p(x,y)).
Essentially these two graphs are independent. Assume the interval of the random walk G() is [xmin,xmax],[ymin,ymax] and of size N. The bivariate normal in the z-plane should be drawn from the same interval, such that for each (x,y) pair in the random walk, I can draw a (dashed) line from some subset of the random walk n < N to the bivariate normal. Assume that G(10) = (5,5) then I would like to draw a dashed line from (5,5) up the Z-axes, until it hits the bivariate normal.
So far, I've managed to plot G() in a 3-d space, and estimate the density f(X,Y) using scipy.stats.gaussian_kde. In another (2d) graph, I have the sort of contour lines I want. What I don't have, is the contour lines in the 3d-plot using the estimated KDE density. I also don't have the bivariate normal plot, or the projection of a few random points from the random walk, to the surface of the bivariate normal. I've added a hand drawn figure, which might ease intuition (ignore the label on the z-axis and the fact that there is no mesh.. difficult to draw!)
Any input, even just partial, such as how to draw the contour lines in the (x,y) plane of the 3d graph, or a mesh of a bivariate normal would be much appreciated.
Thanks!
import matplotlib as mpl
import matplotlib.pyplot as plt
import random
import numpy as np
import seaborn as sns
import scipy
from mpl_toolkits.mplot3d import Axes3D
%matplotlib inline
def randomwalk():
mpl.rcParams['legend.fontsize'] = 10
xyz = []
cur = [0, 0]
for _ in range(400):
axis = random.randrange(0, 2)
cur[axis] += random.choice([-1, 1])
xyz.append(cur[:])
x, y = zip(*xyz)
data = np.vstack([x,y])
kde = scipy.stats.gaussian_kde(data)
density = kde(data)
fig1 = plt.figure()
ax = fig1.gca(projection='3d')
ax.plot(x, y, label='Random walk')
sns.kdeplot(data[0,:], data[1,:], 0)
ax.scatter(x[-1], y[-1], c='b', marker='o') # End point
ax.legend()
fig2 = plt.figure()
sns.kdeplot(data[0,:], data[1,:])
Calling randomwalk() initialises and plots this:
Edit #1:
Made some progress, actually the only thing I need is to restrict the height of the dashed vertical lines to the bivariate. Any ideas?
import matplotlib as mpl
import matplotlib.pyplot as plt
import random
import numpy as np
import seaborn as sns
import scipy
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.mlab import bivariate_normal
%matplotlib inline
# Data for random walk
def randomwalk():
mpl.rcParams['legend.fontsize'] = 10
xyz = []
cur = [0, 0]
for _ in range(40):
axis = random.randrange(0, 2)
cur[axis] += random.choice([-1, 1])
xyz.append(cur[:])
# Get density
x, y = zip(*xyz)
data = np.vstack([x,y])
kde = scipy.stats.gaussian_kde(data)
density = kde(data)
# Data for bivariate gaussian
a = np.linspace(-7.5, 7.5, 20)
b = a
X,Y = np.meshgrid(a, b)
Z = bivariate_normal(X, Y)
surprise_Z = -np.log(Z)
# Get random points from walker and plot up z-axis to the gaussian
M = data[:,np.random.choice(20,5)].T
# Plot figure
fig = plt.figure(figsize=(10, 7))
ax = fig.gca(projection='3d')
ax.plot(x, y, 'grey', label='Random walk') # Walker
ax.scatter(x[-1], y[-1], c='k', marker='o') # End point
ax.legend()
surf = ax.plot_surface(X, Y, surprise_Z, rstride=1, cstride=1,
cmap = plt.cm.gist_heat_r, alpha=0.1, linewidth=0.1)
#fig.colorbar(surf, shrink=0.5, aspect=7, cmap=plt.cm.gray_r)
for i in range(5):
ax.plot([M[i,0], M[i,0]],[M[i,1], M[i,1]], [0,10],'k--',alpha=0.8, linewidth=0.5)
ax.set_zlim(0, 50)
ax.set_xlim(-10, 10)
ax.set_ylim(-10, 10)
Final code,
import matplotlib as mpl
import matplotlib.pyplot as plt
import random
import numpy as np
import seaborn as sns
import scipy
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.mlab import bivariate_normal
%matplotlib inline
# Data for random walk
def randomwalk():
mpl.rcParams['legend.fontsize'] = 10
xyz = []
cur = [0, 0]
for _ in range(50):
axis = random.randrange(0, 2)
cur[axis] += random.choice([-1, 1])
xyz.append(cur[:])
# Get density
x, y = zip(*xyz)
data = np.vstack([x,y])
kde = scipy.stats.gaussian_kde(data)
density = kde(data)
# Data for bivariate gaussian
a = np.linspace(-7.5, 7.5, 100)
b = a
X,Y = np.meshgrid(a, b)
Z = bivariate_normal(X, Y)
surprise_Z = -np.log(Z)
# Get random points from walker and plot up z-axis to the gaussian
M = data[:,np.random.choice(50,10)].T
# Plot figure
fig = plt.figure(figsize=(10, 7))
ax = fig.gca(projection='3d')
ax.plot(x, y, 'grey', label='Random walk') # Walker
ax.legend()
surf = ax.plot_surface(X, Y, surprise_Z, rstride=1, cstride=1,
cmap = plt.cm.gist_heat_r, alpha=0.1, linewidth=0.1)
#fig.colorbar(surf, shrink=0.5, aspect=7, cmap=plt.cm.gray_r)
for i in range(10):
x = [M[i,0], M[i,0]]
y = [M[i,1], M[i,1]]
z = [0,-np.log(bivariate_normal(M[i,0],M[i,1]))]
ax.plot(x,y,z,'k--',alpha=0.8, linewidth=0.5)
ax.scatter(x, y, z, c='k', marker='o')
I would like to add a fourth dimension to the scatter plot by defining the ellipticity of the markers depending on a variable. Is that possible somehow ?
EDIT:
I would like to avoid a 3D-plot. In my opinion these plots are usually not very informative.
You can place Ellipse patches directly onto your axes, as demonstrated in this matplotlib example. To adapt it to use eccentricity as your "third dimension") keeping the marker area constant:
from pylab import figure, show, rand
from matplotlib.patches import Ellipse
import numpy as np
import matplotlib.pyplot as plt
N = 25
# ellipse centers
xy = np.random.rand(N, 2)*10
# ellipse eccentrities
eccs = np.random.rand(N) * 0.8 + 0.1
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal')
A = 0.1
for pos, e in zip(xy, eccs):
# semi-minor, semi-major axes, b and a:
b = np.sqrt(A/np.pi * np.sqrt(1-e**2))
a = A / np.pi / b
ellipse = Ellipse(xy=pos, width=2*a, height=2*b)
ax.add_artist(ellipse)
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
show()
Of course, you need to scale your marker area to your x-, y- values in this case.
You can use colorbar as the 4th dimension to your 3D plot. One example is as shown below:
import matplotlib.cm as cmx
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import matplotlib
import numpy as np
def scatter3d(x,y,z, cs, colorsMap='jet'):
cm = plt.get_cmap(colorsMap)
cNorm = matplotlib.colors.Normalize(vmin=min(cs), vmax=max(cs))
scalarMap = cmx.ScalarMappable(norm=cNorm, cmap=cm)
fig = plt.figure()
ax = Axes3D(fig)
ax.scatter(x, y, z, c=scalarMap.to_rgba(cs))
scalarMap.set_array(cs)
fig.colorbar(scalarMap,label='Test')
plt.show()
x = np.random.uniform(0,1,50)
y = np.random.uniform(0,1,50)
z = np.random.uniform(0,1,50)
so scatter3D(x,y,z,x+y) produces:
with x+y being the 4th dimension shown in color. You can add your calculated ellipticity depending on your specific variable instead of x+y to get what you want.
To change the ellipticity of the markers you will have to create them manually as such a feature is not implemented yet. However, I believe you can show 4 dimensions with a 2D scatter plot by using color and size as additional dimensions. You will have to take care of the scaling from data to marker size yourself. I added a simple function to handle that in the example below:
import matplotlib.pyplot as plt
import numpy as np
data = np.random.rand(60,4)
def scale_size(data, data_min=None, data_max=None, size_min=10, size_max=60):
# if the data limits are set to None we will just infer them from the data
if data_min is None:
data_min = data.min()
if data_max is None:
data_max = data.max()
size_range = size_max - size_min
data_range = data_max - data_min
return ((data - data_min) * size_range / data_range) + size_min
plt.scatter(data[:,0], data[:,1], c=data[:,2], s=scale_size(data[:,3]))
plt.colorbar()
plt.show()
Result: