I would like to set individual properties (zorder and label for example) for a specific element of a matplotlib.collections.PathCollection. I couldn't find a way in the documentation.
Here I jot down a user case.
Let say we have the following snippet, and we'd like to change the zorder of the red ball, bringing it to the top, by using the balls handle, which is a matplotlib.collections.PathCollection.
balls = plt.scatter([-1, 1], [0, 0], c = ['r', 'b'], s = 4e4)
plt.axis([-5, 5, -5, 5])
Does anyone have any idea about how to tweak individual paths of a PathCollection?
The alternative would be using plt.plot('o'), which actually returns a list of handles. Unfortunately the plt.plot('o') solution won't allow me to set a different colour per ball, since they would all belong to the same chart. So a for loop would be required.
The drastic solution, which I bet I'll go for, since my deadline, is going of Inkscape :/
Not sure if this is the best solution, but it might help you.
From what I can see, the paths in the PathCollection are always plotted in the order they are created. So in your case, the path with the x-position of -1 is created first, then the one with 1.
You can switch that order after initially plotting them, by changing the offsets, in your case using balls.set_offsets():
In [4]: balls = plt.scatter([-1, 1], [0, 0], c = ['r', 'b'], s = 4e4)
In [5]: plt.axis([-5, 5, -5, 5])
This creates the following figure:
In [42]: print balls.get_offsets()
[[-1. 0.]
[ 1. 0.]]
On [43]: balls.set_offsets([[1,0],[-1,0]])
Now, this has plotted the left-hand ball on top of the right-hand ball:
But as you can see, this has also switched the facecolors around (since we set the order of that in the call to plt.scatter as ['r','b']. There's a solution to this, which is to also switch the facecolors around:
In [46]: balls.set_facecolors(['b','r'])
Great, so putting that all together, we can define a function to switch the offsets and facecolors of any two arbitrary paths in the PathCollection.
import matplotlib.pyplot as plt
fig,ax = plt.subplots()
balls = ax.scatter([-3, -1, 1, 3], [0, 0, 0, 0], c = ['r', 'b', 'g', 'm'], s = 4e4)
ax.set_xlim(-6,6)
ax.set_ylim(-6,6)
plt.savefig('balls_01.png')
def switch_scatter(pathcoll,a,b):
# Switch offsets
offsets = pathcoll.get_offsets()[:]
offsets[[a,b]] = offsets[[b,a]]
# Switch facecolors
facecolors = pathcoll.get_facecolors()
facecolors[[a,b]] = facecolors[[b,a]]
# Switch sizes
sizes = pathcoll.get_sizes()
sizes[[a,b]] = sizes[[b,a]]
# Set the new offsets, facecolors and sizes on the PathCollection
pathcoll.set_offsets(offsets)
pathcoll.set_facecolors(facecolors)
pathcoll.set_sizes(sizes)
switch_scatter(balls,2,1)
plt.savefig('balls_02.png')
Heres balls_01.png:
And here is balls_02.png (where we switch ball 1 and ball 2 (the blue and green balls)
A final note: if you have other properties varying in your scatter plot (e.g. linecolor), you will also need to switch them around in the function I defined above.
Related
I am working on a manual implementation of Ruppert's algorithm for my final project. The algorithm requires a constrained delaunay diagram as an input, which PyVista can produce by using the delaunay_2d() function. However, I am finding some inconsistency between how they managed to produce a mesh with hollowed out shapes inside, while I am unable to make my own custom inputs that achieve the same results.
For some context, here is the documentation on delaunay_2d() in PyVista: https://docs.pyvista.org/api/core/_autosummary/pyvista.PolyData.delaunay_2d.html
The code and image of the final example of this page, modified slightly for my own purposes (I turned the circles into squares), is shown below:
import pyvista as pv
squar = pv.Polygon(n_sides=4, radius=8, fill=False)
squar = squar.rotate_z(45, inplace=False)
hole1 = pv.Polygon(center=(2,3,0), n_sides=4, radius=1)
hole2 = pv.Polygon(center=(-2,-3,-0), n_sides=4, radius=0.5)
comb = hole1 + hole2 + squar
comb.plot(cpos='xy',show_edges=True)
tess = comb.delaunay_2d(edge_source=comb)
tess.plot(cpos='xy', show_edges=True)
The plot of comb, before the triangulation
The resulting constrained delaunay diagram
I tried to implement my own custom edge constraints as follows:
points1 = [[1,0,0],[1,1,0],[0,1,0],[0,0,0]]
points2 = [[0.25,0.5,0],[0.25,0.25,0],[0.5,0.25,0],[0.5,0.5,0]]
faces2 = [4, 0, 1, 2, 3]
lines1 = [5, 0, 1, 2, 3, 0]
lines2 = [5, 0, 1, 2, 3, 0]
rect1 = pv.PolyData(points1, lines=lines1)
rect2 = pv.PolyData(points2, faces2, lines=lines2)
PSLG = rect2 + rect1
PSLG.plot(cpos='xy', show_edges=True)
tess = PSLG.delaunay_2d(edge_source=PSLG)
tess.plot(cpos='xy', show_edges=True)
The plot of the PSLG constraints
The resulting constrained delaunay diagram
For some reason, my input does not recognize the inside square as a hole and it only uses the outer edges for the delaunay diagram. I am unsure why this is the case as from my understanding, I should have followed how the API example performed the operation to the letter. Please let me know if you have any insight to this matter.
Gary Lucas pointed out how other APIs use the order of the polygon to indicate "enclosing" or "excluding." For PyVista, apparently that order is determined by the input given to the face.
In this case, reversing the face input to [4, 0, 3, 2, 1] is sufficient.
The result after reversing the point order of the face I wish to exclude
In pyqtgraph you can scatterplot each item for itself or a whole bunch of them as bulk (using spots). working with large datasets i prefer the last method since the figure stays light and is movable without lagging all over the screen.
my problem
some of my symbols i need an angle... that isn't that much of a problem, however if i add them separately to the plot it results in a laggy figure. so my problem is that i am currently unable to find a suitable way to subclass the whole thing and implement a small method for the keyword argument "rotation"/"angle". has anyone finished this task already or has someone an idea?
thank you very much in advance!
After another look today I finally found that it was way too simple: Just rotating my symbol before adding it to the ScatterPlotItem did the trick. For the sake of documentation and maybe some other struggling programmers, a snippet:
import numpy as np
import pyqtgraph as pg
# define a symbol bowtie style
_mos = np.asarray([
[0.5, 0.25],
[0.5, -0.25],
[-0.5, 0.25],
[-0.5, -0.25],
[0.5, 0.25]
])
my_symbol = pg.arrayToQPath(_mos[:, 0], _mos[:, 1], connect='all')
# define color and stuff for your items
exit_item = pg.ScatterPlotItem(
size=20,
pen=pg.mkPen(128, 128, 128, 255),
brush=pg.mkBrush(255, 255, 255, 255),
)
# calculate angle between two sets of points
angle = np.arctan2(np.asarray(y1-y0), np.asarray(x1-x0)) * 180/np.pi
# rotate symbol with that angle
tr = QTransform()
angle_rot = tr.rotate(angle)
my_rotated_symbol = angle_rot.map(my_symbol)
# may be a whole list of spots with different angles and positions
exit_spots = []
exit_spots.append({
'pos': (0, 0),
'symbol': my_rotated_symbol
})
# add the spots to the item
exit_item.addPoints(exit_spots)
# create a plot and add the content
win = pg.GraphicsWindow()
plot = win.addPlot()
plot.addItem(exit_item)
I am running into a problem that I am having trouble figuring out in python (which I will currently blame on sever jetlag).
I have an array, let's call it x. The plot of x where y-axis is generic value, x-axis is index of array, looks like:
What I want to do is isolate the flat sections after the initial bump (see next picture that I am interested in):
I want to ignore the leading flat line and bump, and make an array of the five red boxes in the second image such that I have something like
x_chunk = [[box 0], [box 1], [box 2], [box 3], [box 4]]
I want to ignore all of the sloped transition line between the red chunks. I am having trouble figuring out the proper iterating procedure and setting the condition such that I get what I need.
So, this is probably not the cleanest solution, however it works:
import numpy as np
import matplotlib.pyplot as plt
# Create data
r=np.random.random(50)
y1 = np.array([50,40,30,20,10])
y=np.repeat(y1,10)
y[9]=y[9]+10
y=y+r
# Plot data
x=np.arange(len(y))
plt.plot(x,y)
plt.show()
Will give you something like this:
# Find maximum and start from there
idxStart=np.argmax(y)
y2=y[idxStart:]
# Grab jump indices
idxs=np.where(np.diff(y2)<-1)[0]+1
# Put into boxes
boxs=[]
for i in range(len(idxs)-1):
boxs.append(y2[idxs[i]:idxs[i+1]])
print boxs
Of course you will need to find the right threshold to distinguish the "jumps/drops" in the data, in my case -1 was good enough since random returns values between 0 and 1. Hope your jetlag gets better soon.
Not tested as I have no data, but something like this should work
def findSteps(arr, thr=.02, window=10, disc=np.std):
d = disc(np.lib.stride_tricks.as_strided(arr, strides = arr.strides*2, shape = (arr.size-window+1, window)), axis = 1)
m = np.minimum(np.abs(d[:-window]), np.abs(d[window:])) < thr
i = np.nonzero(np.diff(m))
return np.split(arr[window:-window], i)[::2]
May have to play around with the window and threshold value, and you may want to write a slope function for disc if np.std doesn't work, but the basic idea is looking forward and backward by window steps and seeing if the standard deviation (or slope) of the stride is close to 0.
You'll end up with blocks of True values, which you find the start and end of by np.nonzero(np.diff())
You then np.split the array into a list of arrays by the blocks and only take every other member of the list (since the other sub-arrays will be the transitions).
Suppose I have the 3x3 matrix below:
[apples 19 3.5]
[oranges 07 2.2]
[grapes 23 7.8]
Only in real life the matrix has dozens of rows, not just three.
I want to create an XY plot where the second column is the X coordinate, the third column is the Y coordinate, and the words themselves (i.e., the first column) are the markers (so no dots, lines, or any other symbols).
I also want the font size of each word to be determined by the second column (in the example above, that means making "grapes" have about three times the size of "oranges", for instance).
Finally, I want to color the words on a red-to-blue scale corresponding to the third column, with 0 = darkest red and 10 = darkest blue.
What's the best way to go about it in Python 2.x? I know I can use matplotlib's "annotate" and "text" to do many (if not all) of those things, but somehow that feels like a workaround. Surely there must be a way of declaring the words to be markers (so I don't have to treat them as "annotations")? Perhaps something outside matplotlib? Has anyone out there ever done something similar?
As you did not want to use annotate or text the next best thing is py.scatter which will accept a marker
``'$...$'`` render the string using mathtext.
For example
import pylab as py
data = [["peach", 1.0, 1.0],
["apples", 19, 3.5],
["oranges", 7, 2.2],
["grapes", 23, 7.8]]
for item in data:
py.scatter(item[1], item[2], s=700*item[1],
c=(item[2]/10.0, 0, 1 - item[2]/10.0),
marker=r"$ {} $".format(item[0]), edgecolors='none' )
py.show()
This method has several issues
Using \textrm{} in the math text so that it is not italic appears to break matplotlib
The letters sizes need to be adjusted by hand (hence the factor of 700)
It would probably be better to use a colormap rather than simply defining the RGB color value.
While looking around for a solution to the same problem, I've found one that seems a bit cleaner (or at least more in spirit to what the original question asked), namely to use TextPath:
from matplotlib import pyplot as plt
from matplotlib.text import TextPath
data = [["peach", 1.0, 1.0],
["apples", 19, 3.5],
["oranges", 7, 2.2],
["grapes", 23, 7.8]]
max_d2 = max([d[2] for d in data]) + 1e-3
max_d1 = max([d[1] for d in data]) + 1e-3
cmap = plt.get_cmap('RdBu')
for d in data:
path = TextPath((0,0), d[0])
# These dots are to display the weakness below, remove for the actual question
plt.plot(d[1],d[2],'.',color='k')
plt.plot(d[1],d[2],marker=path,markersize=100, color=cmap(d[2]/max_d2))
plt.xlim([0,max_d1+5])
plt.ylim([0,max_d2+0.5])
This solution has some advantages and disadvantages of its own:
Main disadvantage: as the dots show, I wasn't able to properly center the text as I wanted. Instead, the required value is the bottom left of the picture.
Main advantage: this has no latex issue and uses a "real" marker path, which means that it can easily be used to e.g. mark line plots (not the original question, though)
Code:
import numpy as np
x = np.cumsum(np.random.randn(100,5), axis=0)
plt.figure(figsize=(15,5))
for i in range(5):
label = TextPath((0,0), str(i), linewidth=1)
plt.plot(x[:,i], color='k')
plt.plot(np.arange(0,len(x),5),x[::5,i], color='k', marker=label, markersize=15, linewidth=0)
Doing the above via a naive loop over "text" or "annotate" would be very slow if you had many lines / markers, while this scales better.
I would like to get data from a single contour of evenly spaced 2D data (an image-like data).
Based on the example found in a similar question: How can I get the (x,y) values of the line that is ploted by a contour plot (matplotlib)?
>>> import matplotlib.pyplot as plt
>>> x = [1,2,3,4]
>>> y = [1,2,3,4]
>>> m = [[15,14,13,12],[14,12,10,8],[13,10,7,4],[12,8,4,0]]
>>> cs = plt.contour(x,y,m, [9.5])
>>> cs.collections[0].get_paths()
The result of this call into cs.collections[0].get_paths() is:
[Path([[ 4. 1.625 ]
[ 3.25 2. ]
[ 3. 2.16666667]
[ 2.16666667 3. ]
[ 2. 3.25 ]
[ 1.625 4. ]], None)]
Based on the plots, this result makes sense and appears to be collection of (y,x) pairs for the contour line.
Other than manually looping over this return value, extracting the coordinates and assembling arrays for the line, are there better ways to get data back from a matplotlib.path object? Are there pitfalls to be aware of when extracting data from a matplotlib.path?
Alternatively, are there alternatives within matplotlib or better yet numpy/scipy to do a similar thing? Ideal thing would be to get a high resolution vector of (x,y) pairs describing the line, which could be used for further analysis, as in general my datasets are not a small or simple as the example above.
For a given path, you can get the points like this:
p = cs.collections[0].get_paths()[0]
v = p.vertices
x = v[:,0]
y = v[:,1]
from: http://matplotlib.org/api/path_api.html#module-matplotlib.path
Users of Path objects should not access the vertices and codes arrays
directly. Instead, they should use iter_segments() to get the
vertex/code pairs. This is important, since many Path objects, as an
optimization, do not store a codes at all, but have a default one
provided for them by iter_segments().
Otherwise, I'm not really sure what your question is. [Zip] is a sometimes useful built in function when working with coordinates. 1
The vertices of an all paths can be returned as a numpy array of float64 simply via:
cs.allsegs[i][j] # for element j, in level i
where cs is defined as in the original question as:
import matplotlib.pyplot as plt
x = [1, 2, 3, 4]
y = [1, 2, 3, 4]
m = [[15, 14, 13, 12], [14, 12, 10, 8], [13, 10, 7, 4], [12, 8, 4, 0]]
cs = plt.contour(x, y, m, [9.5])
More detailed:
Going through the collections and extracting the paths and vertices is not the most straight forward or fastest thing to do. The returned Contour object actually has attributes for the segments via cs.allsegs, which returns a nested list of shape [level][element][vertex_coord]:
num_levels = len(cs.allsegs)
num_element = len(cs.allsegs[0]) # in level 0
num_vertices = len(cs.allsegs[0][0]) # of element 0, in level 0
num_coord = len(cs.allsegs[0][0][0]) # of vertex 0, in element 0, in level 0
See reference:
https://matplotlib.org/stable/api/contour_api.html
I am facing a similar problem, and stumbled over this matplotlib list discussion.
Basically, it is possible to strip away the plotting and call the underlying functions directly, not super convenient, but possible. The solution is also not pixel precise, as there is probably some interpolation going on in the underlying code.
import matplotlib.pyplot as plt
import matplotlib._cntr as cntr
import scipy as sp
data = sp.zeros((6,6))
data[2:4,2:4] = 1
plt.imshow(data,interpolation='none')
level=0.5
X,Y = sp.meshgrid(sp.arange(data.shape[0]),sp.arange(data.shape[1]))
c = cntr.Cntr(X, Y, data.T)
nlist = c.trace(level, level, 0)
segs = nlist[:len(nlist)//2]
for seg in segs:
plt.plot(seg[:,0],seg[:,1],color='white')
plt.show()