Matplotlib to plot hundreds of Rectangle contours - python

I want to plot a few Rectangle outlines using matplotlib. The thing is, I need tons of them, so the "normal" way of drawing rectangles is pretty slow. I solved this using How to speed up plot.... The thing is, I am not able anymore to just plot the edge of the rectangles using fill=None or edgecolor=... and facecolor=None.
See a toy example of my code:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import PatchCollection
from matplotlib.patches import Rectangle
def plot_rectangle_from_area(area, color):
"""Plot a rectangle for a given area with color"""
return Rectangle(xy=(area["min_x"], area["min_y"]), width=area["max_x"] - area["min_x"],
height=area["max_y"] - area["min_y"],
fill=None) # linewidth=0, edgecolor=color, facecolor=None, zorder=100, alpha=0,
sample_areas = [{"min_x": -1, "max_x": 0.4, "min_y": 0.7, "max_y": 1},
{"min_x": 0.5, "max_x": 1, "min_y": 0.1, "max_y": 0.5}]
rectangles = []
fig, ax = plt.subplots()
# ... print some contour via meshgrid
if sample_areas:
for area_i in sample_areas:
rectangles.append(plot_rectangle_from_area(area_i, color="r"))
# ... some more cases
# Append the rectangles all at once instead of on their own, see:
# https://stackoverflow.com/questions/33905305/how-to-speed-up-the-plot-of-a-large-number-of-rectangles-with-matplotlib
ax.add_collection(PatchCollection(rectangles))
# ... legend, save, grid, ...
plt.show()
At first I create a array for all the Rectangles, the append to it and use PatchCollection to plot it 1. The following example does exactly the same, just without the PatchCollection and is working fine.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import PatchCollection
from matplotlib.patches import Rectangle
def plot_rectangle_from_area(area, color):
"""Plot a rectangle from given area"""
return Rectangle(xy=(area["min_x"], area["min_y"]), width=area["max_x"] - area["min_x"],
height=area["max_y"] - area["min_y"],
fill=None) # linewidth=0, edgecolor=color, facecolor=None, zorder=100, alpha=0,
sample_areas = [{"min_x": -1, "max_x": 0.4, "min_y": 0.7, "max_y": 1},
{"min_x": 0.5, "max_x": 1, "min_y": 0.1, "max_y": 0.5}]
fig, ax = plt.subplots()
if sample_areas:
for area_i in sample_areas:
ax.add_patch(plot_rectangle_from_area(area_i, color="r"))
plt.show()
Here are some plots I created with both of these codes. On the left the wished result using the slow method, on the right the result I get with PatchCollection:
I tried multiple combinations of fill, edgecolor, facecolor and even the suggestion with zorder from here.
Is it possible to use the "fast" way of creating rectangles and have just the borders shown?

Yes - looking at the documentation for PatchCollection, there is a parameter called match_original, which - when True - will set the properties of the patches to match those of the original rectangles.
So simply change
ax.add_collection(PatchCollection(rectangles))
to
ax.add_collection(PatchCollection(rectangles, match_original=True))

Related

Draw Pathpatch with gradient fill in Python

