3D PCA in matplotlib: how to add legend? - python

I am attempting to use http://scikit-learn.org/stable/auto_examples/decomposition/plot_pca_iris.html for my own data to construct a 3D PCA plot. The tutorial, however, did not specify how I can add a legend. Another page, https://matplotlib.org/users/legend_guide.html did, but I cannot see how I can apply the information in the second tutorial to the first.
How can I modify the code below to add a legend?
# Code source: Gae"l Varoquaux
# License: BSD 3 clause
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from sklearn import decomposition
from sklearn import datasets
np.random.seed(5)
centers = [[1, 1], [-1, -1], [1, -1]]
iris = datasets.load_iris()
X = iris.data#the floating point values
y = iris.target#unsigned integers specifying group
fig = plt.figure(1, figsize=(4, 3))
plt.clf()
ax = Axes3D(fig, rect=[0, 0, .95, 1], elev=48, azim=134)
plt.cla()
pca = decomposition.PCA(n_components=3)
pca.fit(X)
X = pca.transform(X)
for name, label in [('Setosa', 0), ('Versicolour', 1), ('Virginica', 2)]:
ax.text3D(X[y == label, 0].mean(),
X[y == label, 1].mean() + 1.5,
X[y == label, 2].mean(), name,
horizontalalignment='center',
bbox=dict(alpha=.5, edgecolor='w', facecolor='w'))
# Reorder the labels to have colors matching the cluster results
y = np.choose(y, [1, 2, 0]).astype(np.float)
ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=y, cmap=plt.cm.spectral,
edgecolor='k')
ax.w_xaxis.set_ticklabels([])
ax.w_yaxis.set_ticklabels([])
ax.w_zaxis.set_ticklabels([])
plt.show()

There are some issues with the other answer on which neither the OP, nor the answerer seem to be clear about; this is hence not a complete answer, but rather an appendix to the existing answer.
The spectral colormap has been removed from matplotlib in version 2.2,
use Spectral or nipy_spectral or any other valid colormap.
Any colormap in matplotlib ranges from 0 to 1. If you call it with any value outside that range,
it will just give your the outmost color. To get a color from a colormap you hence need to normalize the values.
This is done via a Normalize instance. In this case this is internal to scatter.
Hence use sc = ax.scatter(...) and then sc.cmap(sc.norm(value)) to get a value according to the same mapping that is used within the scatter.
Therefore the code should rather use
[sc.cmap(sc.norm(i)) for i in [1, 2, 0]]
The legend is outside the figure. The figure is 4 x 3 inches in size (figsize=(4, 3)).
The axes takes 95% of that space in width (rect=[0, 0, .95, 1]).
The call to legend places the legend's right center point at 1.7 times the axes width = 4*0.95*1.7 = 6.46 inches. (bbox_to_anchor=(1.7,0.5)).
Alternative suggestion from my side: Make the figure larger (figsize=(5.5, 3)), such that the legend will fit in, make the axes take only 70% of the figure width, such that you have 30% left for the legend. Position the legend's left side close to the axes boundary (bbox_to_anchor=(1.0, .5)).
For more on this topic see How to put the legend out of the plot.
The reason you still see the complete figure including the legend in a jupyter notebook is that jupyter will just save everything inside the canvas, even if it overlaps and thereby enlarge the figure.
In total the code may then look like
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np; np.random.seed(5)
from sklearn import decomposition, datasets
centers = [[1, 1], [-1, -1], [1, -1]]
iris = datasets.load_iris()
X = iris.data #the floating point values
y = iris.target #unsigned integers specifying group
fig = plt.figure(figsize=(5.5, 3))
ax = Axes3D(fig, rect=[0, 0, .7, 1], elev=48, azim=134)
pca = decomposition.PCA(n_components=3)
pca.fit(X)
X = pca.transform(X)
labelTups = [('Setosa', 0), ('Versicolour', 1), ('Virginica', 2)]
for name, label in labelTups:
ax.text3D(X[y == label, 0].mean(),
X[y == label, 1].mean() + 1.5,
X[y == label, 2].mean(), name,
horizontalalignment='center',
bbox=dict(alpha=.5, edgecolor='w', facecolor='w'))
# Reorder the labels to have colors matching the cluster results
y = np.choose(y, [1, 2, 0]).astype(np.float)
sc = ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=y, cmap="Spectral", edgecolor='k')
ax.w_xaxis.set_ticklabels([])
ax.w_yaxis.set_ticklabels([])
ax.w_zaxis.set_ticklabels([])
colors = [sc.cmap(sc.norm(i)) for i in [1, 2, 0]]
custom_lines = [plt.Line2D([],[], ls="", marker='.',
mec='k', mfc=c, mew=.1, ms=20) for c in colors]
ax.legend(custom_lines, [lt[0] for lt in labelTups],
loc='center left', bbox_to_anchor=(1.0, .5))
plt.show()
and produce

