How to make the plot's shape round? - matplotlib

I have created a plot, which is working just fine.
But I really want to change its shape to a circle.
This is my current plotting code:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(5,5), dpi=300)
ax = fig.add_axes([0,0,1,1])
ax.plot(30, 80, marker="o", markersize=20, markeredgecolor="#ed6033", markerfacecolor="#ed6033")
ax.spines['left'].set_position('center')
ax.spines['bottom'].set_position('center')
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
ax.set_facecolor('#8cc9e2')
ax.margins(0.1)
plt.setp(ax.get_xticklabels()[4], visible=False)
plt.xlim(10, 90)
plt.ylim(10, 90)
plt.grid(color='white')
plt.show()
and this is the output I get:
eventually, this is my desired output:

You can clip the path of artists including the background patch using the path of another artist.
Add this snippet before the plt.show() call:
clip_path = plt.Circle(
(0.5, 0.5), 0.5, transform=ax.transAxes, # circle coordinates defined in axes fractions
fill=None, linewidth=0 # makes circle invisible
)
ax.add_patch(clip_path)
ax.patch.set_clip_path(clip_path)

Related

matplotlib text in display coordinate

I try to understand how different coordinate systems work in matplotlib. My understanding is that if I add some texts using data coordinates, then I can achieve the same effect if I first transform the data coordinates to the display coordinates and then add the text using these display coordinates. In the following snippet, I expect the 'x' in green are right on top of the 'x' in red. But they are not. What do I miss here? Thank you!
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.set_xlim(0, 10)
ax.set_ylim(-1, 1)
ax.grid()
ax.text(4,0,'x', horizontalalignment='center', verticalalignment='center', color='r')
_x2, _y2 = ax.transData.transform((4.0, 0))
ax.text(_x2,_y2,'x', horizontalalignment='center', verticalalignment='center', transform=None, color='g')
Result:
Once again, we have examined the FIGURE, DATA, and AXIS criteria. We have modified them for clarity from the data range in question. The same is obtained with the figure criterion, divided by the graph size in inches and dpi value. The position of the origin of the axis is also obtained with the figure reference, and when drawn, it overlaps with the origin of the graph. The origin of the graph is shown in black.
import matplotlib.pyplot as plt
fig_x, fig_y = plt.rcParams['figure.figsize']
dpi = plt.rcParams['figure.dpi']
print(fig_x, fig_y, dpi)
fig, ax = plt.subplots()
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
ax.grid()
g = ax.text(0,0,'o', horizontalalignment='center', verticalalignment='center', color='r')
print('get_position', g.get_position())
_x2, _y2 = ax.transData.transform((0.0, 0))
print('ax.transData', _x2, _y2)
_x3, _y3 = ax.transAxes.transform((0.0, 0))
print('ax.transAxes', _x3, _y3)
# From Figure position to ax.transData
ax.text(_x2/fig_x/dpi,_y2/fig_y/dpi,'x', horizontalalignment='center', verticalalignment='center', color='g', transform=fig.transFigure)
# From Figure position to ax.transAxes
ax.text(_x3/fig_x/dpi,_y3/fig_y/dpi,'x', horizontalalignment='center', verticalalignment='center', color='b', transform=fig.transFigure)
# Figure x0, y0
ax.text(0,0,'x', horizontalalignment='center', verticalalignment='center', color='k', transform=fig.transFigure)
plt.show()

How to access and remove all unwanted objects in a matplotlib figure manually?