I'd like to draw path patches in python with a gradient fill to better visualize the start and end of a path connection. I know that I can use FanyArrowPatch to have an arrow to indicate the direction but I'd prefer to use gradient colors (e.g. Blue to red).
Is there a way to define the color of a patch using something simple like just idnicating color = "RdBu"?
For wedge patches I used a not so convient way (for loop) by plotting small segements of it seperatly with adapting the colors. However, in my end plot I will have all lot of connections using patches.PathPatch and I need a more efficient way to define a gradient color. Also, I would not know how the apply the for-loop trick on Paths patches...
Do you have any suggestions how to plot pathes with gradient colors? Are there any other packages I could try?
import numpy as np
import matplotlib.patches as mpatches
from matplotlib.collections import PatchCollection
import matplotlib.pyplot as plt
import seaborn as sns
#---------------------------------------------
# Drawing example
#---------------------------------------------
fig, ax = plt.subplots(figsize=(8,8))
plt.title('matplotlib mpatch / path does not accept gradient colors directly')
ring = mpatches.Wedge((0,0), 0.7, 190, 350, color='r',width=0.2, label = 'wedge with only one color, no direction')
ax.add_patch(ring)
# drawing desired output with easy fix using for-loop, whcih is not working for mpatches.FancyArrowPatch
total_deg = 170
n_steps = 100
d_deg = total_deg / n_steps
colors = sns.color_palette("vlag", n_steps)
for i in range(n_steps-1):
ring = mpatches.Wedge((0,0), 0.7, i*d_deg, (i+1)*d_deg, color=colors[i],width=0.2)
ax.add_patch(ring)
i = i+1
ring = mpatches.Wedge((0,0), 0.7, i*d_deg, (i+1)*d_deg, color=colors[i],width=0.2, label = 'wedge with gradient color using for-loop')
ax.add_patch(ring)
# drawing desired output with easy fix using for-loop, whcih is not working for mpatches.FancyArrowPatch
verts = [[0.8,0.2],[0,1.4],[-0.9, 0.2]]
codes = [Path.MOVETO, Path.CURVE3, Path.CURVE3]
lw = 5
path = Path(verts, codes)
patch = mpatches.FancyArrowPatch(path=path,arrowstyle="-|>,head_length=10,head_width=10", lw=lw, color='b', zorder=1, label = 'path that should have gradient color')
ax.add_patch(patch)
plt.xlim([-1.5,1.5])
plt.ylim([-1.5,1.5])
plt.legend()
plt.axis('off')
plt.show()

Matplotlib contour: set range of colorbar (not range of colormap)

I have a problem with the colorbar for contour plots with user-defined levels. As you can see in the figure below, the color for the highest level (red) is not shown well in the colorbar. To solve this, I would like to change the range of the colorbar (e.g. from 0 to 1), while not changing the contour levels or range of the colormap.
So far, I have tried to manually set the ticks of the colorbar in the colorbar-call, but that doesn't change the range. (That is, ticks outside 0.3-0.8 will just not show up)
Minimum working example:
import numpy as np
import matplotlib.pyplot as plt
z = np.random.rand(10,10)
im = plt.contour(z, levels = [0.3,0.5,0.8], cmap="rainbow")
plt.colorbar(im)
I would like to change the range of the colorbar (e.g. from 0 to 1)
Save the colorbar object, change its boundaries and the ticks' position, finally update the plot to reflect our wishes.
import numpy as np
import matplotlib.pyplot as plt
z = np.random.rand(10,10)
im = plt.contour(z, levels = [0.3,0.5,0.8], cmap="rainbow")
cb = plt.colorbar(im)
cb.boundaries = np.array((0.0, 0.5, 1.0))
cb.set_ticks((0, 0.3, 0.5, 0.8, 1))
cb.update_normal(im)

How do I put a circle with annotation in matplotlib?