Needed a few tweaks (plt.cm.spectral is the danged weirdest colormap I've ever dealt with), but it seems to be good now:
from matplotlib.lines import Line2D
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
from sklearn import decomposition
from sklearn import datasets
np.random.seed(5)
centers = [[1, 1], [-1, -1], [1, -1]]
iris = datasets.load_iris()
X = iris.data#the floating point values
y = iris.target#unsigned integers specifying group
fig = plt.figure(1, figsize=(4, 3))
plt.clf()
ax = Axes3D(fig, rect=[0, 0, .95, 1], elev=48, azim=134)
plt.cla()
pca = decomposition.PCA(n_components=3)
pca.fit(X)
X = pca.transform(X)
labelTups = [('Setosa', 0), ('Versicolour', 1), ('Virginica', 2)]
for name, label in labelTups:
ax.text3D(X[y == label, 0].mean(),
X[y == label, 1].mean() + 1.5,
X[y == label, 2].mean(), name,
horizontalalignment='center',
bbox=dict(alpha=.5, edgecolor='w', facecolor='w'))
# Reorder the labels to have colors matching the cluster results
y = np.choose(y, [1, 2, 0]).astype(np.float)
ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=y, cmap=plt.cm.spectral, edgecolor='k')
ax.w_xaxis.set_ticklabels([])
ax.w_yaxis.set_ticklabels([])
ax.w_zaxis.set_ticklabels([])
colors = [plt.cm.spectral(np.float(i/2)) for i in [1, 2, 0]]
custom_lines = [Line2D([0], [0], linestyle="none", marker='.', markeredgecolor='k', markerfacecolor=c, markeredgewidth=.1, markersize=20) for c in colors]
ax.legend(custom_lines, [lt[0] for lt in labelTups], loc='right', bbox_to_anchor=(1.7, .5))
plt.show()
Here's a link to an online Jupyter notebook with a live version of the script (requires an account for rerunning, though).
Short explanation
You're trying to add three legend markers for a single plot, which is nonstandard behavior. Thus, you need to manually create the shapes that your legend will display.
Longer explanation
This line of code recreates the colors you used in your plot:
colors = [plt.cm.spectral(np.float(i/2)) for i in [1, 2, 0]]
and then this line of code draws some appropriate-looking dots that we'll eventually display on your legend:
custom_lines = [Line2D([0], [0], linestyle="none", marker='.', markeredgecolor='k', markerfacecolor=c, markeredgewidth=.1, markersize=20) for c in colors]
The first two args are just the (internal) x and y coords of the single dot that will be drawn, linestyle="none" suppresses the line that Line2D would normally draw by default, and the rest of the args create and style the dot itself (referred to as a marker in the terminology of the matplotlib api).
Finally, this statement actually creates the legend:
ax.legend(custom_lines, [lt[0] for lt in labelTups], loc='right', bbox_to_anchor=(1.7, .5))
The first arg is of course a list of the dots we just drew, and the second arg is a list of the labels (one per dot). The remaining two args tell matplotlib where to draw the actual box containing the legend. The last arg, bbox_to_anchor, is basically a way to manually fiddle with the positioning of the legend, which I had to do since matplotlib support for 3D anything is still a little behind the curve. On 2D plots you typically don't need it, and, since matplotlib usually does a decent job of automatically positioning the legend on 2D plots in the first place, you often don't even need the loc arg either.
Some colormap weirdness
Don't quite know what was going on with plt.cm.spectral, but in order to get it to behave, for every value I fed it I had to:
a) first cast the value to float
b) then divide the value by 2
a) does occur explicitly in the OP's original code, right before they plot. The divide by 2 thing, I don't know where that comes from. Somehow the call to ax.scatter is implicitly normalizing all of the y values so that the maximum is 1? I guess?

