I'm trying to clip a cloud of points by several polygons, but I don't know if this is possible with plt.axis.set_clip_path().
Since set_clip_path() requires a Path or a Patch as arguments, how could you create a geometry formed by several Polygons? It would be something like a plt.MultiPolygon(), but that doesn't exist. I've tried to create a matplotlib.PatchCollection with all the Polygons, but that does not work.
Here is the desired goal (from upper to lower figure):
Here is how I'd like the code to look like:
import matplotlib.pyplot as plt
from matplotlib.collections import PatchCollection
import numpy as np
fig, ax = plt.subplots()
points = np.array([np.random.random(100)*400,
np.random.random(100)*100]).T
A = plt.Polygon( np.array([( 0, 0),(50,100),(100, 0)]), color='w', ec='k' )
B = plt.Polygon( np.array([(120 , 0),(170 , 100), (220, 0)]), color='w', ec='k' )
C = plt.Polygon( np.array([(240 , 0),(290 , 100), (340, 0)]), color='w', ec='k' )
[ax.add_patch(i) for i in (A,B,C)]
ax.scatter(points[:,0], points[:,1], zorder=3).set_clip_path([A,B,C])
You can concatenate the vertices and the codes of all polygons, and use them to create a "compound path". Matplotlib's path tutorial contains an example creating a histogram from just one compound path.
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import PathPatch
import numpy as np
points = np.array([np.random.random(100) * 400,
np.random.random(100) * 100]).T
A = plt.Polygon(np.array([(0, 0), (50, 100), (100, 0)]), color='w', ec='k')
B = plt.Polygon(np.array([(120, 0), (170, 100), (220, 0)]), color='w', ec='k')
C = plt.Polygon(np.array([(240, 0), (290, 100), (340, 0)]), color='w', ec='k')
fig, ax = plt.subplots()
all_polys = [A, B, C]
[ax.add_patch(i) for i in all_polys]
vertices = np.concatenate([i.get_path().vertices for i in all_polys])
codes = np.concatenate([i.get_path().codes for i in all_polys])
dots = ax.scatter(points[:, 0], points[:, 1], zorder=3)
dots.set_clip_path(PathPatch(Path(vertices, codes), transform=ax.transData))
plt.show()
Related
I am trying to automatically give each Patch in a PatchCollection a color from a color map like tab20.
from matplotlib.collections import PatchCollection
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(5,5))
coords = [
(0, 0),
(1, 2),
(1, 3),
(2, 2),
]
patches = [plt.Circle(coords[i], 0.1) for i in range(len(coords))]
patch_collection = PatchCollection(patches, cmap='tab20', match_original=True)
ax.add_collection(patch_collection)
ax.set_xlim(-1, 3)
ax.set_ylim(-1, 4)
plt.axis('equal')
But the above code is drawing each circle using the same color. How can the colors be cycled?
Here I've sampled the tab20 colormap, so that the RGBA array cmap.colors has exactly 20 different entries, then I've assigned this RGBA array to the keyword argument facecolors that every collection accepts.
Not just for cosmetics, I've added a colormap, so that it's possible to recognize the order in which the circles were drawn.
from matplotlib.collections import PatchCollection
import matplotlib.pyplot as plt
from numpy.random import rand, seed
seed(20230104)
N = 20
coords = rand(N,2)*[2,1.2]
cmap = plt.get_cmap('tab20', N)
fig, ax = plt.subplots()
patches = [plt.Circle(coord, 0.06) for coord in coords]
# use facecolors=...
collection = PatchCollection(patches, facecolors=cmap.colors[:N-1])
ax.add_collection(collection)
cb = plt.colorbar(plt.cm.ScalarMappable(plt.Normalize(-0.5, N-0.5), cmap))
cb.set_ticks(range(N), labels=('%02d'%(n+1) for n in range(N)))
ax.autoscale(collection)
ax.set_aspect(1)
Overdone Version
from matplotlib.collections import PatchCollection
import matplotlib.pyplot as plt
from numpy.random import rand, seed
seed(20230104)
N = 20
coords = rand(N, 2) * [2, 1.2]
cmap = plt.get_cmap("tab20", N)
patches = (plt.Circle(coord, 0.06) for coord in coords)
fig, ax = plt.subplots()
im = ax.add_collection(
PatchCollection(
patches,
facecolors=cmap.colors,
edgecolors="w",
linewidth=2,
cmap=cmap,
norm=plt.Normalize(-0.50, N - 0.50),
)
)
cb = plt.colorbar(
im,
location="bottom",
fraction=0.05,
aspect=50,
drawedges=True,
)
cb.set_ticks(range(N), labels=("%02d" % (n + 1) for n in range(N)))
cb.dividers.set_color(ax._facecolor)
cb.dividers.set_linewidth(3)
ax.autoscale()
ax.set_aspect(1)
This gives each patch its color from a fixed subset of colors in the selected colormap, repeating as necessary:
from matplotlib.collections import PatchCollection
import matplotlib.pyplot as plt
num_col = 3
cmap = plt.cm.tab20
fig, ax = plt.subplots(figsize=(5,5))
coords = [
(0, 0),
(1, 2),
(1, 3),
(2, 2),
]
patches = [plt.Circle(coords[i], 0.1) for i in range(len(coords))]
patch_collection = PatchCollection(patches, facecolor=cmap.colors[0:num_col])
ax.add_collection(patch_collection)
ax.set_xlim(-1, 3)
ax.set_ylim(-1, 4)
plt.axis('equal')
Output:
This gives a random color from the selected colormap by using numpy to generate a list of random numbers, then using the patch objects set_array method:
from matplotlib.collections import PatchCollection
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(figsize=(5,5))
coords = [
(0, 0),
(1, 2),
(1, 3),
(2, 2),
]
patches = [plt.Circle(coords[i], 0.1) for i in range(len(coords))]
color_vals = np.random.rand(len(patches))
patch_collection = PatchCollection(patches, cmap='tab20', match_original=True)
patch_collection.set_array(color_vals)
ax.add_collection(patch_collection)
ax.set_xlim(-1, 3)
ax.set_ylim(-1, 4)
plt.axis('equal')
Output:
I don't think match_original=True is necessary as you want to change the default color of the original patches. I'm sure there other ways of doing this as well. This SO post was helpful: setting color range in matplotlib patchcollection
I'm trying to create a scatter plot with 100 data points and three variables: x value, y value, and category. This information is stored in an ndarray.
I can create the scatter plot, but I don't know how to use a different color for each category. I used the following code for the plot, which seems to work fine (although it's not finished):
def my_plot(data, color_map):
f, ax = plt.subplots()
ax.scatter(data.x, data.y, s = 150, edgecolors = "r")
return f
In my function, color_map is a parameter which refers to a dictionary I created to color the different categories (there are four in total). This is the dictionary:
color_map = {"winter":(15, 28, 75), "spring":(92, 57, 32), "summer":(255, 253, 211), "fall":(174, 12, 12)}
What I would like to do is to somehow integrate this color_map in my function so that each dot in my plot receives a different color.
I think this could be done using np.where to create a mask, but I'm not sure how to proceed...
The color values need to be divided by 255 because matplotlib likes them between 0 and 1.
With this dict you can create an array of colors for the categories:
from matplotlib import pyplot as plt
from matplotlib.lines import Line2D
import pandas as pd
import numpy as np
color_map = {"winter": (15, 28, 75), "spring": (92, 57, 32), "summer": (255, 253, 211), "fall": (174, 12, 12)}
color_map = {key: (r / 255, g / 255, b / 255,) for key, (r, g, b) in color_map.items()}
N = 200
data = pd.DataFrame({'x': np.random.uniform(1, 9, N), 'y': np.random.uniform(1, 5, N),
'cat': np.random.choice([*color_map.keys()], N)})
fig, ax = plt.subplots()
ax.scatter(data.x, data.y, s=150, color=[color_map[c] for c in data.cat], ec='r')
handles = [Line2D([], [], marker='o', ls='', color=col, markeredgecolor='r', label=label)
for label, col in color_map.items()]
plt.legend(handles=handles, bbox_to_anchor=[1.02, 1.02], loc='upper left')
plt.tight_layout()
plt.show()
PS: A similar plot can be generated with seaborn, which also automatically adds the corresponding legend. Note that the current version of matplotlib (3.3.1) has a problem with the hue parameter. Normally you would add it as hue='cat' but in this version a workaround via .to_list is needed.
import seaborn as sns
ax = sns.scatterplot(x='x', y='y', hue=data['cat'].to_list(), s=150, palette=color_map, edgecolor='r', data=data)
I am plotting a collection of rectangles with matplotlib.patches. My code is:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig = plt.figure(figsize=(14, 10))
for i in rectangles_list:
ax1 = fig.add_subplot(111, aspect='equal')
ax1.add_patch(patches.Rectangle(
(x[i], y[i]),
width[i],
height[i],
alpha = 1.0,
facecolor = colors_list[i]
)
)
plt.show()
The rectangles may be overlapping, therefore some of them may be completely hidden. Do you know if it is possible to get the colors of the visible rectangles? I mean the colors of the rectangles that are not completely hidden and therefore that can be actually viewed by the user. I was thinking to some function that returns the color of the pixels, but more intelligent ideas are welcome. If possible, I'd prefer to not use PIL. Unfortunately I cannot find any solution on the internet.
Following Vlass Sokolov comment and this Stackoverflow post by Joe Kington, here is how you could get a numpy array containing all the unique colors that are visible on a matplotlib figure:
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np
plt.close('all')
# Generate some data :
N = 1000
x, y = np.random.rand(N), np.random.rand(N)
w, h = np.random.rand(N)/10 + 0.05, np.random.rand(N)/10 + 0.05
colors = np.vstack([np.random.random_integers(0, 255, N),
np.random.random_integers(0, 255, N),
np.random.random_integers(0, 255, N)]).T
# Plot and draw the data :
fig = plt.figure(figsize=(7, 7), facecolor='white')
ax = fig.add_subplot(111, aspect='equal')
for i in range(N):
ax.add_patch(Rectangle((x[i], y[i]), w[i], h[i], fc=colors[i]/255., ec='none'))
ax.axis([0, 1, 0, 1])
ax.axis('off')
fig.canvas.draw()
# Save data in a rgb string and convert to numpy array :
rgb_data = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep='')
rgb_data = rgb_data.reshape((int(len(rgb_data)/3), 3))
# Keep only unique colors :
rgb_data = np.vstack({tuple(row) for row in rgb_data})
# Show and save figure :
fig.savefig('rectangle_colors.png')
plt.show()
I have a list of color values (in either of the formats: hex ('#ffffff') or rgb (255,255,255) if that helps). These colors correspond explicitly with the line segment between points. Currently I plot a line as a collection of line segments via:
import matplotlib.pyplot as plt
import itertools
colors = itertools.cycle('#ffffff', '#ffffff', '#ff0320', '#452143', ...)
t = (0, 1, 2, 3, ...)
var1 = (43, 15, 25, 9, ...)
ax = plt.subplot2grid((3,1), (0,0), colspan=3, rowspan=1)
ps = [(t,var1) for (t,var1) in zip(t, val)]
for start, end in zip(ps[:-1], ps[1:]):
t, var1 = zip(start, end)
c = next(colors)
ax.plot(t, var1, color=c)
However since I have a color for each point I would much prefer to set a cmap for the plot. How might I accomplish converting a list of colors into a cmap which I can use when plotting a line?
As tcaswell says, use a LineCollection for this:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.collections import LineCollection
# a random walk
xy = np.cumsum(np.random.randn(1000, 2), axis=0)
z = np.linspace(0, 1, 1000)
lc = LineCollection(zip(xy[:-1], xy[1:]), array=z, cmap=plt.cm.hsv)
fig, ax = plt.subplots(1, 1)
ax.add_collection(lc)
ax.margins(0.1)
plt.show()
Is it possible to tie the linewidth of a matplotlib path to the figure zoom/scale level?
I am drawing a map where the matplotlib path (with bezier curves) draws the road on the map. Upon zooming in I would like the width of the path to zoom in.
In attached script, the polygonal approximation can properly zoom, but the path (red line) cannot zoom (in width).
Is it possible to tie the linewidth to some scale transformation and redraw via callback ?
import matplotlib.pyplot as plt
from matplotlib.path import Path
import matplotlib.patches as patches
import numpy as np
def main():
ax = plt.subplot(111)
verts = np.array([ (0., 0.), (0.5, .5), (1., 0.8), (0.8, 0.)])
codes = np.array([Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.LINETO ])
# Can this curve have zoomable width
path = Path(verts, codes)
patch = patches.PathPatch(path, fc='none', color='r', lw=4, zorder=3)
ax.add_patch(patch)
ax.plot(verts[:,0], verts[:,1], 'o--', lw=2, color='k', zorder=2)
# these will be polygonal approx that will have proper zoom
v=np.array([]).reshape((-1,2))
c=[]
for i in range(len(verts)-1):
vtmp, ctmp = line2poly(verts[[i,i+1],:],0.03)
v = np.vstack( (v,vtmp) )
c = np.concatenate( (c,ctmp) )
path_zoom = Path(v,c)
patch_zoom = patches.PathPatch(path_zoom, fc='r', ec='k', zorder=1, alpha=0.4)
ax.add_patch(patch_zoom)
ax.set_xlim(-0.1, 1.1)
ax.set_ylim(-0.1, 1.1)
plt.show()
def line2poly(line, width):
dx,dy = np.hstack(np.diff(line,axis=0)).tolist()
theta = np.arctan2(dy,dx)
print(np.hstack(np.diff(line,axis=0)).tolist())
print(np.degrees(theta))
s = width/2 * np.sin(theta)
c = width/2 * np.cos(theta)
trans = np.array([(-s,c),(s,-c),(s,-c),(-s,c)])
verts = line[[0,0,1,1],:]+trans
verts = np.vstack((verts, verts[0,:]))
codes = np.array([Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
return verts,codes
if __name__=='__main__':
main()
To the best of my knowledge, there's no way to do this in matplotlib, as the stroke width of a line cannot be directly tied to data coordinates. (As you mentioned, you could connect a callback to the draw event and accomplish this. It would incur a large performance penalty, though.)
However, a quick workaround would be to use shapely to generate polygons by buffering your street paths.
As a quick example:
import shapely.geometry
import descartes
import matplotlib.pyplot as plt
lines = ([(0, 0), (1, 0), (0, 1)],
[(0, 0), (1, 1)],
[(0.5, 0.5), (1, 0.5)],
)
lines = shapely.geometry.MultiLineString(lines)
# "0.05" is the _radius_ in data coords, so the width will be 0.1 units.
poly = lines.buffer(0.05)
fig, ax = plt.subplots()
patch = descartes.PolygonPatch(poly, fc='gray', ec='black')
ax.add_artist(patch)
# Rescale things to leave a bit of room around the edges...
ax.margins(0.1)
plt.show()
If you did want to take the callback route, you might do something like this:
import matplotlib.pyplot as plt
def main():
lines = ([(0, 0), (1, 0), (0, 1)],
[(0, 0), (1, 1)],
[(0.5, 0.5), (1, 0.5)],
)
fig, ax = plt.subplots()
artists = []
for verts in lines:
x, y = zip(*verts)
line, = ax.plot(x, y)
artists.append(line)
scalar = StrokeScalar(artists, 0.1)
ax.callbacks.connect('xlim_changed', scalar)
ax.callbacks.connect('ylim_changed', scalar)
# Rescale things to leave a bit of room around the edges...
ax.margins(0.05)
plt.show()
class StrokeScalar(object):
def __init__(self, artists, width):
self.width = width
self.artists = artists
# Assume there's only one axes and one figure, for the moment...
self.ax = artists[0].axes
self.fig = self.ax.figure
def __call__(self, event):
"""Intended to be connected to a draw event callback."""
for artist in self.artists:
artist.set_linewidth(self.stroke_width)
#property
def stroke_width(self):
positions = [[0, 0], [self.width, self.width]]
to_inches = self.fig.dpi_scale_trans.inverted().transform
pixels = self.ax.transData.transform(positions)
points = to_inches(pixels) * 72
return points.ptp(axis=0).mean() # Not quite correct...
main()