I am trying to understand the underlying concepts of matplotlib, especially Axes and Figure. Therefore I am trying to plot two scatters and then remove any superfluous space (the red one below) by accessing different APIs & objects in the hierarchy.
Yet I fail to understand where the remaining red space is coming from. This is the code:
# Random data
df = pd.DataFrame(np.random.randint(0,100,size=(100, 2)), columns=list('AB'))
# Create a single Axes and preconfigure the figure with red facecolor.
# Then plot a scatter
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10,5), facecolor='r')
ax1 = df.plot(kind='scatter', x='A', y='B', ax=axes[0])
ax2 = df.plot(kind='scatter', x='B', y='A', ax=axes[1])
# Remove except the scatter
for a in [ax1, ax2]:
a.set_xlabel(''), a.set_ylabel('') # Remove x and y labels
for loc in ['left', 'right', 'bottom', 'top']:
a.spines[loc].set_visible(False) # Remove spines
a.set_xticks([], []), a.set_yticks([], []) # Remove ticks
a.set_xmargin(0), a.set_ymargin(0) # No margin beyond outer values
# On figure-level we can make it more tight
fig.tight_layout()
It produces the following figure:
I saw that there is something like..
a.set_axis_off()
.. but this doesn't seem to be the right solution. Somewhere there seems to be some kind of padding that remains. It doesn't look like it's from some X/Y axis as it's the same for all four edges in both subplots.
Any help appreciated.
Solution
Two things are needed:
First we need to initialize the Figure with frameon=False:
fig, axes = plt.subplots(
// ...
frameon=False)
The space between the subplots can be removed using the subplot layout:
plt.subplots_adjust(wspace=.0, hspace=.0)
For the finest level of layout control, you can position your axes manually instead of relying on matplotlib to do it for you. There are a couple of ways of doing this.
One option is Axes.set_position
# Random data
df = pd.DataFrame(np.random.randint(0,100,size=(100, 2)), columns=list('AB'))
# Create a pair of Axes and preconfigure the figure with red facecolor.
# Then plot a scatter
fig, axes = plt.subplots(1, 2, figsize=(10, 5), facecolor='r')
df.plot(kind='scatter', x='A', y='B', ax=axes[0]).set_position([0, 0, 0.5, 1])
df.plot(kind='scatter', x='B', y='A', ax=axes[1]).set_position([0, 0.5, 0.5, 1])
You could also use the old-fashioned Figure.add_axes method:
# Random data
df = pd.DataFrame(np.random.randint(0,100,size=(100, 2)), columns=list('AB'))
# Create a pair of Axes and preconfigure the figure with red facecolor.
# Then plot a scatter
fig = plt.figure(figsize=(10, 5), facecolor='r')
df.plot(kind='scatter', x='A', y='B', ax=fig.add_axes([0, 0, 0.5, 1]))
df.plot(kind='scatter', x='B', y='A', ax=fig.add_axes([0, 0.5, 0.5, 1]))

How to add a legend for a GeoAxes that adds a Cartopy shapely feature?

I copied the code for adding legend via proxy artists from matplotlib's documentation but it doesn't work. I also tried the rest in matplotlib's legends guide but nothing works. I guess it's because the element is a shapely feature which ax.legend() somehow doesn't recognize.
Code
bounds = [116.9283371, 126.90534668, 4.58693981, 21.07014084]
stamen_terrain = cimgt.Stamen('terrain-background')
fault_line = ShapelyFeature(Reader('faultLines.shp').geometries(), ccrs.epsg(32651),
linewidth=1, edgecolor='black', facecolor='none') # geometry is multilinestring
fig = plt.figure(figsize=(15,10))
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
ax.set_extent(bounds)
ax.add_image(stamen_terrain, 8)
a = ax.add_feature(fault_line, zorder=1, label='test')
ax.legend([a], loc='lower left', fancybox=True) #plt.legend() has the same result
plt.show()
Result
When copying the matplotlib example, you omitted the actual "proxy" artist line!
red_patch = mpatches.Patch(color='red', label='The red data')
plt.legend(handles=[red_patch])
That red_patch is the proxy artist. You have to create a dummy artist to pass to legend(). Your code as written is still passing the unrecognized Shapely feature.
It's tedious, but the relevant code would be something like:
fault_line = ShapelyFeature(Reader('faultLines.shp').geometries(), ccrs.epsg(32651), linewidth=1, edgecolor='black', facecolor='none')
ax.add_feature(fault_line, zorder=1)
# Now make a dummy object that looks as similar as possible
import matplotlib.patches as mpatches
proxy_artist = mpatches.Rectangle((0, 0), 1, 0.1, linewidth=1, edgecolor='black', facecolor='none')
# And manually add the labels here
ax.legend([proxy_artist], ['test'], loc='lower left', fancybox=True)
Here I just used a Rectangle, but depending on the feature, you can use various supported matplotlib "artists".

Pyplot: Circle class: What are the units of the axis arg?

What units does Pyplot use for the radius size of Circle class objects? Playing around with it, it seems that a radius of .6 is already almost off the screen... small example below
import matplotlib as plt
plt.use('TkAgg')
import matplotlib.pyplot as plt
circle = plt.Circle((0.5, 0.5), 0.6, color='white')
fig, ax = plt.subplots()
plt.subplot
plt.axis('off')
ax.add_artist(circle)
fig.set_facecolor("black")
Absolute units are used for the radius. The reason why 0.6 goes out of the axis is that by default, if you just plot a blank figure, its extent is from 0 to 1. For example
fig, ax = plt.subplots()
ax.set_aspect('equal')
results in
Now if you center your circle at (0.5, 0.5) and use radius of 0.6, it will go outside the axis and you will see a truncated patch as you see in your question.
Things become clear when you remove plt.axis('off') and use radius=0.5
circle = plt.Circle((0.5, 0.5), 0.5, color='blue')
fig, ax = plt.subplots(figsize=(3,3))
# plt.axis('off') <--- commented out
ax.add_artist(circle)
fig.set_facecolor("gray")
ax.set_aspect('equal')
plt.plot([0.5], [0.5], 'ro')