Related

matplotlib.pyplot.scatter - define sizes of entries in legend for size of marker

How to change the marker size and the respective label in the legend to meaningful values like [20,40,60,80] ?
Do I need to derive handles and labels from an additional dummy dataset and how to plot it, so that it will not be visible (alpha=0.0 will not work?)?
import matplotlib.pyplot as plt
import numpy as np
x = [1,2,3,4,5]
y = [1,2,3,4,5]
size = np.asarray([0.84,0.53,0.24,0.47,0.18]) * 100
s1 = plt.scatter(x, y, s=size)
handles, labels = s1.legend_elements(prop="sizes")
legend2 = plt.legend(handles, labels, frameon=False, title="Sizes")
plt.show()
The function legend_elements(...) has a parameter num= which can be a Locator. So, you can try e.g. a MultipleLocator:
import matplotlib.pyplot as plt
from matplotlib.ticker import MultipleLocator
import numpy as np
x = [1, 2, 3, 4, 5]
y = [1, 2, 3, 4, 5]
size = np.asarray([0.84, 0.53, 0.24, 0.47, 0.18]) * 100
s1 = plt.scatter(x, y, s=size)
handles, labels = s1.legend_elements(prop="sizes", num=MultipleLocator(20))
legend2 = plt.legend(handles, labels, frameon=False, title="Sizes")
plt.show()
PS: In this case, you can also just put a number for num=, e.g. s1.legend_elements(prop="sizes", num=4). That also seems to put rounded values. When only a few different size values are used in the plot, the default num='auto', uses these values instead of rounded values.

What's the meaning of clip_box in Matplotlib Artist?

