Save specific part of matplotlib figure - python

I want to save only a specific part of a matplotlib figure by giving coordinates of a rectangle. The below code creates and saves the whole figure:
import numpy as np
import matplotlib.pyplot as plt
# Fixing random state for reproducibility
np.random.seed(19680801)
N = 50
x = np.random.rand(N)
y = np.random.rand(N)
colors = np.random.rand(N)
area = (30 * np.random.rand(N))**2
plt.scatter(x, y, s=area, c=colors, alpha=0.5)
plt.savefig('Plot.png', format='png')
I want to save only a specific part inside the plot determined by 4 points (in data coordinates), for example only the highlighted rectangular area:
Desired result: Save only the part highlighted in green

You can use the parameter bbox_inches= of savefig() to delimit the region to save. The problem is finding out the coordinates of the region in inches. For that, you have to use transforms:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.transforms import Bbox
# Fixing random state for reproducibility
np.random.seed(19680801)
N = 50
x = np.random.rand(N)
y = np.random.rand(N)
colors = np.random.rand(N)
area = (30 * np.random.rand(N))**2
fig, ax = plt.subplots()
ax.scatter(x, y, s=area, c=colors, alpha=0.5)
fig.canvas.draw() # force draw
x0,x1 = 0.2, 0.6
y0,y1 = 0.4, 0.8
bbox = Bbox([[x0,y0],[x1,y1]])
bbox = bbox.transformed(ax.transData).transformed(fig.dpi_scale_trans.inverted())
fig.savefig('test.png', bbox_inches=bbox)
test.png

Related

Pyplot: Drawing figure in a custom scale (both x and y)

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)

Matplotlib: Set cmap in plot_surface to x and y-axes

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.

Python/Matplotlib: 2d random walk with kde joint density contour in a 3d plot

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')

Python, Matplotlib: Drawing vertical lines in 3d plot, when data is independent

I have a random walker in the (x,y) plane and a -log(bivariate gaussian) in the (x,y,z) plane. These two datasets are essentially independent.
I want to sample, say 5 (x,y) pairs of the random walker and draw vertical lines up the z-axis and terminate the vertical line when it "meets" the bivariate gaussian.
This is my code so far:
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, 40)
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)
Which produces
As you can see the only thing I'm struggling with is how to terminate the vertical lines when they meet the appropriate Z-value. Any ideas are welcome!
You're currently only letting those lines get to a height of 10 by using [0,10] as the z coordinates. You can change your loop to the following:
for i in range(5):
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)
This takes the x and y coordinates for each point you loop over and calculates the height of overlying Gaussian for that point and plots to there. Here is a plot with the linestyle changed to emphasize the lines relevant to the question:

matplotlib scatterplot: adding 4th dimension by the marker shape

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:

Categories