I plot a graph created by networkx with matplotlib. Now, I'd like to add annotation to a specific node with a circle around. For instance,
I use plt.annotate(*args, **kwargs) with the following code,
# add annotate text
pos = nx.get_node_attributes(G, 'pos')
pos_annotation_node = pos['Medici']
ax2.annotate('Midici',
xy=pos_annotation_node,
xytext=(i+0.2 for i in pos_annotation_node),
color='blue',
arrowprops=dict(facecolor='blue', shrink=0.01)
)
And I got this ugly graph,
I have two questions:
how do I draw a circle around the node 6 as shown in first figure.
to get a nice looking figure, I need manually set the value of xytext many times. Is there a better way?
If you use the fancyarrow arrowprops syntax as demonstrated in annotation_demo2, there is a shrinkA and shrinkB option that lets you shrink your arrow tail (shrinkA) and tip (shrinkB) independently, in points units.
Here's some arbitrary setup code:
import matplotlib.pyplot as plt
import numpy as np
# Some data:
dat = np.array([[5, 3, 4, 4, 6],
[1, 5, 3, 2, 2]])
# This is the point you want to point out
point = dat[:, 2]
# Make the figure
plt.figure(1, figsize=(4, 4))
plt.clf()
ax = plt.gca()
# Plot the data
ax.plot(dat[0], dat[1], 'o', ms=10, color='r')
ax.set_xlim([2, 8])
ax.set_ylim([0, 6])
And here is the code that puts a circle around one of these points and draws an arrow that is shrunk-back at the tip only:
circle_rad = 15 # This is the radius, in points
ax.plot(point[0], point[1], 'o',
ms=circle_rad * 2, mec='b', mfc='none', mew=2)
ax.annotate('Midici', xy=point, xytext=(60, 60),
textcoords='offset points',
color='b', size='large',
arrowprops=dict(
arrowstyle='simple,tail_width=0.3,head_width=0.8,head_length=0.8',
facecolor='b', shrinkB=circle_rad * 1.2)
)
Note here that:
1) I've made the marker face color of the circle transparent with mfc='none', and set the circle size (diameter) to twice the radius.
2) I've shrunk the arrow by 120% of the circle radius so that it backs off of the circle just a bit. Obviously you can play with circle_rad and the value of 1.2 until you get what you want.
3) I've used the "fancy" syntax that defines several of the arrow properties in a string, rather than in the dict. As far as I can tell the shrinkB option is not available if you don't use the fancy arrow syntax.
4) I've used the textcoords='offset points' so that I can specify the position of the text relative to the point, rather than absolute on the axes.
how do I draw a circle around the node 6 as shown in first figure.
You get a center of node #6 (tuple pos). Use this data to set the blue circle position.
to get a nice looking figure, I need manually set the value of xytext many times. Is there a better way?
Make a list of your labels and iterate in it and in tuples of coordinates of nodes to post annotate text. Look to comments of a code.
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from matplotlib.patches import Circle
import matplotlib.patches as patches
import numpy as np
from matplotlib.font_manager import FontProperties
font = FontProperties()
font.set_weight('bold')
font.set_size('medium')
labels = ["Midici","Firenze"]
image = mpimg.imread("g.png") # just a image of your graph
plt.imshow(image)
ax = plt.gca()
# set your own radius and centers of circles in loop, like here
r = 11; c = (157,177)
circ1 = patches.Circle(c,2*r,lw=3.,ec='b',fill=False)
ax.add_artist(circ1)
circ1.set_clip_box(ax.bbox)
# annotate circles
# I have one circle but for your array pos_annotation_node
# you need 'i' to extract proper position
for i,label in enumerate(labels):
annot_array_end = (c[0], c[1]+2*(-1)**i*r)
annot_text_pos = (c[0]+3,c[1]+5*(-1)**i*r)
ax.annotate(label,
xy= annot_array_end,
xytext=annot_text_pos,
color='b',
fontproperties=font,
arrowprops=dict(fc='b', shrink=.005)
)
plt.show()
Just an observation for other people finding this thread. Not everything has to be done in Matplotlib.
It might well be easier to use a drawing package, with your network chart saved as a PDF (or PNG) in the background...

Is there a way to plot a Line2D in points coordinates in Matplotlib in Python?