how to remove the white space of invisiable axes in matplotlib during active plot?

I want to completely remove white space around my axes during active plot (not save_fig as others asked).
Here we cannot use bbox_inches='tight'. I can use tight_layout(pad=0).
When axis is on, it works fine, it shows all the ticks and x-y labels.
However, in some cases, I set the axis off. What I expected is to see the contents expand to fill up the empty space where the axes are. However, this does not work. It still keep the padding as there are still x-y labels and axes.
How can I remove the white space of invisible axes objects?
edit:
I am aware that I can use ax.set_yticks([]) and ax.set_xticks([]) to turn those off. But this is clumsy, I have to remember the the ticks before I clear them. And if I remove-then-add those ticks. The ticks cannot automatically update any more.
I wonder is there any more straightforward way to do this?
We can still see there is a small border spacing even after removing all ticks. If someone can come up a way to remove that too. It will be fantastic.
I would also like to keep the title if there is one. Thus the hard-coded ax.set_position([0,0,1,x]) is not very good for this usage. Surely we can still try to get the top spacing when there is a title, but if someone can provide a more direct/simple way to handle this, it will be preferred.
Example code:
def demo_tight_layout(w=10, h=6, axisoff=False, removeticks=False):
fig,ax = plt.subplots()
fig.set_facecolor((0.8, 0.8, 0.8))
rect = patches.Rectangle((-w/2, -h/2), w, h, color='#00ffff', alpha=0.5)
ax.add_patch(rect)
ax.plot([-w/2,w/2], [-h/2,h/2])
ax.plot([-w/2,w/2], [h/2,-h/2])
ax.set_ylabel("ylabel")
ax.margins(0)
_texts = []
if axisoff:
ax.set_axis_off()
_texts.append("axisoff")
if removeticks:
ax.set_xticks([])
ax.set_yticks([])
ax.set_ylabel("")
_texts.append("removeticks")
fig.text(0.5, 0.6, " ".join(_texts))
fig.tight_layout(pad=0)
plt.show()
return fig, ax, text
You may adjust the subplot parameters depending on whether you turned the axis off or not.
import matplotlib.pyplot as plt
from matplotlib import patches
def demo_tight_layout(w=10, h=6, axisoff=False):
fig,ax = plt.subplots()
fig.set_facecolor((0.8, 0.8, 0.8))
rect = patches.Rectangle((-w/2, -h/2), w, h, color='#00ffff', alpha=0.5)
ax.add_patch(rect)
ax.plot([-w/2,w/2], [-h/2,h/2])
ax.plot([-w/2,w/2], [h/2,-h/2])
ax.set_ylabel("ylabel")
ax.margins(0)
_texts = []
fig.tight_layout()
if axisoff:
ax.set_axis_off()
_texts.append("axisoff")
params = dict(bottom=0, left=0, right=1)
if ax.get_title() == "":
params.update(top=1)
fig.subplots_adjust(**params)
fig.text(0.5, 0.6, " ".join(_texts))
plt.show()
Now demo_tight_layout(axisoff=True) produces
and demo_tight_layout(axisoff=False) produces
You need to set the axes position to fill the figure. If you create your figure and plot with
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.gca()
ax.plot(some_x_data, some_y_data)
you need to add the following line to fill the figure with the axes:
ax.set_position([0, 0, 1, 1], which='both')
This sets the axes location relative to the figure size in the following way:
[left, bottom, width, height]
So to completely fill the figure use [0, 0, 1, 1] as shown above.
So taking your code, it should look like this (using fill_figure bool to check):
def demo_tight_layout(w=10, h=6, axisoff=False, removeticks=False, fill_figure=False):
fig,ax = plt.subplots()
fig.set_facecolor((0.8, 0.8, 0.8))
rect = patches.Rectangle((-w/2, -h/2), w, h, color='#00ffff', alpha=0.5)
ax.add_patch(rect)
ax.plot([-w/2,w/2], [-h/2,h/2])
ax.plot([-w/2,w/2], [h/2,-h/2])
ax.set_ylabel("ylabel")
ax.margins(0)
_texts = []
if axisoff:
ax.set_axis_off()
_texts.append("axisoff")
if removeticks:
ax.set_xticks([])
ax.set_yticks([])
ax.set_ylabel("")
_texts.append("removeticks")
fig.text(0.5, 0.6, " ".join(_texts))
fig.tight_layout(pad=0)
if fill_figure:
ax.set_position([0, 0, 1, 1], which='both')
plt.show()
return fig, ax, text
ax.set_position needs to be after fig.tight_layout.
If a figure title is needed, there is no direct way to do it. This unluckily can't be avoided. You need to adapt the height parameters manually so that the title fits in the figure, for example with:
ax.set_position([0, 0, 1, .9], which='both')