I have a numpy array that contains some image data. I would like to plot the 'profile' of a transect drawn across the image. The simplest case is a profile running parallel to the edge of the image, so if the image array is imdat, then the profile at a selected point (r,c) is simply imdat[r] (horizontal) or imdat[:,c] (vertical).
Now, I want to take as input two points (r1,c1) and (r2,c2), both lying inside imdat. I would like to plot the profile of the values along the line connecting these two points.
What is the best way to get values from a numpy array, along such a line? More generally, along a path/polygon?
I have used slicing and indexing before, but I can't seem to arrive at an elegant solution for such a where consecutive slice elements are not in the same row or column. Thanks for your help.
#Sven's answer is the easy way, but it's rather inefficient for large arrays. If you're dealing with a relatively small array, you won't notice the difference, if you're wanting a profile from a large (e.g. >50 MB) you may want to try a couple of other approaches. You'll need to work in "pixel" coordinates for these, though, so there's an extra layer of complexity.
There are two more memory-efficient ways. 1) use scipy.ndimage.map_coordinates if you need bilinear or cubic interpolation. 2) if you just want nearest neighbor sampling, then just use indexing directly.
As an example of the first:
import numpy as np
import scipy.ndimage
import matplotlib.pyplot as plt
#-- Generate some data...
x, y = np.mgrid[-5:5:0.1, -5:5:0.1]
z = np.sqrt(x**2 + y**2) + np.sin(x**2 + y**2)
#-- Extract the line...
# Make a line with "num" points...
x0, y0 = 5, 4.5 # These are in _pixel_ coordinates!!
x1, y1 = 60, 75
num = 1000
x, y = np.linspace(x0, x1, num), np.linspace(y0, y1, num)
# Extract the values along the line, using cubic interpolation
zi = scipy.ndimage.map_coordinates(z, np.vstack((x,y)))
#-- Plot...
fig, axes = plt.subplots(nrows=2)
axes[0].imshow(z)
axes[0].plot([x0, x1], [y0, y1], 'ro-')
axes[0].axis('image')
axes[1].plot(zi)
plt.show()
The equivalent using nearest-neighbor interpolation would look something like this:
import numpy as np
import matplotlib.pyplot as plt
#-- Generate some data...
x, y = np.mgrid[-5:5:0.1, -5:5:0.1]
z = np.sqrt(x**2 + y**2) + np.sin(x**2 + y**2)
#-- Extract the line...
# Make a line with "num" points...
x0, y0 = 5, 4.5 # These are in _pixel_ coordinates!!
x1, y1 = 60, 75
num = 1000
x, y = np.linspace(x0, x1, num), np.linspace(y0, y1, num)
# Extract the values along the line
zi = z[x.astype(np.int), y.astype(np.int)]
#-- Plot...
fig, axes = plt.subplots(nrows=2)
axes[0].imshow(z)
axes[0].plot([x0, x1], [y0, y1], 'ro-')
axes[0].axis('image')
axes[1].plot(zi)
plt.show()
However, if you're using nearest-neighbor, you probably would only want samples at each pixel, so you'd probably do something more like this, instead...
import numpy as np
import matplotlib.pyplot as plt
#-- Generate some data...
x, y = np.mgrid[-5:5:0.1, -5:5:0.1]
z = np.sqrt(x**2 + y**2) + np.sin(x**2 + y**2)
#-- Extract the line...
# Make a line with "num" points...
x0, y0 = 5, 4.5 # These are in _pixel_ coordinates!!
x1, y1 = 60, 75
length = int(np.hypot(x1-x0, y1-y0))
x, y = np.linspace(x0, x1, length), np.linspace(y0, y1, length)
# Extract the values along the line
zi = z[x.astype(np.int), y.astype(np.int)]
#-- Plot...
fig, axes = plt.subplots(nrows=2)
axes[0].imshow(z)
axes[0].plot([x0, x1], [y0, y1], 'ro-')
axes[0].axis('image')
axes[1].plot(zi)
plt.show()
Probably the easiest way to do this is to use scipy.interpolate.interp2d():
# construct interpolation function
# (assuming your data is in the 2-d array "data")
x = numpy.arange(data.shape[1])
y = numpy.arange(data.shape[0])
f = scipy.interpolate.interp2d(x, y, data)
# extract values on line from r1, c1 to r2, c2
num_points = 100
xvalues = numpy.linspace(c1, c2, num_points)
yvalues = numpy.linspace(r1, r2, num_points)
zvalues = f(xvalues, yvalues)
I've been testing the above routines with galaxy images and think I found a small error. I think a transpose needs to be added to the otherwise great solution provided by Joe. Here is a slightly modified version of his code that reveals the error. If you run it without the transpose, you can see the profile doesn't match up; with the transpose it looks okay. This isn't apparent in Joe's solution since he uses a symmetric image.
import numpy as np
import scipy.ndimage
import matplotlib.pyplot as plt
import scipy.misc # ADDED THIS LINE
#-- Generate some data...
x, y = np.mgrid[-5:5:0.1, -5:5:0.1]
z = np.sqrt(x**2 + y**2) + np.sin(x**2 + y**2)
lena = scipy.misc.lena() # ADDED THIS ASYMMETRIC IMAGE
z = lena[320:420,330:430] # ADDED THIS ASYMMETRIC IMAGE
#-- Extract the line...
# Make a line with "num" points...
x0, y0 = 5, 4.5 # These are in _pixel_ coordinates!!
x1, y1 = 60, 75
num = 500
x, y = np.linspace(x0, x1, num), np.linspace(y0, y1, num)
# Extract the values along the line, using cubic interpolation
zi = scipy.ndimage.map_coordinates(z, np.vstack((x,y))) # THIS DOESN'T WORK CORRECTLY
zi = scipy.ndimage.map_coordinates(np.transpose(z), np.vstack((x,y))) # THIS SEEMS TO WORK CORRECTLY
#-- Plot...
fig, axes = plt.subplots(nrows=2)
axes[0].imshow(z)
axes[0].plot([x0, x1], [y0, y1], 'ro-')
axes[0].axis('image')
axes[1].plot(zi)
plt.show()
Here's the version WITHOUT the transpose. Notice that only a small fraction on the left should be bright according to the image but the plot shows almost half of the plot as bright.
Here's the version WITH the transpose. In this image, the plot seems to match well with what you'd expect from the red line in the image.
For a canned solution look into scikit-image's measure.profile_line function.
It's built on top of scipy.ndimage.map_coordinates as in #Joe's answer and has some extra useful functionality baked in.
Combining this answer with the Event Handling example on MPL's documentation, here's the code to allow for GUI-based dragging to draw/update your slice, by dragging on the plot data (this is coded for pcolormesh plots):
import numpy as np
import matplotlib.pyplot as plt
# Handle mouse clicks on the plot:
class LineSlice:
'''Allow user to drag a line on a pcolor/pcolormesh plot, and plot the Z values from that line on a separate axis.
Example
-------
fig, (ax1, ax2) = plt.subplots( nrows=2 ) # one figure, two axes
img = ax1.pcolormesh( x, y, Z ) # pcolormesh on the 1st axis
lntr = LineSlice( img, ax2 ) # Connect the handler, plot LineSlice onto 2nd axis
Arguments
---------
img: the pcolormesh plot to extract data from and that the User's clicks will be recorded for.
ax2: the axis on which to plot the data values from the dragged line.
'''
def __init__(self, img, ax):
'''
img: the pcolormesh instance to get data from/that user should click on
ax: the axis to plot the line slice on
'''
self.img = img
self.ax = ax
self.data = img.get_array().reshape(img._meshWidth, img._meshHeight)
# register the event handlers:
self.cidclick = img.figure.canvas.mpl_connect('button_press_event', self)
self.cidrelease = img.figure.canvas.mpl_connect('button_release_event', self)
self.markers, self.arrow = None, None # the lineslice indicators on the pcolormesh plot
self.line = None # the lineslice values plotted in a line
#end __init__
def __call__(self, event):
'''Matplotlib will run this function whenever the user triggers an event on our figure'''
if event.inaxes != self.img.axes: return # exit if clicks weren't within the `img` axes
if self.img.figure.canvas.manager.toolbar._active is not None: return # exit if pyplot toolbar (zooming etc.) is active
if event.name == 'button_press_event':
self.p1 = (event.xdata, event.ydata) # save 1st point
elif event.name == 'button_release_event':
self.p2 = (event.xdata, event.ydata) # save 2nd point
self.drawLineSlice() # draw the Line Slice position & data
#end __call__
def drawLineSlice( self ):
''' Draw the region along which the Line Slice will be extracted, onto the original self.img pcolormesh plot. Also update the self.axis plot to show the line slice data.'''
'''Uses code from these hints:
http://stackoverflow.com/questions/7878398/how-to-extract-an-arbitrary-line-of-values-from-a-numpy-array
http://stackoverflow.com/questions/34840366/matplotlib-pcolor-get-array-returns-flattened-array-how-to-get-2d-data-ba
'''
x0,y0 = self.p1[0], self.p1[1] # get user's selected coordinates
x1,y1 = self.p2[0], self.p2[1]
length = int( np.hypot(x1-x0, y1-y0) )
x, y = np.linspace(x0, x1, length), np.linspace(y0, y1, length)
# Extract the values along the line with nearest-neighbor pixel value:
# get temp. data from the pcolor plot
zi = self.data[x.astype(np.int), y.astype(np.int)]
# Extract the values along the line, using cubic interpolation:
#import scipy.ndimage
#zi = scipy.ndimage.map_coordinates(self.data, np.vstack((x,y)))
# if plots exist, delete them:
if self.markers != None:
if isinstance(self.markers, list):
self.markers[0].remove()
else:
self.markers.remove()
if self.arrow != None:
self.arrow.remove()
# plot the endpoints
self.markers = self.img.axes.plot([x0, x1], [y0, y1], 'wo')
# plot an arrow:
self.arrow = self.img.axes.annotate("",
xy=(x0, y0), # start point
xycoords='data',
xytext=(x1, y1), # end point
textcoords='data',
arrowprops=dict(
arrowstyle="<-",
connectionstyle="arc3",
color='white',
alpha=0.7,
linewidth=3
),
)
# plot the data along the line on provided `ax`:
if self.line != None:
self.line[0].remove() # delete the plot
self.line = self.ax.plot(zi)
#end drawLineSlice()
#end class LineTrace
# load the data:
D = np.genfromtxt(DataFilePath, ...)
fig, ax1, ax2 = plt.subplots(nrows=2, ncols=1)
# plot the data
img = ax1.pcolormesh( np.arange( len(D[0,:]) ), np.arange(len(D[:,0])), D )
# register the event handler:
LnTr = LineSlice(img, ax2) # args: the pcolor plot (img) & the axis to plot the values on (ax2)
This results in the following (after adding axis labels etc.), after dragging on the pcolor plot:
Here is a method without using scipy package(s). It should run much faster and is easy to understand. Basically, any pair of coordinates between point 1 (pt1) and point 2 (pt2) can be converted to x- and y- pixel integers, so we don't need any interpolation.
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
def euclideanDistance(coord1,coord2):
return np.sqrt((coord1[0]-coord2[0])**2+(coord1[1]-coord2[1])**2)
def getLinecut(image,X,Y,pt1,pt2):
row_col_1, row_col_2 = getRowCol(pt1,X,Y), getRowCol(pt2,X,Y)
row1,col1 = np.asarray(row_col_1).astype(float)
row2,col2 = np.asarray(row_col_2).astype(float)
dist = np.sqrt((pt1[0]-pt2[0])**2+(pt1[1]-pt2[1])**2)
N = int(euclideanDistance(row_col_1,row_col_2))#int(np.sqrt((row1-row2)**2+(col1-col2)**2))
rowList = [int(row1 + (row2-row1)/N*ind) for ind in range(N)]
colList = [int(col1 + (col2-col1)/N*ind) for ind in range(N)]
distList = [dist/N*ind for ind in range(N)]
return distList,image[rowList,colList]#rowList,colList
def getRowCol(pt,X,Y):
if X.min()<=pt[0]<=X.max() and Y.min()<=pt[1]<=Y.max():
pass
else:
raise ValueError('The input center is not within the given scope.')
center_coord_rowCol = (np.argmin(abs(Y-pt[1])),np.argmin(abs(X-pt[0])))
return center_coord_rowCol
image = np.asarray(Image.open('./Picture1.png'))[:,:,1]
image_copy = image.copy().astype(float)
X = np.linspace(-27,27,np.shape(image)[1])#[::-1]
Y = np.linspace(-15,15,np.shape(image)[0])[::-1]
pt1, pt2 = (-12,-14), (20,13)
distList, linecut = getLinecut(image_copy,X,Y,pt1,pt2)
plt.plot(distList, linecut)
plt.figure()
plt.pcolormesh(X,Y,image_copy)
plt.plot([pt1[0],pt2[0]],[pt1[1],pt2[1]],color='red')
plt.gca().set_aspect(1)
Picture1.png figure used:
See here for more details:
https://github.com/xuejianma/fastLinecut_radialLinecut
There is another function of the code: taking an average of several angle-evenly-spaced lines.
Related
the first results of my DEM plotting with matplotlib are working, but are RAM consuming and do not look very pretty.
Since Im just a beginner I dont know how to improve the result further, so it will look more like a terrain surface (in terms of elevations).
What I did so far:
gathering DEM geotiff data. Plotting it in 2d is simple and the result will look like that:
secondly i used that geotiff to squeez it into some plotting code samples i gathered online (especially from here: https://jackmckew.dev/3d-terrain-in-python.html )
As seen, the 3d-plot is on square basis and the surface is very "spiky". Id like to keep it in real proportions (geo-projections) and have it less spiky.
I already tried to lower the Z-ratio, but that does not change the end-result. The spiked are still there. I got no problems with smoothing the data and lose/change the data a bit.
The code so far:
from osgeo import gdal
import matplotlib.pyplot as plt
import numpy as np
source_file_dem = 'path_to_the_tif_file'
dem = gdal.Open(source_file_dem)
gt = dem.GetGeoTransform()
dem_array = dem.ReadAsArray()
lin_x = np.linspace(0,1,dem_array.shape[0],endpoint=False)
lin_y = np.linspace(0,1,dem_array.shape[1],endpoint=False)
y,x = np.meshgrid(lin_y,lin_x)
z = dem_array
# Creating figure
fig = plt.figure(figsize=(10,7))
ax = plt.axes(projection='3d')
surf = ax.plot_surface(x,y,z,cmap='terrain', edgecolor='none')
fig.colorbar(surf, ax=ax, shrink=0.5, aspect=5)
ax.set_title('Surface plot')
plt.xticks([]) # disabling xticks by Setting xticks to an empty list
plt.yticks([]) # disabling yticks by setting yticks to an empty list
# show plot
plt.show()
TIF files can be obtained here:
https://srtm.csi.cgiar.org/download
or you can just download a randomly picked tile in the US:
https://srtm.csi.cgiar.org/wp-content/uploads/files/srtm_5x5/TIFF/srtm_15_06.zip
For all who want to try that out:
the python gdal wheels can be found here:
https://www.lfd.uci.edu/~gohlke/pythonlibs/#gdal
Does anyone know where to look or could help out to reach a better looking result?
Thanks
UPDATE:
after some further research and hints from simon I got some results.
The current state result looks like that:
The code for the plot + an addition class for drawing arrows in 3D. Feel free to use it or even improve it. Id be glad to hear about improvements:
Plot:
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from osgeo import gdal
import matplotlib.pyplot as plt
import scipy as sp
import scipy.ndimage
from PIL import Image
from arrows3dplot import * # python_file in project with class
import matplotlib.cm as cm
source_file_dem = 'path_to_the_tif_file'
# Set max number of pixel to: 'None' to prevent errors. Its not nice, but works for that case. Big images will load RAM+CPU heavily (like DecompressionBomb)
Image.MAX_IMAGE_PIXELS = None # first we set no limit to open
img = Image.open(source_file_dem)
# get aspect ratio of tif file for late plot box-plot-ratio
y_ratio,x_ratio = img.size
# open georeference TIF file
dem = gdal.Open(source_file_dem)
gt = dem.GetGeoTransform()
dem_array = dem.ReadAsArray()
# create arrays and declare x,y,z variables
lin_x = np.linspace(0,1,dem_array.shape[0],endpoint=False)
lin_y = np.linspace(0,1,dem_array.shape[1],endpoint=False)
y,x = np.meshgrid(lin_y,lin_x)
z = dem_array
# Apply gaussian filter, with sigmas as variables. Higher sigma = more smoothing and more calculations. Downside: min and max values do change due to smoothing
sigma_y =100
sigma_x = 100
sigma = [sigma_y, sigma_x]
z_smoothed = sp.ndimage.gaussian_filter(z, sigma)
# Some min and max and range values coming from gaussian_filter calculations
z_smoothed_min = np.amin(z_smoothed)
z_smoothed_max = np.amax(z_smoothed)
z_range = z_smoothed_max - z_smoothed_min
# Creating figure
fig = plt.figure(figsize=(12,10))
ax = plt.axes(projection='3d')
ax.azim = -30
ax.elev = 42
ax.set_box_aspect((x_ratio,y_ratio,((x_ratio+y_ratio)/8)))
ax.arrow3D(1,1,z_smoothed_max, -1,0,1, mutation_scale=20, ec ='black', fc='red') #draw arrow to "north" which is not correct north. But with georeferenced sources it should work
surf = ax.plot_surface(x,y,z_smoothed, cmap='terrain', edgecolor='none')
# setting colors for colorbar range
m = cm.ScalarMappable(cmap=surf.cmap, norm=surf.norm)
m.set_array(z_smoothed)
cbar = fig.colorbar(m, shrink=0.5, aspect=20, ticks=[z_smoothed_min, 0, (z_range*0.25+z_smoothed_min), (z_range*0.5+z_smoothed_min), (z_range*0.75+z_smoothed_min), z_smoothed_max])
cbar.ax.set_yticklabels([f'{z_smoothed_min}', ' ', f'{(z_range*0.25+z_smoothed_min)}', f'{(z_range*0.5+z_smoothed_min)}', f'{(z_range*0.75+z_smoothed_min)}', f'{z_smoothed_max}'])
plt.xticks([]) # disabling xticks by Setting xticks to an empty list
plt.yticks([]) # disabling yticks by setting yticks to an empty list
# draw flat rectangle at z = 0 to indicate where mean sea level is in 3d
x_rectangle = [0,1,1,0]
y_rectangle = [0,0,1,1]
z_rectangle = [0,0,0,0]
verts = [list(zip(x_rectangle,y_rectangle,z_rectangle))]
ax.add_collection3d(Poly3DCollection(verts, alpha=0.5))
fig.tight_layout()
plt.show()
Class 3D Arrow (taken from here: https://gist.github.com/WetHat/1d6cd0f7309535311a539b42cccca89c )
import numpy as np
from matplotlib.patches import FancyArrowPatch
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.proj3d import proj_transform
class Arrow3D(FancyArrowPatch):
def __init__(self, x, y, z, dx, dy, dz, *args, **kwargs):
super().__init__((0, 0), (0, 0), *args, **kwargs)
self._xyz = (x, y, z)
self._dxdydz = (dx, dy, dz)
def draw(self, renderer):
x1, y1, z1 = self._xyz
dx, dy, dz = self._dxdydz
x2, y2, z2 = (x1 + dx, y1 + dy, z1 + dz)
xs, ys, zs = proj_transform((x1, x2), (y1, y2), (z1, z2), self.axes.M)
self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
super().draw(renderer)
def do_3d_projection(self, renderer=None):
x1, y1, z1 = self._xyz
dx, dy, dz = self._dxdydz
x2, y2, z2 = (x1 + dx, y1 + dy, z1 + dz)
xs, ys, zs = proj_transform((x1, x2), (y1, y2), (z1, z2), self.axes.M)
self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
return np.min(zs)
def _arrow3D(ax, x, y, z, dx, dy, dz, *args, **kwargs):
'''Add an 3d arrow to an `Axes3D` instance.'''
arrow = Arrow3D(x, y, z, dx, dy, dz, *args, **kwargs)
ax.add_artist(arrow)
setattr(Axes3D, 'arrow3D', _arrow3D)
If you make a 2D numpy array from the data, you can apply a convolution to it. Look into OpenCV; it has functions for blurring and such.
I want to create and save a number of sequential plots so I can then make an mp4 movie out of those plots. I want the color of the plot to depend on z (the value of the third axis):
The code I am using:
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.ticker import LinearLocator
import numpy as np
file_dir1 = r"C:\Users\files\final_files\B_6_sec\_read.csv"
specs23 = pd.read_csv(file_dir1, sep=',')
choose_file = specs23 # Choose file betwenn specs21, specs22,...
quant = 0 # Choose between 0,1,...,according to the following list
column = ['$\rho$', '$V_{x}$', '$V_{y}$', '$V_{z}$','$B_{x}$', '$B_{y}$','$B_{z}$','$Temperature$']
choose_column = choose_file[column[quant]]
resolution = 1024 # Specify resolution of grid
t_steps = int(len(specs23)/resolution) # Specify number of timesteps
fig, ax = plt.subplots(subplot_kw={"projection": "3d"},figsize=(15,10))
# Make data.
X = np.arange(0, resolution, 1)
Y = np.arange(0, int(len(specs23)/resolution),1)
X, Y = np.meshgrid(X, Y)
Z = choose_file[column[quant]].values
new_z = np.zeros((t_steps,resolution)) # Selected quantity as a function of x,t
### Plot figure ###
for i in range(0,int(len(choose_file)/resolution)):
zs = choose_column[i*resolution:resolution*(i+1)].values
new_z[i] = zs
for i in range(len(X)):
ax.plot(X[i], Y[i], new_z[i]) #%// color binded to "z" values
ax.zaxis.set_major_locator(LinearLocator(10))
# A StrMethodFormatter is used automatically
ax.zaxis.set_major_formatter('{x:.02f}')
plt.show()
What I am getting looks like this:
I would like to look it like this:
I have created the second plot using the LineCollection module. The problem is that it prints all the lines at once not allowing me to save each separately to create a movie.
You can find the dataframe I am using to create the figure here:
https://www.dropbox.com/s/idbeuhyxqfy9xvw/_read.csv?dl=0
The poster wants two things
lines with colors depending on z-values
animation of the lines over time
In order to achieve(1) one needs to cut up each line in separate segments and assign a color to each segment; in order to obtain a colorbar, we need to create a scalarmappable object that knows about the outer limits of the colors.
For achieving 2, one needs to either (a) save each frame of the animation and combine it after storing all the frames, or (b) leverage the animation module in matplotlib. I have used the latter in the example below and achieved the following:
from mpl_toolkits.mplot3d import axes3d
import matplotlib.pyplot as plt, numpy as np
from mpl_toolkits.mplot3d.art3d import Line3DCollection
fig, ax = plt.subplots(subplot_kw = dict(projection = '3d'))
# generate data
x = np.linspace(-5, 5, 500)
y = np.linspace(-5, 5, 500)
z = np.exp(-(x - 2)**2)
# uggly
segs = np.array([[(x1,y2), (x2, y2), (z1, z2)] for x1, x2, y1, y2, z1, z2 in zip(x[:-1], x[1:], y[:-1], y[1:], z[:-1], z[1:])])
segs = np.moveaxis(segs, 1, 2)
# setup segments
# get bounds
bounds_min = segs.reshape(-1, 3).min(0)
bounds_max = segs.reshape(-1, 3).max(0)
# setup colorbar stuff
# get bounds of colors
norm = plt.cm.colors.Normalize(bounds_min[2], bounds_max[2])
cmap = plt.cm.plasma
# setup scalar mappable for colorbar
sm = plt.cm.ScalarMappable(norm, plt.cm.plasma)
# get average of segment
avg = segs.mean(1)[..., -1]
# get colors
colors = cmap(norm(avg))
# generate colors
lc = Line3DCollection(segs, norm = norm, cmap = cmap, colors = colors)
ax.add_collection(lc)
def update(idx):
segs[..., -1] = np.roll(segs[..., -1], idx)
lc.set_offsets(segs)
return lc
ax.set_xlim(bounds_min[0], bounds_max[0])
ax.set_ylim(bounds_min[1], bounds_max[1])
ax.set_zlim(bounds_min[2], bounds_max[2])
fig.colorbar(sm)
from matplotlib import animation
frames = np.linspace(0, 30, 10, 0).astype(int)
ani = animation.FuncAnimation(fig, update, frames = frames)
ani.save("./test_roll.gif", savefig_kwargs = dict(transparent = False))
fig.show()
I want to draw the graph like the picture below. Its x-axis is the order of the data points, e.g. from 1 to 7. The y-axis is the scale from 0 to 25. If I want to draw a triangle, for example, with its data (1,22,20), then '1' gives the order among all data points(different triangles), the triangle should be drew in most left; "22,20" gives the "bottom-tip" of the triangle along the y-axis.
Does anyone know how to draw such triangle with multiple number in a graph using matplotlib python package?
Read this post and this post about drawing polygons with matplotlib.
EDIT1: Just saw #Poolka's answer. This was also my way to go, but notice that in one of the above links, it is stated, that adding single polygons (p = pat.Polygon([[x1, y1], [x2, y2], [x3, y3]); ax.add_patch(p)) to the figure can become very slow, and therefore collections are preferred.
EDIT 2: Also see TheImportanceOfBeingErnest's answer for a more elaborated version of this concept.
Together with this snippet of code, it should get you going:
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as pat # Patches like pat.Polygon()
from matplotlib.collections import PolyCollection # Collections of patches
test = ((1, 22, 20),
(2, 21, 19.5),
(3, 18, 20)) # Test data
triangles = []
fig, ax = plt.subplots(1, 1)
for t in test:
xmid = t[0] # Middle x-coord
xleft = t[0] - 0.5
xright = t[0] + 0.5 # Use fixed width of 0.5
y1 = t[1] # y-coords
y2 = t[2]
coordinates = [[xleft, y1], [xright, y1], [xmid, y2]]
print(coordinates)
triangles.append(coordinates) # Append to collection
z = np.random.random(len(triangles))
collec = PolyCollection(triangles, array=z, cmap=matplotlib.cm.viridis)
ax.add_collection(collec) # Plot polygon collection
ax.autoscale_view()
plt.show()
Consider the following simple example:
import matplotlib.pyplot as plt
# data
data = [[1, 22, 20], [3, 20, 25]]
plt.figure()
for val in data:
# coordinates
dy = val[1] - val[2]
dx = abs(dy) / 2
x0 = val[0]
y0 = val[1]
# drawing
triangle = plt.Polygon([[x0, y0], [x0 - dx, y0 + dy], [x0 + dx, y0 + dy]])
plt.gca().add_patch(triangle)
# misc
plt.grid()
plt.axis('square')
# these 2 lines are needed because patches in matplotlib do not adjust axes limits automatically, another approach is to add some data to the figure with plot, scatter, etc.
plt.xlim([-20, 20])
plt.ylim([0, 40])
Result is:
Using a PolyCollection (as shown in #cripcate's answer) is advantageous in this case. A more condensed version using a single numpy array could look like this:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import PolyCollection
def triangle_collection(d, ax=None, width=0.4, **kwargs):
ax = ax or plt.gca()
verts = np.c_[d[:,0]-width/2, d[:,1], d[:,0]+width/2,
d[:,1], d[:,0], d[:,2]].reshape(len(d),3,2)
c = PolyCollection(verts, **kwargs)
ax.add_collection(c)
ax.autoscale()
return c
data = np.array([(1,22,20), (2,21,19.5), (3,18,20),
(4,17,19), (5,15,17), (6,11,8.5), (7,14,12)])
fig, ax = plt.subplots()
fig.subplots_adjust(left=0.3, right=0.7)
triangle_collection(data, facecolors=plt.cm.tab10(np.arange(len(data))))
plt.show()
So I am currently plotting a scatter graph with many x and ys in matplotlib:
plt.scatter(x, y)
I want to draw a line on this scatter graph that crosses through the whole graph (i.e hits two 'borders') I know the gradient and the intercept - m and the c in the equation y = mx +c.
I have thought about acquiring the 4 points of the plot (calculating the min and max scatter x and ys) and from that calculating the min and max coords for the line and then plotting but that seems very convoluted. Is there any better way to do this bearing in mind the line may not even be 'within' the 'plot'?
Example of scatter graph:
as identified visually in the plot the four bordering coordinates are ruffly:
bottom left: -1,-2
top left: -1,2
bottom right: 6,-2
top right 6,2
I now have a line that I need to plot that must not exceed these boundaries but if it enters the plot must touch two of the boundary points.
So I could check what y equals when x = -1 and then check if that value is between -1 and 6 and if it is the line must cross the left border, so plot it, and so on and so fourth.
Ideally though I would create a line from -infinity to infinity and then crop it to fit the plot.
The idea here is to draw a line of some equation y=m*x+y0 into the plot. This can be achieved by transforming a horizontal line, originally given in axes coordinates, into data coordinates, applying the Affine2D transform according to the line equation and transforming back to screen coordinates.
The advantage here is that you do not need to know the axes limits at all. You may also freely zoom or pan your plot; the line will always stay within the axes boundaries. It hence effectively implements a line ranging from -infinity to + inifinty.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
def axaline(m,y0, ax=None, **kwargs):
if not ax:
ax = plt.gca()
tr = mtransforms.BboxTransformTo(
mtransforms.TransformedBbox(ax.viewLim, ax.transScale)) + \
ax.transScale.inverted()
aff = mtransforms.Affine2D.from_values(1,m,0,0,0,y0)
trinv = ax.transData
line = plt.Line2D([0,1],[0,0],transform=tr+aff+trinv, **kwargs)
ax.add_line(line)
x = np.random.rand(20)*6-0.7
y = (np.random.rand(20)-.5)*4
c = (x > 3).astype(int)
fig, ax = plt.subplots()
ax.scatter(x,y, c=c, cmap="bwr")
# draw y=m*x+y0 into the plot
m = 0.4; y0 = -1
axaline(m,y0, ax=ax, color="limegreen", linewidth=5)
plt.show()
While this solution looks a bit complicated on first sight, one does not need to fully understand it. Just copy the axaline function to your code and use it as it is.
In order to get the automatic updating working without the transforms doing this, one may add callbacks which would reset the transform every time something changes in the plot.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import transforms
class axaline():
def __init__(self, m,y0, ax=None, **kwargs):
if not ax: ax = plt.gca()
self.ax = ax
self.aff = transforms.Affine2D.from_values(1,m,0,0,0,y0)
self.line = plt.Line2D([0,1],[0,0], **kwargs)
self.update()
self.ax.add_line(self.line)
self.ax.callbacks.connect('xlim_changed', self.update)
self.ax.callbacks.connect('ylim_changed', self.update)
def update(self, evt=None):
tr = ax.transAxes - ax.transData
trinv = ax.transData
self.line.set_transform(tr+self.aff+trinv)
x = np.random.rand(20)*6-0.7
y = (np.random.rand(20)-.5)*4
c = (x > 3).astype(int)
fig, ax = plt.subplots()
ax.scatter(x,y, c=c, cmap="bwr")
# draw y=m*x+y0 into the plot
m = 0.4; y0 = -1
al = axaline(m,y0, ax=ax, color="limegreen", linewidth=5)
plt.show()
You may try:
import matplotlib.pyplot as plt
import numpy as np
m=3
c=-2
x1Data= np.random.normal(scale=2, loc=.4, size=25)
y1Data= np.random.normal(scale=3, loc=1.2, size=25)
x2Data= np.random.normal(scale=1, loc=3.4, size=25)
y2Data= np.random.normal(scale=.65, loc=-.2, size=25)
fig = plt.figure()
ax = fig.add_subplot( 1, 1, 1 )
ax.scatter(x1Data, y1Data)
ax.scatter(x2Data, y2Data)
ylim = ax.get_ylim()
xlim = ax.get_xlim()
ax.plot( xlim, [ m * x + c for x in xlim ], 'r:' )
ax.set_ylim( ylim )
ax.set_xlim( xlim )
plt.show()
which gives:
It's possible to fill between lines with a color:
http://matplotlib.sourceforge.net/examples/pylab_examples/fill_between_demo.html
It's also possible to use a continuous colormap for a line:
http://matplotlib.sourceforge.net/examples/pylab_examples/multicolored_line.html
Is it possible (and reasonably easy) to use a continuous colormap for the colored fill between two lines? For example, the color fill may change along x based on the difference between the two lines at x (or based on another set of data).
I found a solution to this problem. It builds on the brilliant but hacky solution of #Hooked. You create a 2D grid filed from lots of small boxes. It's not the fastest solution but it should be pretty flexible (more so than solutions which apply imshow to the patches).
import numpy as np
import pylab as plt
#Plot a rectangle
def rect(ax, x, y, w, h, c,**kwargs):
#Varying only in x
if len(c.shape) is 1:
rect = plt.Rectangle((x, y), w, h, color=c, ec=c,**kwargs)
ax.add_patch(rect)
#Varying in x and y
else:
#Split into a number of bins
N = c.shape[0]
hb = h/float(N); yl = y
for i in range(N):
yl += hb
rect = plt.Rectangle((x, yl), w, hb,
color=c[i,:], ec=c[i,:],**kwargs)
ax.add_patch(rect)
#Fill a contour between two lines
def rainbow_fill_between(ax, X, Y1, Y2, colors=None,
cmap=plt.get_cmap("Reds"),**kwargs):
plt.plot(X,Y1,lw=0) # Plot so the axes scale correctly
dx = X[1]-X[0]
N = X.size
#Pad a float or int to same size as x
if (type(Y2) is float or type(Y2) is int):
Y2 = np.array([Y2]*N)
#No colors -- specify linear
if colors is None:
colors = []
for n in range(N):
colors.append(cmap(n/float(N)))
#Varying only in x
elif len(colors.shape) is 1:
colors = cmap((colors-colors.min())
/(colors.max()-colors.min()))
#Varying only in x and y
else:
cnp = np.array(colors)
colors = np.empty([colors.shape[0],colors.shape[1],4])
for i in range(colors.shape[0]):
for j in range(colors.shape[1]):
colors[i,j,:] = cmap((cnp[i,j]-cnp[:,:].min())
/(cnp[:,:].max()-cnp[:,:].min()))
colors = np.array(colors)
#Create the patch objects
for (color,x,y1,y2) in zip(colors,X,Y1,Y2):
rect(ax,x,y2,dx,y1-y2,color,**kwargs)
# Some Test data
X = np.linspace(0,10,100)
Y1 = .25*X**2 - X
Y2 = X
g = np.exp(-.3*(X-5)**2)
#Plot fill and curves changing in x only
fig, axs =plt.subplots(1,2)
colors = g
rainbow_fill_between(axs[0],X,Y1,Y2,colors=colors)
axs[0].plot(X,Y1,'k-',lw=4)
axs[0].plot(X,Y2,'k-',lw=4)
#Plot fill and curves changing in x and y
colors = np.outer(g,g)
rainbow_fill_between(axs[1],X,Y1,Y2,colors=colors)
axs[1].plot(X,Y1,'k-',lw=4)
axs[1].plot(X,Y2,'k-',lw=4)
plt.show()
The result is,
Your solution is great and flexible ! In particular the 2D case is really nice. Such a feature could be added to fill_between maybe if the colors kwargs of the function would accept an array of the same length of x and y ?
Here is a simpler case for the 1D case using the fill_between function. It does the same but as it use trapezes instead of rectangle the result is smoother.
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import norm
# Select a color map
cmap = mpl.cm.bwr
# Some Test data
npts = 100
x = np.linspace(-4, 4, npts)
y = norm.pdf(x)
z = np.sin(2 * x)
normalize = mpl.colors.Normalize(vmin=z.min(), vmax=z.max())
# The plot
fig = plt.figure()
ax = fig.add_axes([0.12, 0.12, 0.68, 0.78])
plt.plot(x, y, color="gray")
for i in range(npts - 1):
plt.fill_between([x[i], x[i+1]], [y[i], y[i+1]], color=cmap(normalize(z[i])))
cbax = fig.add_axes([0.85, 0.12, 0.05, 0.78])
cb = mpl.colorbar.ColorbarBase(cbax, cmap=cmap, norm=normalize, orientation='vertical')
cb.set_label("Sin function", rotation=270, labelpad=15)
plt.show()