It is very straight forward to plot a line between two points (x1, y1) and (x2, y2) in Matplotlib using Line2D:
Line2D(xdata=(x1, x2), ydata=(y1, y2))
But in my particular case I have to draw Line2D instances using Points coordinates on top of the regular plots that are all using Data coordinates. Is that possible?
As #tom mentioned, the key is the transform kwarg. If you want an artist's data to be interpreted as being in "pixel" coordinates, specify transform=IdentityTransform().
Using Transforms
Transforms are a key concept in matplotlib. A transform takes coordinates that the artist's data is in and converts them to display coordinates -- in other words, pixels on the screen.
If you haven't already seen it, give the matplotlib transforms tutorial a quick read. I'm going to assume a passing familiarity with the first few paragraphs of that tutorial, so if you're
For example, if we want to draw a line across the entire figure, we'd use something like:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
# The "clip_on" here specifies that we _don't_ want to clip the line
# to the extent of the axes
ax.plot([0, 1], [0, 1], lw=3, color='salmon', clip_on=False,
transform=fig.transFigure)
plt.show()
This line will always extend from the lower-left corner of the figure to the upper right corner, no matter how we interactively resize/zoom/pan the plot.
Drawing in pixels
The most common transforms you'll use are ax.transData, ax.transAxes, and fig.transFigure. However, to draw in points/pixels, you actually want no transform at all. In that case, you'll make a new transform instance that does nothing: the IdentityTransform. This specifies that the data for the artist is in "raw" pixels.
Any time you'd like to plot in "raw" pixels, specify transform=IdentityTransform() to the artist.
If you'd like to work in points, recall that there are 72 points to an inch, and that for matplotlib, fig.dpi controls the number of pixels in an "inch" (it's actually independent of the physical display). Therefore, we can convert points to pixels with a simple formula.
As an example, let's place a marker 30 points from the bottom-left edge of the figure:
import matplotlib.pyplot as plt
from matplotlib.transforms import IdentityTransform
fig, ax = plt.subplots()
points = 30
pixels = fig.dpi * points / 72.0
ax.plot([pixels], [pixels], marker='o', color='lightblue', ms=20,
transform=IdentityTransform(), clip_on=False)
plt.show()
Composing Transforms
One of the more useful things about matplotlib's transforms is that they can be added to create a new transform. This makes it easy to create shifts.
For example, let's plot a line, then add another line shifted by 15 pixels in the x-direction:
import matplotlib.pyplot as plt
from matplotlib.transforms import Affine2D
fig, ax = plt.subplots()
ax.plot(range(10), color='lightblue', lw=4)
ax.plot(range(10), color='gray', lw=4,
transform=ax.transData + Affine2D().translate(15, 0))
plt.show()
A key thing to keep in mind is that the order of the additions matters. If we did Affine2D().translate(15, 0) + ax.transData instead, we'd shift things by 15 data units instead of 15 pixels. The added transforms are "chained" (composed would be a more accurate term) in order.
This also makes it easy to define things like "20 pixels from the right hand side of the figure". For example:
import matplotlib.pyplot as plt
from matplotlib.transforms import Affine2D
fig, ax = plt.subplots()
ax.plot([1, 1], [0, 1], lw=3, clip_on=False, color='salmon',
transform=fig.transFigure + Affine2D().translate(-20, 0))
plt.show()
You can use the transform keyword to change between data coordinate (the default) and axes coordinates. For example:
import matplotlib.pyplot as plt
import matplotlib.lines as lines
plt.plot(range(10),range(10),'ro-')
myline = lines.Line2D((0,0.5,1),(0.5,0.5,0),color='b') # data coords
plt.gca().add_artist(myline)
mynewline = lines.Line2D((0,0.5,1),(0.5,0.5,0),color='g',transform=plt.gca().transAxes) # Axes coordinates
plt.gca().add_artist(mynewline)
plt.show()

How to zoomed a portion of image and insert in the same plot in matplotlib