I am testing the clip_box feature of Artist using the code snippet below:
import matplotlib.pyplot as plt
from matplotlib.transforms import Bbox
import numpy as np
fig = plt.figure()
ax = fig.subplots(1, 2)
x = [1, 2, 3, 4]
y = [3, 8, 5, 2]
line_a, = ax[0].plot(x, y, color='red', linewidth=3.0)
line_b, = ax[1].plot(x, y, color='red', linewidth=3.0)
boundingbox = Bbox(np.array([[0, 0], [3, 9]]))
line_b.set_clip_box(boundingbox)
line_b.set_clip_on(True)
plt.show()
What I expect is the last part of line_b will be cut out by the clip box, and line_b will be a bit shorter than line_a.
It turns out that there's nothing left on the second subplot. It's totally empty. Is my understanding of the clip_box wrong or are there some issues in the code snippet?
The "natural" clip box for the right hand side plot is ax[1].bbox. Finding its extent tells us what units should be used to specify the clip box Bbox.
Since we don't add the Bbox instance to any axes when we create, it could only be relative to the figure. When we print ax[1].bbox, we can see that its size is to be specified in pixels.
It's indeed much simpler to use a Rectangle or Polygon to specify the clip box because they can be added to axes. Using 'none' color for its facecolor could be more convenient because it's figure style-independent.
import matplotlib.pyplot as plt
from matplotlib.transforms import Bbox
fig = plt.figure(dpi=89)
ax = fig.subplots(1, 2)
x = [1, 2, 3, 4]
y = [3, 8, 5, 2]
line_a, = ax[0].plot(x, y, color='red', linewidth=3.0)
line_b, = ax[1].plot(x, y, color='red', linewidth=3.0)
print(ax[1].bbox, '\n', ax[1].bbox.extents)
# the line above prints
# TransformedBbox(
# Bbox(x0=0.5477272727272726, y0=0.10999999999999999, x1=0.8999999999999999, y1=0.88),
# BboxTransformTo(
# TransformedBbox(
# Bbox(x0=0.0, y0=0.0, x1=6.393258426966292, y1=4.797752808988764),
# Affine2D().scale(178.0))))
# [ 623.31363636 93.94 1024.2 751.52 ]
# 178.0 is 2 * dpi, I believe the doubling happens because of what screen I have got
boundingbox = Bbox.from_extents([623.31363636, 93.94, 900.2, 751.52])
print(boundingbox, '\n', boundingbox.extents)
# the line above prints
# Bbox(x0=623.31363636, y0=93.94, x1=900.2, y1=751.52)
# [623.31363636 93.94 900.2 751.52 ]
line_b.set_clip_box(boundingbox)
line_b.set_clip_on(True)
plt.show()
I've spent some time reading about Bboxes in Matplotlib and they are pretty complicated. The set_clip_box method you refer to has not got very helpful documentation, and the examples of its use both use the bbox of an Axes, which is a nested transformation; ie _, ax = plt.subplots(); ax.bbox is a TransformedBbox based on a linear transform of another TransformedBbox based on an Affine2D transform of a plain Bbox! (All of this explained in more detail here.)
It seems that these involve transformations between different sets of co-ordinates; in the case of a regular Axes it is between x- and y-values, pixels, and the specific adaptations to screen size. I would be happy to hear from someone who knows more about Bboxes why your Bbox acts the way it does. But what you want to achieve can be done much more easily, using a FancyBboxPatch (a Rectangle patch would work just as well):
from matplotlib.patches import FancyBboxPatch
f, ax = plt.subplots(1, 2)
x = [1, 2, 3, 4]
y = [3, 8, 5, 2]
line_a, = ax[0].plot(x, y, color='red', linewidth=3.0)
line_b, = ax[1].plot(x, y, color='red', linewidth=3.0)
bb = Bbox([[0, 0], [3, 9]])
ax[1].add_patch(FancyBboxPatch((bb.xmin, bb.ymin), bb.width, bb.height, boxstyle="square",
ec='white', fc='white', zorder=2.1))
(ec and fc are edge colour and fill colour; zorder determines the artist order. Lines are 2, so we just need out Bbox patch to be slightly higher.)

Phase plot using matplotlib tricontourf

