Is there an 'empty' patch in matplotlib? - matplotlib

I am adding patches according to a list of ones and zeros (e.g. [1, 0, 1, 1, 0, 0, 1, 0, 1, 0]). I want to add patches where there are ones using matplotlib and leave the zeros empty. However, trying the following code raises a list index out of range error:
fig = plt.figure()
ax = plt.axes()
self.patches = []
for i, val in enumerate(my_list):
if val == 1:
self.patches.append(plt.Rectangle((i, 0), 0.9, 1, angle=0.0,
facecolor='r', edgecolor='w',
linewidth='2.0',
animated=False))
ax.add_patch(self.patches[i])
The only thing I can think of is using an else statement in the code above to add a rectangle with the same colour as the background for the zeros. Is there an empty patch object that one could use instead? I want the patches to be in the same position as the ones in the list.

use color='none' to set all colors (facecolor and edgecolor to invisible).
alternatively, you can pass visible=False to the constructor to hide the patch.
Your loop could be:
my_list = [1, 0, 1, 1, 0, 0, 1, 0, 1, 0]
patches = []
fig, ax = plt.subplots()
for i, val in enumerate(my_list):
p = plt.Rectangle((i, 0), 0.9, 1, angle=0.0,
facecolor='r', edgecolor='w',
linewidth='2.0',
animated=False, visible=bool(val))
patches.append(p)
ax.add_patch(p)

Sure, you can pass an empty patch to Matplotlib, but I wouldn't unless for some reason you want matplotlib to know about your missing data:
fig = plt.figure()
ax = plt.axes()
self.patches = []
for i, val in enumerate(my_list):
if val == 1:
self.patches.append(plt.Rectangle((i, 0), 0.9, 1, angle=0.0,
facecolor='r', edgecolor='w',
linewidth='2.0',
animated=False))
ax.add_patch(self.patches[i])
else:
self.patches.append(None)

Related

How to align a legend relative to a GridSpec cell?

I am creating a figure like this:
fig = plt.figure(figsize = (7, 8))
outer_grid = gridspec.GridSpec(2, 1, height_ratios = [2, 1])
inner_grid1 = gridspec.GridSpecFromSubplotSpec(4, 3, subplot_spec=outer_grid[0])
inner_grid2 = gridspec.GridSpecFromSubplotSpec(2, 3, subplot_spec=outer_grid[1])
Now I would like to have one legend for all plots in inner_grid1 and a separate legend for all plots in inner_grid2. And I would like those legends to be placed nicely, even though they are higher than a single plot, and cannot have more than one column to not make the figure too wide.
Here is an example where I tried to align the legends with trial and error with method 2 below, however this took ages to make.
So I see three options to achieve this, none of which work:
Place the legend as part of an Axes object, but manually move it outside of the actual plot using axes.legend([...], bbox_to_anchor=(x, y)). This does not work when the legend is higher as a single plot, because it rescales the plots to fit the legend into its grid cell.
Place the legend globally on the Figure object. This works, but makes the correct placement really hard. I cannot use loc = "center right", since it centers it for the full figure instead of just the inner_grid1 or inner_grid2 plots.
Place the legend locally on the GridSpecFromSubplotSpec object. This would be perfect. However there is no method to create a legend on a GridSpecFromSubplotSpec or related classes, and the pyplot.legend method misses parameters to restrict the loc to parts of a grid.
Is there a way to place a legend as described?
As requested, a small code example generating something similar as desired.
This example uses method 2:
#!/usr/bin/env python3
import pandas as pd, seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
GENOMES = ["spneumoniae", "ecoliK12", "scerevisiae", "celegans", "bmori", "hg38"]
fig = plt.figure(figsize = (7, 8))
outer_grid = gridspec.GridSpec(2, 1, height_ratios = [2, 1])
inner_grid1 = gridspec.GridSpecFromSubplotSpec(4, 3, subplot_spec=outer_grid[0])
inner_grid2 = gridspec.GridSpecFromSubplotSpec(2, 3, subplot_spec=outer_grid[1])
# plots are in sets of six, 2 rows by 3 columns each
for index, genome in enumerate(GENOMES):
data = pd.DataFrame({"x": [0, 1, 2, 3, 0, 1, 2, 3], "y": [1, 0, 3, 2, 1, 0, 3, 2], "hue": ["a", "a", "a", "a", "b", "b", "b", "b"]})
# first set of six
ax1 = plt.Subplot(fig, inner_grid1[index])
ax1 = sns.lineplot(data = data, x = "x", y = "y", hue = "hue", ax = ax1)
ax1.set_xlabel("")
ax1.set_ylabel("")
if index == 2:
ax1.legend()
handles, labels = ax1.get_legend_handles_labels()
fig.legend(handles, labels, loc = "center left", title = "", bbox_to_anchor=(0.9, 2/3 - 0.03))
ax1.legend([], [], loc = "lower center", title = f"{genome}")
fig.add_subplot(ax1)
# second set of six
ax2 = plt.Subplot(fig, inner_grid1[index + 6])
ax2 = sns.lineplot(data = data, x = "x", y = "y", hue = "hue", ax = ax2)
ax2.set_xlabel("")
ax2.set_ylabel("")
ax2.legend([], [], loc = "upper center", title = f"{genome}")
fig.add_subplot(ax2)
#third set of six
ax3 = plt.Subplot(fig, inner_grid2[index])
ax3 = sns.lineplot(data = data, x = "x", y = "y", hue = "hue", ax = ax3)
ax3.set_xlabel("")
ax3.set_ylabel("")
if index == 2:
ax3.legend(["#unitigs", "avg. unitig len."])
handles, labels = ax3.get_legend_handles_labels()
fig.legend(handles, labels, loc = "center left", title = "", bbox_to_anchor=(0.9, 1/6 + 0.05))
ax3.legend([], [], loc = "upper center", title = f"{genome}")
fig.add_subplot(ax3)
plt.savefig("stackoverflow_test.pdf", bbox_inches="tight")

