Consider that you have three artists in a matplotlib plot. What is the easiest way to not show the intermediate-level artist in the bbox of the top-level artist while retaining the low-level artist visible throughout the whole area?
Illustration of what I want to achieve:
It there was no requirement of being able to see the lowest level a non-transparent facecolor of the toplevel plot would be enough. This does not work with the three levels as then the both of the lower levels would be concealed.
See this IPython notebook for a solution using shapely. Here is a not yet completely functional pure matplotlib example, but I'm hoping there is a simpler way of getting the same result that I have not thought of yet.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import patches, cm
from matplotlib.path import Path
fig, ax = plt.subplots()
imdata = np.random.randn(10, 10)
ax.imshow(imdata, extent=(0, 1, 0, 1), aspect='auto', cmap=cm.coolwarm)
text = ax.text(0.5, 0.5, 'Text', fontsize='xx-large', fontweight='bold',
color='k', ha='center', va='center')
renderer = fig.canvas.get_renderer()
bbox = text.get_window_extent(renderer).transformed(ax.transData.inverted())
bboxrect = patches.Rectangle((bbox.x0, bbox.y0), bbox.width, bbox.height)
bbpath = bboxrect.get_path().transformed(bboxrect.get_patch_transform())
patch = patches.Rectangle((0.3, 0.3), 0.4, 0.4)
path = patch.get_path().transformed(patch.get_patch_transform())
path = Path.make_compound_path(path, bbpath)
patch = patches.PathPatch(path, facecolor='none', hatch=r'//')
ax.add_patch(patch)
I came up with another answer that's a bit cleaner: it involves creating a clip mask for the hatched region that has a hole in it, so that you can see everything in the background behind it.
from matplotlib.path import Path
from matplotlib.patches import PathPatch
def DoubleRect(xy1, width1, height1,
xy2, width2, height2, **kwargs):
base = np.array([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
verts = np.vstack([xy1 + (width1, height1) * base,
xy2 + (width2, height2) * base[::-1],
xy1])
codes = 2 * ([Path.MOVETO] + 4 * [Path.LINETO]) + [Path.CLOSEPOLY]
return PathPatch(Path(verts, codes), **kwargs)
fig, ax = plt.subplots()
imdata = np.random.randn(10, 10)
# plot the image
im = ax.imshow(imdata, extent=(0, 1, 0, 1), aspect='auto',
cmap='coolwarm', interpolation='nearest')
# plot the hatched rectangle
patch = plt.Rectangle((0.3, 0.3), 0.4, 0.4, facecolor='none',
hatch=r'//')
ax.add_patch(patch)
# add the text
text = ax.text(0.5, 0.5, 'Text', fontsize='xx-large', fontweight='bold',
color='k', ha='center', va='center')
# create a mask for the hatched rectangle
mask = DoubleRect((0, 0), 1, 1, (0.4, 0.45), 0.2, 0.1,
facecolor='none', edgecolor='black')
ax.add_patch(mask)
patch.set_clip_path(mask)
It's a bit of a hack, but I would probably accomplish this by showing the image twice, once in the background, and once with a custom clip path in the foreground. Here's an example:
fig, ax = plt.subplots()
imdata = np.random.randn(10, 10)
# plot the background image
im = ax.imshow(imdata, extent=(0, 1, 0, 1), aspect='auto',
cmap=cm.coolwarm, zorder=1)
# plot the hatched rectangle
patch = patches.Rectangle((0.3, 0.3), 0.4, 0.4, facecolor='none',
hatch=r'//', zorder=2)
ax.add_patch(patch)
# plot the box around the text
minirect = patches.Rectangle((0.4, 0.45), 0.2, 0.1, facecolor='none',
edgecolor='black', zorder=4)
ax.add_patch(minirect)
# duplicate image and set a clip path
im2 = ax.imshow(imdata, extent=(0, 1, 0, 1), aspect='auto',
cmap=cm.coolwarm, zorder=3)
im2.set_clip_path(minirect)
# add the text on top
text = ax.text(0.5, 0.5, 'Text', fontsize='xx-large', fontweight='bold',
color='k', ha='center', va='center', zorder=5)
Related
I have the following issue.
I have a graph of which has colored segments. The problem is in relating those segments to the color bar (which also contains text), so that each color segment is aligned with the color bar.
The code is the following:
from matplotlib.colorbar import colorbar_factory
x_v = datosg["Hour"]+div
y_v = datosg["UV Index"]
fig, ax= plt.subplots(figsize = (7,7))
ax.plot(x_v, y_v, color = "green")
ax.set_xlim(7, 19)
ax.grid()
ax.axhspan(0, 2.5, facecolor='green', alpha=0.8)
ax.axhspan(2.5, 5.5, facecolor='blue', alpha=0.7)
ax.axhspan(5.5, 7.5, facecolor='red', alpha=0.7)
ax.axhspan(7.5, 10.5, facecolor='yellow', alpha=0.7)
ax.axhspan(10.5, 16, facecolor='pink', alpha=0.7)
ax.margins(0)
from matplotlib.colors import ListedColormap
#discrete color scheme
cMap = ListedColormap(['green', 'blue','red', 'yellow', 'pink'])
#data
np.random.seed(42)
data = np.random.rand(5, 5)
heatmap = ax.pcolor(data, cmap=cMap)
#legend
cbar_ay = fig.add_axes([0.93, 0.125, 0.2, 0.755])
cbar = plt.colorbar(heatmap, cax=cbar_ay, orientation="vertical")
cbar.ax.get_yaxis().set_ticks([])
for j, lab in enumerate(['$Bajo$','$Medio$','$Alto$','$Muy Alto$','$Extremo$']):
cbar.ax.text(.5, (2 * j + 1) / 10.0, lab, ha='center', va='center')
plt.show()
The graph that results from this code is as follows:
Result_code
I have tried everything, the result I expect is very similar to this graph:
resulting image
But I can't change the range of the colors in the color bar.
Also note that I created random values in order to create the colorbar, I couldn't think of any other way, however so far it has worked. I only have to modify the range, so that it is similar to the last graph.
Any help would be appreciated.
I guess it's much easier to just draw a second Axes and fill it with axhspans the same way you did it with the main Axes, but if you want to use a colorbar, you can do it as follows:
import itertools
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
colors = ['green', 'blue','red', 'yellow', 'pink']
labels = ['$Bajo$','$Medio$','$Alto$','$Muy Alto$','$Extremo$']
bounds = np.array([0, 2.5, 5.5, 7.5, 10.5, 16 ])
fig, ax= plt.subplots()
for span, color in zip(itertools.pairwise(bounds), colors):
ax.axhspan(*span, facecolor=color, alpha=0.8)
ax.margins(0)
cmap = mpl.colors.ListedColormap(colors)
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
ax_pos = ax.get_position().bounds
cbar_ay = fig.add_axes([0.93, ax_pos[1], 0.2, ax_pos[3]])
cbar = plt.colorbar(mpl.cm.ScalarMappable(cmap=cmap, norm=norm), cax=cbar_ay, orientation="vertical", spacing='proportional')
cbar.ax.set_axis_off()
for y, lab in zip(bounds[:-1] + np.diff(bounds) / 2, labels):
cbar.ax.text(.5, y, lab, ha='center', va='center')
I have a plot with both a colorbar and a legend. I want to place the legend outside of the plot to the right of the colorbar. To accomplish this, I use bbox_to_anchor argument, but this causes the legend to get cut off:
import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import norm
_, ax = plt.subplots()
extent = np.r_[0, 1, 0, 1]
space = np.linspace(0, 1)
probs = np.array([[norm.cdf(x + y) for x in space] for y in space])
colormap = ax.imshow(probs, aspect="auto", origin="lower", extent=extent, alpha=0.5)
colorbar = plt.colorbar(colormap, ax=ax)
colorbar.set_label(f"Probability")
ax.scatter(
[0.2, 0.4, 0.6], [0.8, 0.6, 0.4], color="r", label="Labeled Points",
)
plt.legend(loc="center left", bbox_to_anchor=(1.3, 0.5))
plt.title
plt.show()
Plot with legend cut off
To fix the legend, I insert a call to plt.tight_layout() before plt.show(), but this causes the aspect ratio to get distorted:
Plot with distorted aspect ratio
How can I show the entire legend and preserve the aspect ratio of the axes?
You can manage the ratio between axis height and width with matplotlib.axes.Axes.set_aspect. Since you want them to be equal:
ax.set_aspect(1)
Then you can use matplotlib.pyplot.tight_layout to fit the legend within the figure.
If you want to adjust margins too, you can use matplotlib.pyplot.subplots_adjust.
Complete Code
import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import norm
_, ax = plt.subplots()
extent = np.r_[0, 1, 0, 1]
space = np.linspace(0, 1)
probs = np.array([[norm.cdf(x + y) for x in space] for y in space])
colormap = ax.imshow(probs, aspect="auto", origin="lower", extent=extent, alpha=0.5)
colorbar = plt.colorbar(colormap, ax=ax)
colorbar.set_label(f"Probability")
ax.scatter([0.2, 0.4, 0.6], [0.8, 0.6, 0.4], color="r", label="Labeled Points",)
plt.legend(loc="center left", bbox_to_anchor=(1.3, 0.5))
ax.set_aspect(1)
plt.tight_layout()
plt.subplots_adjust(left = 0.1)
plt.show()
I would like to draw annotation images in a matplotlib plot, and be able to move them after plotting.
To that end, I started from the demo:
https://matplotlib.org/3.2.1/gallery/text_labels_and_annotations/demo_annotation_box.html
with the notebook backend:
%matplotlib notebook
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Circle
from matplotlib.offsetbox import (TextArea, DrawingArea, OffsetImage,
AnnotationBbox)
from matplotlib.cbook import get_sample_data
fig, ax = plt.subplots()
# Define a 1st position to annotate (display it with a marker)
xy = (0.5, 0.7)
ax.plot(xy[0], xy[1], ".r")
# Annotate the 1st position with a text box ('Test 1')
offsetbox = TextArea("Test 1", minimumdescent=False)
ab = AnnotationBbox(offsetbox, xy,
xybox=(-20, 40),
xycoords='data',
boxcoords="offset points",
arrowprops=dict(arrowstyle="->"))
ax.add_artist(ab)
# Annotate the 1st position with another text box ('Test')
offsetbox = TextArea("Test", minimumdescent=False)
ab = AnnotationBbox(offsetbox, xy,
xybox=(1.02, xy[1]),
xycoords='data',
boxcoords=("axes fraction", "data"),
box_alignment=(0., 0.5),
arrowprops=dict(arrowstyle="->"))
ax.add_artist(ab)
# Define a 2nd position to annotate (don't display with a marker this time)
xy = [0.3, 0.55]
# Annotate the 2nd position with a circle patch
da = DrawingArea(20, 20, 0, 0)
p = Circle((10, 10), 10)
da.add_artist(p)
ab = AnnotationBbox(da, xy,
xybox=(1.02, xy[1]),
xycoords='data',
boxcoords=("axes fraction", "data"),
box_alignment=(0., 0.5),
arrowprops=dict(arrowstyle="->"))
ax.add_artist(ab)
# Annotate the 2nd position with an image (a generated array of pixels)
arr = np.arange(100).reshape((10, 10))
im = OffsetImage(arr, zoom=2)
im.image.axes = ax
ab = AnnotationBbox(im, xy,
xybox=(-50., 50.),
xycoords='data',
boxcoords="offset points",
pad=0.3,
arrowprops=dict(arrowstyle="->"))
ax.add_artist(ab)
# Annotate the 2nd position with another image (a Grace Hopper portrait)
with get_sample_data("grace_hopper.png") as file:
arr_img = plt.imread(file, format='png')
imagebox = OffsetImage(arr_img, zoom=0.2)
imagebox.image.axes = ax
ab = AnnotationBbox(imagebox, xy,
xybox=(120., -80.),
xycoords='data',
boxcoords="offset points",
pad=0.5,
arrowprops=dict(
arrowstyle="->",
connectionstyle="angle,angleA=0,angleB=90,rad=3")
)
ax.add_artist(ab)
# Fix the display limits to see everything
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
plt.show()
To test how to move stuff, I execute in the next cell:
rnd = fig.canvas.renderer
print(rnd)
print(im.get_window_extent(rnd))
print(im.get_window_extent(rnd).get_points())
print(ab.xybox)
ab.xybox = (130, -70)
im._offset = (135.46666666666667, 301.6355555555555)
im.set_zoom(.5)
ab.update_positions(rnd)
print(ab.xybox)
print(im.get_window_extent(rnd).get_points())
ab.draw(rnd)
im.draw(rnd)
fig.draw(rnd)
ax.plot([0, 1], [0, 1])
plt.show()
fig.canvas.draw_idle()
and what I see is that I do get a break in the plotted line, showing that the annotationbbox is shifted, but all its content isn't. (here the line has been plotted first (blue), then all the shifting has been executed, with another line plot (orange))
Any suggestions on how I can actually move the image of Grace Hopper are highly appreciated.
This is not a direct answer to your question, but I was wondering a similar thing (and the matplotlib documentation is lacking), so I hacked up this example, which appears to work. The key thing seems to be using fig.canvas.draw() after every change. Also, I'm using matplotlib version 3.1.3
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
def onpick(evt):
x = (evt.xdata, evt.ydata)
ab.xybox = x
print(ab.xybox)
fig.canvas.draw()
pix = np.linspace(0, 1, 20)
x, y = np.meshgrid(pix, pix)
p = np.cos(2*np.pi*x)*np.sin(8*np.pi*y)
fig = plt.figure()
ax = fig.add_subplot(111)
im = OffsetImage(p, zoom=1, cmap = 'gray')
ab = AnnotationBbox(im, (0.5, 0.5), xycoords='data', frameon=False)
ax.add_artist(ab)
fig.canvas.mpl_connect('button_press_event', onpick)
plt.show()
I have created a figure that displays a shape and table using matplotlib. The problem is how its produced. They overlap each other. The shape is to scale so I don't want to alter it. I was wondering how I can alter the overall size of the plot or move the position of the table.
import matplotlib.pyplot as plt
import matplotlib as mpl
fig, ax = plt.subplots(figsize = (10,6))
ax.axis('equal')
plt.style.use('ggplot')
ax.grid(False)
xy = 0,0
circle = mpl.patches.Circle(xy, 160, lw = 3, edgecolor = 'black', color = 'b', alpha = 0.1, zorder = 5)
ax.add_patch(circle)
col_labels=['A','B','C','D','E']
row_labels=['diff','total']
table_vals=[['','','','',''],['','','','','']]
the_table = plt.table(cellText=table_vals,
colWidths = [0.05]*5,
rowLabels=row_labels,
colLabels=col_labels,
bbox = [0.8, 0.4, 0.2, 0.2])
ax.autoscale()
plt.show()
Add the bbox argument with your table. (instead of loc)
the_table = plt.table(cellText=table_vals,
colWidths = [0.05]*5,
rowLabels=row_labels,
colLabels=col_labels,
bbox = [0.2, 0.4, 0.4, 0.02])
The bbox argument takes 4 inputs: X, Y, Width, Height. Thus X and Y are the coordinates of the bottom left corner. Above, the height was far too small.
EDIT: Create room to play with
The idea is to make the ax smaller in the same manner.
box = ax.get_position()
a.set_position([box.x0, box.y0, box.width * 0.9, box.height])
EDIT 2: Trying to put the table on the right. As I said, you need to play with the box values, took me about 10 tries to get this. I'm using spyder as an IDE, so it's really fast.
import matplotlib.pyplot as plt
import matplotlib as mpl
fig, ax = plt.subplots(figsize = (10,6))
ax.axis('equal')
plt.style.use('ggplot')
ax.grid(False)
xy = 0,0
circle = mpl.patches.Circle(xy, 160, lw = 3, edgecolor = 'black', color = 'b', alpha = 0.1, zorder = 5)
ax.add_patch(circle)
col_labels=['A','B','C','D','E']
row_labels=['diff','total']
table_vals=[['','','','',''],['','','','','']]
the_table = plt.table(cellText=table_vals,
colWidths = [0.05]*5,
rowLabels=row_labels,
colLabels=col_labels,
bbox = [1.1, 0.5, 0.35, 0.1])
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
ax.autoscale()
plt.show()
Output:
Position the table outside the axes
You may use loc="right" to position the table right of the axes. Something like fig.subplots_adjust(right=0.8) will leave enough space for it.
import matplotlib.pyplot as plt
import matplotlib as mpl
plt.style.use('ggplot')
fig, ax = plt.subplots(figsize = (10,6))
fig.subplots_adjust(right=0.8)
ax.axis('equal')
ax.grid(False)
xy = 0,0
circle = mpl.patches.Circle(xy, 160, lw = 3, edgecolor = 'black',
facecolor = 'b', alpha = 0.1, zorder = 5)
ax.add_patch(circle)
col_labels=['A','B','C','D','E']
row_labels=['diff','total']
table_vals=[['','','','',''],['','','','','']]
the_table = plt.table(cellText=table_vals,
colWidths = [0.05]*5,
rowLabels=row_labels,
colLabels=col_labels,
loc='right', zorder=3)
ax.autoscale()
plt.show()
Put the table in its own axes
You may put the table in a new axes next to the existing one. The advantage is that there is no need to then play with the column width or subplot parameters.
import matplotlib.pyplot as plt
import matplotlib as mpl
plt.style.use('ggplot')
fig, (ax, ax_table) = plt.subplots(ncols=2, figsize = (10,6),
gridspec_kw=dict(width_ratios=[3,1]))
ax.axis('equal')
ax_table.axis("off")
ax.grid(False)
xy = 0,0
circle = mpl.patches.Circle(xy, 160, lw = 3, edgecolor = 'black',
facecolor = 'b', alpha = 0.1, zorder = 5)
ax.add_patch(circle)
col_labels=['A','B','C','D','E']
row_labels=['diff','total']
table_vals=[['','','','',''],['','','','','']]
the_table = ax_table.table(cellText=table_vals,
rowLabels=row_labels,
colLabels=col_labels,
loc='center')
ax.autoscale()
plt.show()
I have a code that draws hundreds of small rectangles on top of an image :
The rectangles are instances of
matplotlib.patches.Rectangle
I'd like to put a text (actually a number) into these rectangles, I don't see a way to do that. matplotlib.text.Text seems to allow one to insert text surrounded by a rectangle however I want the rectangle to be at a precise position and have a precise size and I don't think that can be done with text().
I think you need to use the annotate method of your axes object.
You can use properties of the rectangle to be smart about it. Here's a toy example:
import matplotlib.pyplot as plt
import matplotlib.patches as mpatch
fig, ax = plt.subplots()
rectangles = {'skinny' : mpatch.Rectangle((2,2), 8, 2),
'square' : mpatch.Rectangle((4,6), 6, 6)}
for r in rectangles:
ax.add_artist(rectangles[r])
rx, ry = rectangles[r].get_xy()
cx = rx + rectangles[r].get_width()/2.0
cy = ry + rectangles[r].get_height()/2.0
ax.annotate(r, (cx, cy), color='w', weight='bold',
fontsize=6, ha='center', va='center')
ax.set_xlim((0, 15))
ax.set_ylim((0, 15))
ax.set_aspect('equal')
plt.show()
This is an example.
import matplotlib.pyplot as plt
left, width = .25, .25
bottom, height = .25, .25
right = left + width
top = bottom + height
fig, ax = plt.subplots(figsize=(10, 10),sharex=True, sharey=True)
fig.patches.extend([plt.Rectangle((left, bottom), width, height,
facecolor='none',
edgecolor='red',
#fill=True,
#color='black',
#alpha=0.5,
#zorder=1000,
lw=5,
transform=fig.transFigure, figure=fig)])
fig.text(0.5*(left+right), 0.5*(bottom+top), 'Your text',
horizontalalignment='center',
verticalalignment='center',
fontsize=20, color='black',
transform=fig.transFigure)