I want to plot an image of the results of a finite element simulation with a personalized colormap.
I have been trying to use tricontourf to plot it as follow :
#Z = self.phi.compute_vertex_values(self.mesh)
Z = np.mod(self.phi.compute_vertex_values(self.mesh),2*np.pi)
triang = tri.Triangulation(*self.mesh.coordinates().reshape((-1, 2)).T,
triangles=self.mesh.cells())
zMax = np.max(Z)
print(zMax)
#Colormap creation
nColors = np.max(Z)*200/(2*np.pi)
phiRange = np.linspace(0,zMax,nColors)
intensity = np.sin(phiRange)**2
intensityArray = np.array([intensity, intensity, intensity])
colors = tuple(map(tuple, intensityArray.T))
self.cm = LinearSegmentedColormap.from_list("BAM", colors, N=nColors)
#Figure creation
fig, ax = plt.subplots()
levels2 = np.linspace(0., zMax,nColors)
cax = ax.tricontourf(triang, Z,levels=levels2, cmap = self.cm) #plot of the solution
fig.colorbar(cax)
ax.triplot(triang, lw=0.5, color='yellow') #plot of the mesh
plt.savefig("yolo.png")
plt.close(fig)
And it gives the result :
As you can see there are some trouble where the phase goes from 2pi to 0 that comes from tricontourf when there is a modulo...
My first idea for work around was to work directly on my phase Z. The problem is that if I do this I need to create a much larger colormap. Ultimately, the phase will be very large and so will be the colormap if I want a correct color resolution... Furthemore I would like to have only one period in the colormap on the right (just like in the first figure).
Any idea how I could obtain a figure just like the second one, with a colormap just like the one from the first figure and without creating a very large and expensive colormap ?
EDIT : I have written a small code that is runnable out of the box : It reproduces the problem I have and I have also tried to apply Thomas Kuhn answer to my preoblem. However, it seems that there are some problem with the colorbar... Any idea how I could fix this ?
import matplotlib.pyplot as plt
import matplotlib.tri as mtri
import numpy as np
import matplotlib.colors as colors
class PeriodicNormalize(colors.Normalize):
def __init__(self, vmin=None, vmax=None, clip=False):
colors.Normalize.__init__(self, vmin, vmax, clip)
def __call__(self, value, clip=None):
x, y = [self.vmin, self.vmax], [0, 1]
return np.ma.masked_array(np.interp(
np.mod(value-self.vmin, self.vmax-self.vmin),x,y
))
# Create triangulation.
x = np.asarray([0, 1, 2, 3, 0.5, 1.5, 2.5, 1, 2, 1.5])
y = np.asarray([0, 0, 0, 0, 1.0, 1.0, 1.0, 2, 2, 3.0])
triangles = [[0, 1, 4], [1, 2, 5], [2, 3, 6], [1, 5, 4], [2, 6, 5], [4, 5, 7],
[5, 6, 8], [5, 8, 7], [7, 8, 9]]
triang = mtri.Triangulation(x, y, triangles)
cm = colors.LinearSegmentedColormap.from_list('test', ['k','w','k'], N=1000)
#Figure 1 : modulo is applied on the data :
#Results : problem with the interpolation, but the colorbar is fine
z = np.mod(10*x,2*np.pi)
zMax = np.max(z)
levels = np.linspace(0., zMax,100)
fig1, ax1 = plt.subplots()
cax1=ax1.tricontourf(triang, z,cmap = cm,levels= levels)
fig1.colorbar(cax1)
plt.show()
#Figure 2 : We use the norm parameter with a custom norm that does the modulo
#Results : the graph is the way it should be but the colormap is messed up
z = 10*x
zMax = np.max(z)
levels = np.linspace(0., zMax,100)
fig2, ax2 = plt.subplots()
cax2=ax2.tricontourf(triang, z,levels= levels,norm = PeriodicNormalize(0, 2*np.pi),cmap = cm)
fig2.colorbar(cax2)
plt.show()
Last solution would be to do as I did above : to create a much larger colormap that goes up to zmax and is periodic every 2 pi. However the colorbar would not be nice...
here are the results :
I'm guessing that your problem arises from using modulo on your data before you call tricontourf (which, I guess, does some interpolation on your data and then maps that interpolated data to a colormap). Instead, you can pass a norm to your tricontourf function. Writing a small class following this tutorial, you can make the norm take care of the modulo of your data. As your code is not runnable as such, I came up with an a bit simpler example. Hopefully this is applicable to your problem:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as colors
class PeriodicNormalize(colors.Normalize):
def __init__(self, vmin=None, vmax=None, clip=False):
colors.Normalize.__init__(self, vmin, vmax, clip)
def __call__(self, value, clip=None):
x, y = [self.vmin, self.vmax], [0, 1]
return np.ma.masked_array(np.interp(
np.mod(value-self.vmin, self.vmax-self.vmin),x,y
))
fig,ax = plt.subplots()
x,y = np.meshgrid(
np.linspace(0, 1, 1000),
np.linspace(0, 1, 1000),
)
z = x*10*np.pi
cm = colors.LinearSegmentedColormap.from_list('test', ['k','w','k'], N=1000)
ax.pcolormesh(x,y,z,norm = PeriodicNormalize(0, 2*np.pi), cmap = cm)
plt.show()
The result looks like this:
EDIT:
As the ContourSet you get back from tricontourf spans the full phase, not just the first [0,2pi], the colorbar is created for that full range, which is why you see the colormap repeat itself many times. I'm not quite sure if I understand how the ticks are created, but I'm guessing that it would be quite some work to get that automated to work right. Instead, I suggest to generate a colorbar "by hand", as is done in this tutorial. This, however, requires that you create the axes (cax) where the colorbar is put yourself. Luckily there is a function called matplotlib.colorbar.make_axes() that does this for you (all thanks goes to this answer). So, instead of your original colorbar command, use these two lines:
cax,kw = mcbar.make_axes([ax2], location = 'right')
cb1 = mcbar.ColorbarBase(cax, cmap = cm, norm = norm, orientation='vertical')
To get this picture:

How can I use Matplotlib to re-adjust limits of an axis (added to host plot) using the interactive zoom tool?

I am displaying information with two y-axes and a common x-axis using the following script.
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import host_subplot
import mpl_toolkits.axisartist as AA
#creating a host plot with x and y axis
hostplot = host_subplot(111, axes_class=AA.Axes)
#creating a second y axis
extra_y_axis = hostplot.twinx()
extra_y_axis.set_navigate_mode(True)
extra_y_axis.set_navigate(True)
print extra_y_axis.can_zoom() #prints true on output
hostplot.set_xlabel("host_x")
hostplot.set_ylabel("host_y")
extra_y_axis.set_ylabel("extra_y")
hostplot.plot([0, 1, 2], [0, 1, 2])
extra_y_axis.plot([0, 1, 2], [0, 3, 2])
plt.draw()
plt.show()
After this I used the 'Zoom to Rectangle' tool from the tray in the bottom-left as shown below:
.
And I got the following output:
.
Please notice the y-axis scales in both the images. While the zoom functionality is working correctly for the host plot, I am unable to get the extra_y_axis to rescale and it just maintains a constant scale throughout (so I can't really zoom in on plots using the second axis).
How can I make it so that all the axes are rescaled on zooming in a small portion?
Thanks
I've traced down your problem to the sue of the axes_grid1 toolkit. If you don't require the use of this toolkit you can easily fix your issue by initialising your figure in the usual manner:
import matplotlib.pyplot as plt
#creating a host plot with x and y axis
fig, hostplot = plt.subplots()
#creating a second y axis
extra_y_axis = hostplot.twinx()
hostplot.set_xlabel("host_x")
hostplot.set_ylabel("host_y")
extra_y_axis.set_ylabel("extra_y")
hostplot.plot([0, 1, 2], [0, 1, 2])
extra_y_axis.plot([0, 1, 2], [0, 3, 2])
plt.show()
If you do want to use the toolkit then you have to add a couple of lines to get the two y axes to scale and transform together:
import matplotlib.transforms as mtransforms
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.parasite_axes import SubplotHost
fig = plt.figure()
ax1 = SubplotHost(fig, 1, 1, 1)
#set the scale difference between the two y axes
aux_trans = mtransforms.Affine2D().scale(sx = 1.,sy= 1.5)
ax2 = ax1.twin(aux_trans)
fig.add_subplot(ax1)
ax1.plot([0, 1, 2], [0, 1, 2])
ax2.plot([0, 1, 2], [0, 3, 2])
ax1.set_ylim(0,3)
plt.show()

How to plot vectors in python using matplotlib

I am taking a course on linear algebra and I want to visualize the vectors in action, such as vector addition, normal vector, so on.
For instance:
V = np.array([[1,1],[-2,2],[4,-7]])
In this case I want to plot 3 vectors V1 = (1,1), M2 = (-2,2), M3 = (4,-7).
Then I should be able to add V1,V2 to plot a new vector V12(all together in one figure).
when I use the following code, the plot is not as intended
import numpy as np
import matplotlib.pyplot as plt
M = np.array([[1,1],[-2,2],[4,-7]])
print("vector:1")
print(M[0,:])
# print("vector:2")
# print(M[1,:])
rows,cols = M.T.shape
print(cols)
for i,l in enumerate(range(0,cols)):
print("Iteration: {}-{}".format(i,l))
print("vector:{}".format(i))
print(M[i,:])
v1 = [0,0],[M[i,0],M[i,1]]
# v1 = [M[i,0]],[M[i,1]]
print(v1)
plt.figure(i)
plt.plot(v1)
plt.show()
How about something like
import numpy as np
import matplotlib.pyplot as plt
V = np.array([[1,1], [-2,2], [4,-7]])
origin = np.array([[0, 0, 0],[0, 0, 0]]) # origin point
plt.quiver(*origin, V[:,0], V[:,1], color=['r','b','g'], scale=21)
plt.show()
Then to add up any two vectors and plot them to the same figure, do so before you call plt.show(). Something like:
plt.quiver(*origin, V[:,0], V[:,1], color=['r','b','g'], scale=21)
v12 = V[0] + V[1] # adding up the 1st (red) and 2nd (blue) vectors
plt.quiver(*origin, v12[0], v12[1])
plt.show()
NOTE: in Python2 use origin[0], origin[1] instead of *origin
This may also be achieved using matplotlib.pyplot.quiver, as noted in the linked answer;
plt.quiver([0, 0, 0], [0, 0, 0], [1, -2, 4], [1, 2, -7], angles='xy', scale_units='xy', scale=1)
plt.xlim(-10, 10)
plt.ylim(-10, 10)
plt.show()
Your main problem is you create new figures in your loop, so each vector gets drawn on a different figure. Here's what I came up with, let me know if it's still not what you expect:
CODE:
import numpy as np
import matplotlib.pyplot as plt
M = np.array([[1,1],[-2,2],[4,-7]])
rows,cols = M.T.shape
#Get absolute maxes for axis ranges to center origin
#This is optional
maxes = 1.1*np.amax(abs(M), axis = 0)
for i,l in enumerate(range(0,cols)):
xs = [0,M[i,0]]
ys = [0,M[i,1]]
plt.plot(xs,ys)
plt.plot(0,0,'ok') #<-- plot a black point at the origin
plt.axis('equal') #<-- set the axes to the same scale
plt.xlim([-maxes[0],maxes[0]]) #<-- set the x axis limits
plt.ylim([-maxes[1],maxes[1]]) #<-- set the y axis limits
plt.legend(['V'+str(i+1) for i in range(cols)]) #<-- give a legend
plt.grid(b=True, which='major') #<-- plot grid lines
plt.show()
OUTPUT:
EDIT CODE:
import numpy as np
import matplotlib.pyplot as plt
M = np.array([[1,1],[-2,2],[4,-7]])
rows,cols = M.T.shape
#Get absolute maxes for axis ranges to center origin
#This is optional
maxes = 1.1*np.amax(abs(M), axis = 0)
colors = ['b','r','k']
for i,l in enumerate(range(0,cols)):
plt.axes().arrow(0,0,M[i,0],M[i,1],head_width=0.05,head_length=0.1,color = colors[i])
plt.plot(0,0,'ok') #<-- plot a black point at the origin
plt.axis('equal') #<-- set the axes to the same scale
plt.xlim([-maxes[0],maxes[0]]) #<-- set the x axis limits
plt.ylim([-maxes[1],maxes[1]]) #<-- set the y axis limits
plt.grid(b=True, which='major') #<-- plot grid lines
plt.show()
EDIT OUTPUT:
What did you expect the following to do?
v1 = [0,0],[M[i,0],M[i,1]]
v1 = [M[i,0]],[M[i,1]]
This is making two different tuples, and you overwrite what you did the first time... Anyway, matplotlib does not understand what a "vector" is in the sense you are using. You have to be explicit, and plot "arrows":
In [5]: ax = plt.axes()
In [6]: ax.arrow(0, 0, *v1, head_width=0.05, head_length=0.1)
Out[6]: <matplotlib.patches.FancyArrow at 0x114fc8358>
In [7]: ax.arrow(0, 0, *v2, head_width=0.05, head_length=0.1)
Out[7]: <matplotlib.patches.FancyArrow at 0x115bb1470>
In [8]: plt.ylim(-5,5)
Out[8]: (-5, 5)
In [9]: plt.xlim(-5,5)
Out[9]: (-5, 5)
In [10]: plt.show()
Result:
Thanks to everyone, each of your posts helped me a lot.
rbierman code was pretty straight for my question, I have modified a bit and created a function to plot vectors from given arrays. I'd love to see any suggestions to improve it further.
import numpy as np
import matplotlib.pyplot as plt
def plotv(M):
rows,cols = M.T.shape
print(rows,cols)
#Get absolute maxes for axis ranges to center origin
#This is optional
maxes = 1.1*np.amax(abs(M), axis = 0)
colors = ['b','r','k']
fig = plt.figure()
fig.suptitle('Vectors', fontsize=10, fontweight='bold')
ax = fig.add_subplot(111)
fig.subplots_adjust(top=0.85)
ax.set_title('Vector operations')
ax.set_xlabel('x')
ax.set_ylabel('y')
for i,l in enumerate(range(0,cols)):
# print(i)
plt.axes().arrow(0,0,M[i,0],M[i,1],head_width=0.2,head_length=0.1,zorder=3)
ax.text(M[i,0],M[i,1], str(M[i]), style='italic',
bbox={'facecolor':'red', 'alpha':0.5, 'pad':0.5})
plt.plot(0,0,'ok') #<-- plot a black point at the origin
# plt.axis('equal') #<-- set the axes to the same scale
plt.xlim([-maxes[0],maxes[0]]) #<-- set the x axis limits
plt.ylim([-maxes[1],maxes[1]]) #<-- set the y axis limits
plt.grid(b=True, which='major') #<-- plot grid lines
plt.show()
r = np.random.randint(4,size=[2,2])
print(r[0,:])
print(r[1,:])
r12 = np.add(r[0,:],r[1,:])
print(r12)
plotv(np.vstack((r,r12)))
Vector addition performed on random vectors
All nice solutions, borrowing and improvising for special case -> If you want to add a label near the arrowhead:
arr = [2,3]
txt = “Vector X”
ax.annotate(txt, arr)
ax.arrow(0, 0, *arr, head_width=0.05, head_length=0.1)
In order to match the vector lenght and angle with the x,y coordinates of the plot, you can use to following options to plt.quiver:
plt.figure(figsize=(5,2), dpi=100)
plt.quiver(0,0,250,100, angles='xy', scale_units='xy', scale=1)
plt.xlim(0,250)
plt.ylim(0,100)
Quiver is a good method once you figure out its annoying nuances, like not plotting vectors in their original scales. To do as far as I can tell you must pass these params to quiver call as many have pointed out: angles='xy', scale_units='xy', scale=1 AND you should set your plt.xlim and plt.ylim such that you get a square or near square grid. That is the only way I have gotten it to consistently plot the way I want. For instance passing a origin as *[0,0] and U, V as *[5,3] means the resulting plot should be a vector centered at 0,0 origin that goes over 5 units to the right on the x-axis and 3 units up on the y-axis.

Categories

Resources