I'm having trouble drawing rectangles in matplotlib using Patches. When linewidth is supplied to patches.Rectangle, the border is drawn on the outside of the rectangle. Here's an example:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig, ax = plt.subplots(1)
rect = patches.Rectangle((1, 1), 1, 1, facecolor = 'blue')
rect2 = patches.Rectangle((1, 2.1), 1, 1, facecolor = 'none', edgecolor = 'black', linewidth = 6)
ax.add_patch(rect)
ax.add_patch(rect2)
ax.set_xlim([0, 3.5])
ax.set_ylim([0, 3.5])
here's the result:
Note that the border is drawn on the outside of the box such that the box + border now exceeds the size of the blue box. I would like the border to be drawn inside the box, such that it is always the same size as the blue box regardless of linewidth.
I've tried this in two different ways and neither was satisfying:
Convert the linewidth from absolute units into units of the data, then calculate a smaller box that could be drawn with a normal border that matches the other box.
Explore some of the offsetbox functionality as in this post, though I didn't get very far as I couldn't figure out how to specify pad correctly.
Any help would be appreciated!
The easiest approach is to set a clip rectangle that hides everything outside the rectangle. As you already have a rectangle, it can be used to clip itself.
As the border is drawn centered on the border line, half of it will be clipped away. This can be tackled by setting the width to double the desired width.
Note that for clipping to work as desired, the rectangle already needs to be transformed to axes coordinates. So, first add the rectangle patch to the ax and only then use is to set the clipping.
Also note that with default parameters, a rectangle uses the same color for the inside as well as for a thin border. Setting the linewidth to zero ensures that it doesn't draw outside the rectangle.
Similarly, ellipses can be drawn with the line only at the inside.
The code below uses a thickness of 10 and some extra dotted red lines to illustrate what's happening.
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig, ax = plt.subplots()
pad = 0.1 # distance between the rectangles
for i in range(3):
for j in range(2):
x = .5 + i * (1 + pad)
y = .5 + j * (1 + pad)
if i == j:
patch = patches.Rectangle((x, y), 1, 1, facecolor='blue', linewidth=0)
elif i < 2:
patch = patches.Rectangle((x, y), 1, 1, facecolor='none', edgecolor='black',
linewidth=10*2 if j == 0 else 10)
else:
patch = patches.Ellipse((x+0.5, y+0.5), 1, 1, facecolor='none', edgecolor='black',
linewidth=10*2 if j == 0 else 10)
ax.add_patch(patch)
if j == 0:
patch.set_clip_path(patch)
for i in range(3):
x = .5 + i * (1 + pad)
for s in 0,1:
ax.axvline (x+s, color='crimson', ls=':', lw=1)
for j in range(2):
y = .5 + j * (1 + pad)
for s in 0,1:
ax.axhline (y+s, color='crimson', ls=':', lw=1)
ax.set_xlim([0, 4.0])
ax.set_ylim([0, 3.0])
ax.set_aspect('equal')
plt.show()
The image below shows the standard way of drawing at the top, and clipping with double linewidth at the bottom.
Related
currently I am having trouble properly rotating a marker using the plot function on matplotlib. I am using the marker as an arrow and would like the line it is on to go through the tip of the arrow. Currently this works for any rotation that is a multiple of 90 degrees.
The current code I have is this.
import matplotlib.pyplot as plt
from matplotlib.markers import MarkerStyle
from math import sqrt, atan, pi
point_1 = [1,1]
point_2 = [1,2]
mid_point = ((point_1[0] + point_2[0]) / 2, (point_1[1] + point_2[1]) / 2)
plt.plot(mid_point[0], mid_point[1], marker=(3, 0, 0), markersize=20, linestyle='None')
plt.plot([point_1[0], point_2[0]], [point_1[1], point_2[1]], linewidth=1, linestyle='-')
point_1 = [1,1]
point_2 = [2,2]
mid_point = ((point_1[0] + point_2[0]) / 2, (point_1[1] + point_2[1]) / 2)
plt.plot(mid_point[0], mid_point[1], marker=(3, 0, -45), markersize=20, linestyle='None')
plt.plot([point_1[0], point_2[0]], [point_1[1], point_2[1]], linewidth=1, linestyle='-')
point_1 = [2,2]
point_2 = [3,2]
mid_point = ((point_1[0] + point_2[0]) / 2, (point_1[1] + point_2[1]) / 2)
plt.plot(mid_point[0], mid_point[1], marker=(3, 0, -90), markersize=20, linestyle='None')
plt.plot([point_1[0], point_2[0]], [point_1[1], point_2[1]], linewidth=1, linestyle='-')
plt.xlim([0,4])
plt.ylim([0,4])
plt.show()
which produces the following figure.
As you can see, the arrow in the middle should be centered at the line it is on at 45 degrees but it is not. I have also tested this at multiple angles and it seems that the closer the rotation gets to 45 degrees, the more the arrow deviates from the line. This the same for all angles in between multiples of 90 degrees.
Is there a way to fix this. I believe this is due to the center of rotation of the markers, but I am not sure how to change it.
The problem is that a line with the slope of 1 is not generally at a 45 degree angle. If you make the axes the same range and square, then the angles match:
plt.figure(figsize=(6,6))
You can also set the angle based on the angle of the plotted line (when the axes aren't square), but you need to decide what you want to hold constant.
I'd like to emulate the blueish background lines from the given image. Can someone tell me how to do so using Matplotlib?
Basically, it's a less complicated version of this.
Here is an approach using a stretched 1D image to draw the bands. The image gets an extent to fill the complete width and the desired height. The vmax for the color mapping is set a bit higher to avoid too light colors, which would be too similar to the white background.
import matplotlib.pyplot as plt
import numpy as np
# create some test data
N = 40
x = np.linspace(-3, 3, N)
values = np.random.normal(0, 2, N)
error_bands = np.array([2, 1, 0, 0, 1, 2])
plt.imshow(error_bands.reshape(-1, 1), extent=[-10, 10, -3, 3], origin='lower',
cmap='Blues_r', vmin=error_bands.min(), vmax=error_bands.max() * 1.4, alpha=0.3)
plt.axhline(0, color='blueviolet', lw=3) # horizontal line at x=0
plt.bar(x, values, width=(x[1] - x[0]) * 0.8, bottom=0, color='blueviolet')
plt.gca().set_aspect('auto') # removed the fixed aspect ratio forced by imshow
plt.xlim(x[0] - 0.3, x[-1] + 0.3) # explicitly set the xlims, shorter than the extent of imshow
plt.show()
PS: A simpler approach, replacing imshow with some calls to axhspan drawing horizontal bars over eachother, using a small alpha:
for i in range(1, 4):
plt.axhspan(-i, i, color='b', alpha=0.1)
I wonder if it is possible with corrplot (from the biokit package) to have a colobar with patterns.
In this example below, for the colobar, I would like to have 5 bubbles with differents sizes associated to the matrix's values ([-1, -0.5, 0, +0.5, +1]).
Ideas ?
thanks a lot
import numpy as np
import pandas as pd
from biokit.viz import corrplot
from matplotlib import pyplot as plt
import string
letters = string.ascii_uppercase[0:15]
df = pd.DataFrame(dict(( (k, np.random.random(10)+ord(k)-65) for k in letters)))
df = df.corr()
c = corrplot.Corrplot(df)
c.plot(colorbar=True, upper='circle', rotation=60, cmap='Oranges', fontsize=12)
plt.tight_layout()
plt.show()
Adding filled shapes to a colorbar seems not to be part of the standard interface. However, a legend could serve your goals. Some shapes are supported directly by the legend, others need a special handler, as described in this post.
It seems first some circles need to be created, for which color etc. can be set.
To tell the handler which shape exactly is meant, I'm misusing the label parameter.
From the source of biokit's corrplot, we learn that the ellipse is rotated either + or -45°, and that it is scaled by the absolute value of the correlation.
The following code puts everything together. The colorbar is drawn as a reference, but can be left out once everything is checked. The legend is positioned outside the main plot via bbox_to_anchor=(x, y). These coordinates are in axes coordinates. The ideal location depends on the size of the other elements, so some experimentation could be useful. I didn't draw the corrplot itself, as I don't have it installed, but you can replace the dummy scatter plot with it.
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Circle
import matplotlib as mpl
class HandlerEllipse(mpl.legend_handler.HandlerPatch):
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height, fontsize, trans):
d = float(orig_handle.get_label())
center = 0.5 * width - 0.5 * xdescent, 0.5 * height - 0.5 * ydescent
radius = (height + ydescent) * 1.8
p = Ellipse(xy=center, width=radius, height=radius * (1 - abs(d)), angle=45 if d > 0 else -45)
self.update_prop(p, orig_handle, legend)
p.set_transform(trans)
return [p]
#values = [1, 0.5, 0, -0.5, -1]
values = [1, 0.75, 0.5, 0.25, 0, -0.25, -0.5, -0.75, -1]
cmap = plt.cm.Oranges # or plt.cm.PiYG
norm = mpl.colors.Normalize(vmin=-1, vmax=1)
fig, ax = plt.subplots()
plt.scatter([0,1], [0,1], c=[-1,1], cmap=cmap, norm=norm) # a dummy plot as a stand-in for the corrplot
char = plt.colorbar(ticks=values)
shapes = [Circle((0.5, 0.5), 1, facecolor=cmap(norm(d)), edgecolor='k', alpha=1, zorder=2, linewidth=1, label=d)
for d in values]
plt.legend(handles=shapes, labels=values, title='Correlation', framealpha=1,
bbox_to_anchor=(1.25, 1), loc='upper left',
handler_map={Circle: HandlerEllipse()})
plt.tight_layout() # make sure legend and colorbar fit nicely in the plot
plt.show()
I'm fairly new to matplotlib and python in general and what I'm trying to do is rather basic. However, even after quite some googling time I cannot find a solution for this:
Here is the problem:
I'd like to draw a circle with different color of the border and the face, i.e. set edgecolor and facecolor differently. I'd also like to have an alpha-channel, i.e. alpha=0.5. Now while all of this works fine, the resulting circle does not have a single border color, but 2 borders are drawn. One, the outer, in the color I specified for edgecolor and the other in a color I assume to be a combination between the edgecolor and the facecolor.
Here is my code:
from matplotlib import pyplot as plt
point = (1.0, 1.0)
c = plt.Circle(point, 1, facecolor='green', edgecolor='orange', linewidth=15.0, alpha=0.5)
fig, ax = plt.subplots()
ax.add_artist(c)
plt.show()
And here is an illustration:
Ok, this might be a minor thing, but this 2nd border drives me nuts!
Am I doing something wrong? Is that just the way it is? Any help would be much appreciated.
To some extent, yes, this is just the way it is.
The point is that the edge is literally drawn on the edge of the circle.
This means that half of the edge width is drawn on top of the face of the circle and the other half outside. If now you set alpha<1.0 you see, as you concluded correctly, an overlap of the facecolor and the edgecolor.
However, you can get rid of that 'extra border'. Below are 2 ways how to do so which one works best for you depends on what exactly you want to do.
1st Suggestion:
The simplest, IMHO, is to only set alpha for the facecolor. This can be done by setting the alpha channel for the facecolor directly and omit the alpha argument in the call of Circle. You can set the alpha-channel with the colorConverter:
from matplotlib.colors import colorConverter
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 8))
ax.axis('equal')
ax.set_xlim(0,1)
ax.set_ylim(0,1)
# Here you set alpha for the faceolor
fc = colorConverter.to_rgba('green', alpha=0.5)
point = (1.0, 1.0)
r = 1.0
lw = 15.0
ec = 'orange'
# NOTE: do not set alpha when calling Circle!
c = plt.Circle(point, r,fc=fc, ec=ec, lw=lw)
ax.add_artist(c)
plt.show()
2nd Suggestion
A more elaborated option is to 'wipe' the edge of the circle with a white edge after only plotting the face, then only plot the edge.
With this approach both colours appear with the alpha channel. But note that also in this case any object that lies 'under' the edge will be completely masked by the edge:
import matplotlib.pyplot as plt
from matplotlib.collections import PatchCollection
point = (1.0, 1.0)
r = 1.0
alpha = 0.5
lw = 15.0
fc = 'green'
ec = 'orange'
# First only draw the facecolor
c_face = plt.Circle(point, r, alpha=alpha, fc=fc, lw=0.0)
# Draw a non-transparent white edge to wipe the facecolor where they overlap
c_wipe = plt.Circle(point, r, alpha=1.0, ec='white', fc='none', lw=lw)
# Now draw only the edge
c_edge = plt.Circle(point, r, alpha=alpha, fc='none', ec=ec, lw=lw)
circle_patch = PatchCollection([c_face, c_wipe, c_edge], match_original=True)
fig, ax = plt.subplots(figsize=(8, 8))
ax.axis('equal')
ax.set_xlim(0,1)
ax.set_ylim(0,1)
ax.add_artist(circle_patch)
plt.show()
Here is a gist that deals with this issue following the 2nd suggestion. Simply download the mod_patch.py file and off you go.
Here is how it can be used:
import matplotlib.pyplot as plt
from mod_patch import get_Patch
fig, ax = plt.subplots(figsize=(8,8))
c = get_Patch(plt.Circle, (0,0.5), 0.5, facecolor='green', edgecolor='orange', alpha=0.5, lw=15)
ax.add_artist(c)
r = get_Patch(plt.Rectangle, (0.5,0), 0.5, 0.5, facecolor='green', edgecolor='orange', alpha=0.5, lw=15)
ax.add_artist(r)
plt.show()
For completeness, here the definition of get_Patch:
from matplotlib.collections import PatchCollection
def get_Patch(a_Patch, *args, **kwargs):
background_color = kwargs.pop(
'bgc',
kwargs.pop('background_color', 'white')
)
alpha = kwargs.get('alpha', 1.0)
patches = []
lw = kwargs.get('lw', kwargs.get('linewidth', 0.0))
if alpha < 1.0 and lw:
color = kwargs.get('c', kwargs.get('color', None))
fc = kwargs.get('facecolor', kwargs.get('fc', None))
ec = kwargs.get('edgecolor', kwargs.get('ec', None))
face_kwargs = dict(kwargs)
face_kwargs['fc'] = fc if fc is not None else color
face_kwargs['lw'] = 0.0
p_face = a_Patch(*args, **face_kwargs)
patches.append(p_face)
wipe_kwargs = dict(kwargs)
wipe_kwargs['fc'] = 'none'
wipe_kwargs['ec'] = background_color
wipe_kwargs['alpha'] = 1.0
p_wipe = a_Patch(*args, **wipe_kwargs)
patches.append(p_wipe)
edge_kwargs = dict(kwargs)
edge_kwargs['fc'] = 'none'
edge_kwargs['ec'] = ec if ec is not None else color
p_edge = a_Patch(*args, **edge_kwargs)
patches.append(p_edge)
else:
p_simple = a_Patch(*args, **kwargs)
patches.append(p_simple)
return PatchCollection(patches, match_original=True)
I am stuck making the visualization I want. I cannot yet put images so the link is below. I almost have what I want. The issue is the labels are not correctly placed.
inverted-polar-bar-demo
I would like to have the labels be rotated like they are, but have the labels' right edges aligned to just inside the outer edge of the circle.
EDIT To clarify:
The labels I used for this example are all 'testing'. With actual data, these labels will be of different length. I want to have the end of the labels moved so that they always have their last letter next to the outside edge of the circle. So in this case, all the 'g's would be next to the outside edge.
import matplotlib.pyplot as mpl
import numpy as np
import random
bgcolor = '#222222'
barcolor = '#6699cc'
bottom = 15
N = 32
Values = np.random.random(N)*10
MetricLabels = ['testing' for _ in range(1, N+1)]
# Select the radii, thetas, and widths.
Radii = -5*np.ones(N)-Values
Theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False)
width = 2*np.pi/N
# Make a list of shifted thetas to place the labels at.
ThetaShifted = np.copy(Theta)
for i in range(N-1):
ThetaShifted[i] = (Theta[i] + Theta[i+1])/2.0
ThetaShifted[-1] = (Theta[-1] + 2.0*np.pi)/2.0
# Make the figure
fig = mpl.figure()
ax = fig.add_subplot(111, projection='polar')
bars = ax.bar(Theta, Radii, width=width, bottom=bottom)
# Set the outer ring to be invisible.
ax.spines["polar"].set_visible(False)
# Set the grid line locations but set the labels to be invisible.
ax.grid(False)
ax.set_thetagrids([], visible=False)
ax.set_rgrids([3], visible=False)
# Apply colors to bars based on the settings above.
for v, bar in zip(Values, bars):
bar.set_facecolor(barcolor)
bar.set_edgecolor(bar.get_facecolor())
# Show the metric and value labels
for counter in range(N):
ax.text(ThetaShifted[counter], bottom-3, MetricLabels[counter],
horizontalalignment='center', verticalalignment='baseline',
rotation=(counter+.5)*360/N, color=bgcolor)
ax.text(ThetaShifted[counter], bottom+0.75, np.round(Values[counter],2),
horizontalalignment='center', verticalalignment='center',
color=bars[counter].get_facecolor())
# Set the background color to be a dark grey,
ax.set_axis_bgcolor(bgcolor)
fig.set_facecolor(bgcolor)
# Show the figure.
mpl.show()
I actually solved my issue. See image and code below. The main thing to solve it was to use the monospace font family and to use rjust to create the label strings to be fixed length and right justified from the beginning. After that, it is just a matter of choosing the correct radial location for each label which should be much easier when they are all the same number of characters.
import matplotlib.pyplot as mpl
import numpy as np
import random
bgcolor = '#222222'
barcolor = '#6699cc'
bottom = 15
N = 32
Values = np.random.random(N)*10
MetricLabels = [('A'*(4+int(8*random.random()))).rjust(10) for _ in range(1, N+1)]
# Select the radii, thetas, and widths.
Radii = -5*np.ones(N)-Values
Theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False)
width = 2*np.pi/N
# Make a list of shifted thetas to place the labels at.
ThetaShifted = np.copy(Theta)
for i in range(N-1):
ThetaShifted[i] = (Theta[i] + Theta[i+1])/2.0
ThetaShifted[-1] = (Theta[-1] + 2.0*np.pi)/2.0
# Make the figure
fig = mpl.figure()
ax = fig.add_subplot(111, projection='polar')
bars = ax.bar(Theta, Radii, width=width, bottom=bottom)
# Set the outer ring to be invisible.
ax.spines["polar"].set_visible(False)
# Set the grid line locations but set the labels to be invisible.
ax.grid(False)
ax.set_thetagrids([], visible=False)
ax.set_rgrids([3], visible=False)
# Apply colors to bars based on the settings above.
for v, bar in zip(Values, bars):
bar.set_facecolor(barcolor)
bar.set_edgecolor(bar.get_facecolor())
# Show the metric and value labels
for counter in range(N):
ax.text(ThetaShifted[counter], bottom-.075*(10+len(MetricLabels[counter])), MetricLabels[counter]+' '*5,
horizontalalignment='center', verticalalignment='center',
rotation=(counter+.5)*360/N, color=bgcolor,
family='monospace')
ax.text(ThetaShifted[counter], bottom+1, np.round(Values[counter],2),
horizontalalignment='center', verticalalignment='center',
rotation=(counter+.5)*360/N, color=bars[counter].get_facecolor(),
family='monospace')
# Set the background color to be a dark grey,
ax.set_axis_bgcolor(bgcolor)
fig.set_facecolor(bgcolor)
# Show the figure.
mpl.show()
If I correct understand what you want you have to add rotation property to the second call of counter cycle and align the text like here:
...
# Show the metric and value labels
for counter in range(N):
ax.text(ThetaShifted[counter], bottom-3, MetricLabels[counter],
horizontalalignment='center', verticalalignment='baseline',
rotation=(counter+.5)*360/N, color=bgcolor)
ax.text(ThetaShifted[counter], bottom+2.5, np.round(Values[counter],2),
horizontalalignment='center', verticalalignment='center',
rotation=(counter+.5)*360/N,
color=bars[counter].get_facecolor())
...