I would like to zoom a portion of data/image and plot it inside the same figure. It looks something like this figure.
Is it possible to insert a portion of zoomed image inside the same plot. I think it is possible to draw another figure with subplot but it draws two different figures. I also read to add patch to insert rectangle/circle but not sure if it is useful to insert a portion of image into the figure. I basically load data from the text file and plot it using a simple plot commands shown below.
I found one related example from matplotlib image gallery here but not sure how it works. Your help is much appreciated.
from numpy import *
import os
import matplotlib.pyplot as plt
data = loadtxt(os.getcwd()+txtfl[0], skiprows=1)
fig1 = plt.figure()
ax1 = fig1.add_subplot(111)
ax1.semilogx(data[:,1],data[:,2])
plt.show()
Playing with runnable code is one of the
fastest ways to learn Python.
So let's start with the code from the matplotlib example gallery.
Given the comments in the code, it appears the code is broken up into 4 main stanzas.
The first stanza generates some data, the second stanza generates the main plot,
the third and fourth stanzas create the inset axes.
We know how to generate data and plot the main plot, so let's focus on the third stanza:
a = axes([.65, .6, .2, .2], axisbg='y')
n, bins, patches = hist(s, 400, normed=1)
title('Probability')
setp(a, xticks=[], yticks=[])
Copy the example code into a new file, called, say, test.py.
What happens if we change the .65 to .3?
a = axes([.35, .6, .2, .2], axisbg='y')
Run the script:
python test.py
You'll find the "Probability" inset moved to the left.
So the axes function controls the placement of the inset.
If you play some more with the numbers you'll figure out that (.35, .6) is the
location of the lower left corner of the inset, and (.2, .2) is the width and
height of the inset. The numbers go from 0 to 1 and (0,0) is the located at the
lower left corner of the figure.
Okay, now we're cooking. On to the next line we have:
n, bins, patches = hist(s, 400, normed=1)
You might recognize this as the matplotlib command for drawing a histogram, but
if not, changing the number 400 to, say, 10, will produce an image with a much
chunkier histogram, so again by playing with the numbers you'll soon figure out
that this line has something to do with the image inside the inset.
You'll want to call semilogx(data[3:8,1],data[3:8,2]) here.
The line title('Probability')
obviously generates the text above the inset.
Finally we come to setp(a, xticks=[], yticks=[]). There are no numbers to play with,
so what happens if we just comment out the whole line by placing a # at the beginning of the line:
# setp(a, xticks=[], yticks=[])
Rerun the script. Oh! now there are lots of tick marks and tick labels on the inset axes.
Fine. So now we know that setp(a, xticks=[], yticks=[]) removes the tick marks and labels from the axes a.
Now, in theory you have enough information to apply this code to your problem.
But there is one more potential stumbling block: The matplotlib example uses
from pylab import *
whereas you use import matplotlib.pyplot as plt.
The matplotlib FAQ says import matplotlib.pyplot as plt
is the recommended way to use matplotlib when writing scripts, while
from pylab import * is for use in interactive sessions. So you are doing it the right way, (though I would recommend using import numpy as np instead of from numpy import * too).
So how do we convert the matplotlib example to run with import matplotlib.pyplot as plt?
Doing the conversion takes some experience with matplotlib. Generally, you just
add plt. in front of bare names like axes and setp, but sometimes the
function come from numpy, and sometimes the call should come from an axes
object, not from the module plt. It takes experience to know where all these
functions come from. Googling the names of functions along with "matplotlib" can help.
Reading example code can builds experience, but there is no easy shortcut.
So, the converted code becomes
ax2 = plt.axes([.65, .6, .2, .2], axisbg='y')
ax2.semilogx(t[3:8],s[3:8])
plt.setp(ax2, xticks=[], yticks=[])
And you could use it in your code like this:
from numpy import *
import os
import matplotlib.pyplot as plt
data = loadtxt(os.getcwd()+txtfl[0], skiprows=1)
fig1 = plt.figure()
ax1 = fig1.add_subplot(111)
ax1.semilogx(data[:,1],data[:,2])
ax2 = plt.axes([.65, .6, .2, .2], axisbg='y')
ax2.semilogx(data[3:8,1],data[3:8,2])
plt.setp(ax2, xticks=[], yticks=[])
plt.show()
The simplest way is to combine "zoomed_inset_axes" and "mark_inset", whose description and
related examples could be found here:
Overview of AxesGrid toolkit
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
import numpy as np
def get_demo_image():
from matplotlib.cbook import get_sample_data
import numpy as np
f = get_sample_data("axes_grid/bivariate_normal.npy", asfileobj=False)
z = np.load(f)
# z is a numpy array of 15x15
return z, (-3,4,-4,3)
fig, ax = plt.subplots(figsize=[5,4])
# prepare the demo image
Z, extent = get_demo_image()
Z2 = np.zeros([150, 150], dtype="d")
ny, nx = Z.shape
Z2[30:30+ny, 30:30+nx] = Z
# extent = [-3, 4, -4, 3]
ax.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
axins = zoomed_inset_axes(ax, 6, loc=1) # zoom = 6
axins.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
# sub region of the original image
x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
plt.xticks(visible=False)
plt.yticks(visible=False)
# draw a bbox of the region of the inset axes in the parent axes and
# connecting lines between the bbox and the inset axes area
mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
plt.draw()
plt.show()
The nicest way I know of to do this is to use mpl_toolkits.axes_grid1.inset_locator (part of matplotlib).
There is a great example with source code here: https://github.com/NelleV/jhepc/tree/master/2013/entry10
The basic steps to zoom up a portion of a figure with matplotlib
import numpy as np
from matplotlib import pyplot as plt
# Generate the main data
X = np.linspace(-6, 6, 1024)
Y = np.sinc(X)
# Generate data for the zoomed portion
X_detail = np.linspace(-3, 3, 1024)
Y_detail = np.sinc(X_detail)
# plot the main figure
plt.plot(X, Y, c = 'k')
# location for the zoomed portion
sub_axes = plt.axes([.6, .6, .25, .25])
# plot the zoomed portion
sub_axes.plot(X_detail, Y_detail, c = 'k')
# insert the zoomed figure
# plt.setp(sub_axes)
plt.show()

Categories

Resources