How can I fix the size of the last subplot?

I want to have 5x4 subplots, one for each group. I wrote the following code:
axeng = []
for i in range(5):
for ii in range(4):
axeng.append([i,ii])`
yy = (0.5, 4.5, 9.5, 14.5, 19.5, 24.5)
xx=np.arange(0.5,10)
f,axes = plt.subplots(5,4,figsize=(50,50), sharex=True, sharey=True)
cbar_ax = f.add_axes([.92, .3, .03, .4])
for i in range(20):
paxesrow = tuple(axeng[i])[0]
paxescol = tuple(axeng[i])[1]
# gnuplot, jet, YlGnBu, GnBu_r
g=sns.heatmap(heat[i],cmap="viridis",vmin=0.1,vmax=1,
ax=axes[paxesrow,paxescol],linewidth=.1,
cbar=True if i==3 else False,
cbar_ax=cbar_ax if i==3 else None,
square=False)
g.set_yticks(yy)
g.set_xticks(xx)
g.set_yticklabels([' ','25',' ','15',' ','5'],fontsize=33)
g.set_xticklabels([' ','2',' ','4',' ','6',' ','8',' ','10'],fontsize=33,rotation=0)
f.tight_layout(rect=[1, 1, 1, 1])
f.suptitle('Behavior of all subgroups',fontsize=70,y=.93)
cbar=axes[tuple(axeng[3])[0],tuple(axeng[3])[1]].collections[0].colorbar
cbar.ax.tick_params(labelsize=35)
plt.show()
As you can see in the image, the last subplot is scaled, but I have no idea why that would be the case.
Thanks in advance.

Matplotlib 3d alpha transparency bug?

Using matplotlib, I am trying to create a 3d plot that has three semi-transparent planes along the xy, yz, and xz planes. I am basing my code off of this post, which has a partial workaround for a transparency bug reported three years ago.
If you try out the below code and rotate the graph, you'll see that there are sudden color shifts in the areas where the planes overlap. For example below you see the center area suddenly change from green to blue. Is there a further workaround to prevent this?
Here is my code:
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import mpl_toolkits.mplot3d as mp3d
xy = [ (-1, -1, 0),
( 1, -1, 0),
( 1, 1, 0),
(-1, 1, 0),
]
yz = [ (0, -1, -1),
(0, 1, -1),
(0, 1, 1),
(0, -1, 1),
]
xz = [ (-1, 0, -1),
( 1, 0, -1),
( 1, 0, 1),
(-1, 0, 1),
]
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.scatter([-1, -1, -1, -1, 1, 1, 1, 1], [-1, -1, 1, 1, -1, -1, 1, 1], [-1, 1, -1, 1, -1, 1, -1, 1])
face1 = mp3d.art3d.Poly3DCollection([xy], alpha=0.5, linewidth=1)
face2 = mp3d.art3d.Poly3DCollection([yz], alpha=0.5, linewidth=1)
face3 = mp3d.art3d.Poly3DCollection([xz], alpha=0.5, linewidth=1)
# This is the key step to get transparency working
alpha = 0.5
face1.set_facecolor((0, 0, 1, alpha))
face2.set_facecolor((0, 1, 0, alpha))
face3.set_facecolor((1, 0, 0, alpha))
ax.add_collection3d(face1)
ax.add_collection3d(face2)
ax.add_collection3d(face3)
plt.show()
As suggested in the comments, you can divide every plane into its four quadrant planes and draw those individually. This way matplotlib is able to determine which of them should be in front and the planes obey transparency.
A minimal example:
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import mpl_toolkits.mplot3d as mp3d
a = np.array([( 0, 0, 0),( 1, 0, 0),( 1, 1, 0),( 0, 1, 0)])
R1 = np.array([[0,-1,0],[1,0,0],[0,0,1]])
R2 = (R1[::-1].T)[:,[1,0,2]]
R3 = (R1[::-1])[:,[1,0,2]]
f = lambda a,r: np.matmul(r, a.T).T
g = lambda a,r: [a, f(a,r), f(f(a,r),r), f(f(f(a,r),r),r)]
fig = plt.figure()
ax = fig.add_subplot(111, projection=Axes3D.name)
ax.scatter([-1,1], [-1,1], [-1,1], alpha=0.0)
for i, ind , r in zip(range(3),[[0,1,2],[2,0,1],[1,2,0]], [R1,R2,R3]):
xy = g(a[:,ind], r )
for x in xy:
face1 = mp3d.art3d.Poly3DCollection([x] , alpha=0.5, linewidth=1)
face1.set_facecolor((i//2, i%2, i==0, 0.5))
ax.add_collection3d(face1)
plt.show()

Mayavi doesn't draw lines

I want to draw very simple graph with 4 nodes and 3 edges:
from numpy import array, vstack
from mayavi import mlab
mlab.figure(1, bgcolor=(1, 0.9, 1))
mlab.clf()
x = array([0, 3, 2, 3])
y = array([0, 4, 5, 1])
z = array([0, 0, 1, 1])
color = array([0.1, 0.3, 0.5, 0.7])
pts = mlab.points3d(x, y, z,
color,
scale_factor=1,
scale_mode='none',
colormap='Blues',
resolution=20)
edges = vstack([[0, 1], [0, 2], [0, 3]])
pts.mlab_source.dataset.lines = edges
tube = mlab.pipeline.tube(pts, tube_radius=0.1, tube_sides=7)
mlab.pipeline.surface(tube, color=(0.8, 0.8, 0.8))
mlab.show()
It returns that:
Why edges are missing?
There is a bug in Mayavi about this. It is related to unsynchronized changes with VTK and are thus a bit hard to pinpoint. There is a discussion on Mayavi's GitHub https://github.com/enthought/mayavi/issues/388
The bug also shows up with the protein.py example that comes up with Mayavi and it is fixed there by adding
pts.mlab_source.update()
after setting the lines. It is fixed online for the example at https://github.com/enthought/mayavi/commit/afb17fceafe787c8260ca7a37fbb3b8c2fbfd36c
Using the fix did not work for me though but you might try.

How to make a 3d matlibplot not show masked values

The diagram should only show the masked values. As in the (manipulated) figure on the right side.
Default shows all values. In 2d diagramms there is no problem.
Is it also possible in 3d diagrams? If yes, how to?
import matplotlib.pyplot as plt
import numpy as np
Z = np.array([
[ 1, 1, 1, 1, 1, ],
[ 1, 1, 1, 1, 1, ],
[ 1, 1, 1, 1, 1, ],
[ 1, 1, 1, 1, 1, ],
[ 1, 1, 1, 1, 1, ],
])
x, y = Z.shape
xs = np.arange(x)
ys = np.arange(y)
X, Y = np.meshgrid(xs, ys)
M = np.ma.fromfunction(lambda i, j: i > j, (x, y))
R = np.ma.masked_where(M, Z)
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, R)
#ax.plot_wireframe(X, Y, R)
#ax.plot_trisurf(X.flatten(), Y.flatten(), R.flatten())
fig.show()
Update: Matplotlib >= 3.5.0
As pointed out by eric's comment, the issue is solved in matplotlib <= 3.5.0, and the code from the OP works as expected. So right now, if you can probably your best option is to update matplotlib.
The original answer is left here for situations were updating matplotlib might not be an option.
Old: Matplotlib < 3.5.0
The bad news is that it seems that plot_surface() just ignores masks. In fact there is an open issue about it.
However, here they point out a workaround that although it's far from perfect it may allow you get some acceptable results. The key issue is that NaN values will not be plotted, so you need to 'mask' the values that you don't want to plot as np.nan.
Your example code would become something like this:
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
Z = np.array([
[ 1, 1, 1, 1, 1, ],
[ 1, 1, 1, 1, 1, ],
[ 1, 1, 1, 1, 1, ],
[ 1, 1, 1, 1, 1, ],
[ 1, 1, 1, 1, 1, ],
])
x, y = Z.shape
xs = np.arange(x)
ys = np.arange(y)
X, Y = np.meshgrid(xs, ys)
R = np.where(X>=Y, Z, np.nan)
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, R, rstride=1, linewidth=0)
fig.show()
*I had to add the rstride=1 argument to the plot_surface call; otherwise I get a segmentation fault... o_O
And here's the result: