How to add axis lines to ellipse using python matplotlib.patches.ellipse? - python

I have this simple code that generates an ellipse
import matplotlib.patches as patches
import matplotlib.pyplot as plt
fig, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
ellipse = patches.Ellipse((0, 0), 4, 2, angle=45, fill=False)
ax.add_artist(ellipse)
ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-2.2, 2.2)
plt.show()
This is the current output:
ellipse
I need to add axis of ellipse so it would look like this:
ellipse_output
Is there a way to do that?
I need a generic way to use in more complex ellipses, thanks.
I tried to search for parameters in patches.Ellipse() to draw those axis lines, but didn't find anything.

You can add the major and minor axes of the ellipse.
In the code i show, i do the major axis, but you need to work on the angle part (based on the points of the elipse), whereas i just set it to 45 degrees to post a quick answer.
The result of this would give the complete solution.
So, I do something like this:
import matplotlib.patches as patches
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
#################################
# you need to figure this bit out
#################################
ellipse = patches.Ellipse((0, 0), 4, 2, angle=45, fill=False)
ax.add_artist(ellipse)
ellipse.set_clip_box(ax.bbox)
ellipse.set_alpha(0.1)
ax.annotate("",
xy=(ellipse.center[0], ellipse.center[1] - ellipse.height / 2),
xytext=(ellipse.center[0], ellipse.center[1] + ellipse.height / 2),
arrowprops=dict(arrowstyle="<->", color="black"))
ax.annotate("",
xy=(ellipse.center[0] - ellipse.width / 2, ellipse.center[1]),
xytext=(ellipse.center[0] + ellipse.width / 2, ellipse.center[1]),
arrowprops=dict(arrowstyle="<->", color="black"))
ax.annotate("",
xy=(ellipse.center[0] - ellipse.width / 2 * np.cos(np.deg2rad(ellipse.angle)),
ellipse.center[1] - ellipse.height / 2 * np.sin(np.deg2rad(ellipse.angle))),
xytext=(ellipse.center[0] + ellipse.width / 2 * np.cos(np.deg2rad(ellipse.angle)),
ellipse.center[1] + ellipse.height / 2 * np.sin(np.deg2rad(ellipse.angle))),
arrowprops=dict(arrowstyle="<->", color="black"))
ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-2.2, 2.2)
plt.show()
Which leaves you with a plot like this:
Basically, in summary, the anotate lines let you do the final bits that you require.
EDIT:
I was able to reduce to this:
import matplotlib.patches as patches
import matplotlib.pyplot as plt
fig, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
# patches.Ellipse(center, width, height, angle)
ellipse = patches.Ellipse((0, 0), 4, 2, angle=45, fill=False)
ax.add_artist(ellipse)
ellipse.set_clip_box(ax.bbox)
ax.annotate("",
xy=(ellipse.center[0] - ellipse.width+2 ,
ellipse.center[1] - ellipse.height ),
xytext=(ellipse.center[0] + ellipse.width-1,
ellipse.center[1] + ellipse.height+1),
arrowprops=dict(arrowstyle="<->", color="red"))
ax.set_xlim(-2.2, 2.2;)
ax.set_ylim(-2.2, 2.2)
plt.show()
which looks like this:

from math import sin, cos, radians
import matplotlib.patches as patches
import matplotlib.pyplot as plt
fig, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
##############
ellipse_size = (4,2)
ellipse_rotation = 45
ellipse_position = (0,0)
ellipse = patches.Ellipse(ellipse_position, ellipse_size[0], ellipse_size[1], angle=ellipse_rotation, fill=False)
ax.add_artist(ellipse)
ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-2.2, 2.2)
# math for the start and end axis point positions
ax1_points = [
(ellipse_position[0]+ellipse_size[0]/2*cos(radians(ellipse_rotation)),
ellipse_position[1]+ellipse_size[0]/2*sin(radians(ellipse_rotation))),
(ellipse_position[0]+ellipse_size[0]/2*cos(radians(ellipse_rotation + 180)),
ellipse_position[1]+ellipse_size[0]/2*sin(radians(ellipse_rotation + 180)))
]
ax2_points = [
(ellipse_position[0]+ellipse_size[1]/2*cos(radians(ellipse_rotation+90)),
ellipse_position[1]+ellipse_size[1]/2*sin(radians(ellipse_rotation+90))),
(ellipse_position[0]+ellipse_size[1]/2*cos(radians(ellipse_rotation + 270)),
ellipse_position[1]+ellipse_size[1]/2*sin(radians(ellipse_rotation + 270)))]
# ax1 and ax2 contains the start and the end point of the axis ([x,y] format)
# drawing the arrows
arrowprops=dict(arrowstyle="<->", color="red")
ax.annotate("", xy=ax1_points[0], xytext=ax1_points[1], arrowprops=arrowprops)
ax.annotate("", xy=ax2_points[0], xytext=ax2_points[1], arrowprops=arrowprops)
plt.show()
produces this:
Hope this helps.

Related

How to plot a vertical thermal plot?

How to plot this kind of thermal plot in Python? I tried to search for any sample plot like this but didn't find one.
This image I got from the internet. I want to plot something same like this:
FROM
TO
To represent this type of data the canonical solution is, of course, a heat map. Here it is the code to produce both the figures at the top of this post.
import numpy as np
import matplotlib.pyplot as plt
t = np.linspace(0, 5, 501)
x = np.linspace(0, 1, 201)[:, None]
T = 50 + (30-6*t)*(4*x*(1-x)) + 4*t
fig, ax = plt.subplots(layout='constrained')
hm = ax.imshow(T, cmap='plasma',
aspect='auto', origin='lower', extent=(0, 5, 0, 1))
fig.colorbar(hm)
def heat_lines(x, t, T, n):
from matplotlib.cm import ScalarMappable
from matplotlib.collections import LineCollection
lx, lt = T.shape
ones = np.ones(lx)
norm = plt.Normalize(np.min(T), np.max(T))
plasma = plt.cm.plasma
fig, ax = plt.subplots(figsize=(1+1.2*n, 9), layout='constrained')
ax.set_xlim((-0.6, n-0.4))
ax.set_ylim((x[0], x[-1]))
ax.set_xticks(range(n))
ax.tick_params(right=False,top=False, bottom=False)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["bottom"].set_visible(False)
ax.grid(axis='y')
fig.colorbar(ScalarMappable(cmap=plasma, norm=norm))
dt = round(lt/(n-1))
for pos, ix in enumerate(range(0, len(t)+dt//2, dt)):
points = np.array([ones*pos, x[:,0]]).T.reshape(-1,1,2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
lc = LineCollection(segments, linewidth=72, ec=None,
color=plasma(norm(T[:,ix])))
lc.set_array(T[:,ix])
ax.add_collection(lc)
heat_lines(x, t, T, 6)

How to measure and modify length of path?

I am trying to create paths with mathplotlib.path, more precisely n-gons. Although I would like to add the constraint that all polygons have the same perimeter. In order to do that I would have to calculate the perimeter of the polygon, and the adjust the path length to a fixed variable.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.path as mpltPath
N = 10000
points = np.random.rand(N,2)
# regular polygon
sidepoly = 5
polygon = [[np.sin(x),np.cos(x)] for x in np.linspace(0, 2*np.pi, sidepoly)[:sidepoly]]
# Matplotlib mplPath
path = mpltPath.Path(polygon)
fig, ax = plt.subplots()
patch = patches.PathPatch(path, facecolor='none', lw=2)
ax.add_patch(patch)
ax.axis('equal')
ax.set_xlim(-1,1)
ax.set_ylim(-1,1)
plt.show()
Any recomendations?
The side length of a regular polygon can be calculated via twice the sine of half the angle (see e.g. here). The perimeter is just one side length multiplied by the number of sides. Using a radius that divides away the default perimeter and multiplies by the desired perimeter, creates a polygon with that perimeter.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.path as mpltPath
from matplotlib.colors import to_rgba
fig, axs = plt.subplots(ncols=5, nrows=2, figsize=(12,5))
for sidepoly, ax in zip(range(3, 3+axs.size), axs.flatten()):
# regular polygon
desired_perimeter = 5
default_perimeter = 2 * sidepoly * np.sin(np.pi / sidepoly)
theta = np.linspace(0, 2 * np.pi, sidepoly+1)
polygon = np.c_[np.sin(theta), np.cos(theta)] * desired_perimeter / default_perimeter
path = mpltPath.Path(polygon)
patch = patches.PathPatch(path, facecolor=to_rgba('dodgerblue', alpha=0.2), edgecolor='black', lw=2)
ax.add_patch(patch)
side_length = np.sqrt((polygon[1, 0] - polygon[0, 0]) ** 2 + (polygon[1, 1] - polygon[0, 1]) ** 2)
perimeter = side_length * sidepoly
ax.text(0, 0, f'{sidepoly}-gon\nside:{side_length:.2f}\nperim.:{perimeter:.2f}', ha='center', va='center')
ax.axis('equal')
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
plt.tight_layout()
plt.show()

How to change background color of inset figure

I'm trying to create an inset figure that has a different projection from the parent. The only issue I have at this point is the inset figures's tick labels are not legible because they are black and blend in with the plot behind it. I could change the color of the ticks and labels to white, but that does not help when the data in ax0 yields lighter colors. Here is the MWE:
import calipsoFunctions as cf
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import numpy as np
import pylab as pl
from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
from mpl_toolkits.axes_grid1.inset_locator import inset_axes, mark_inset, InsetPosition
x, y = np.arange(100), np.arange(200)
X, Y = np.meshgrid(x, y)
C = np.random.randint(0, 100, (200, 100))
fig = pl.figure(figsize=(6.5, 5.25))
gs0 = pl.GridSpec(3, 1)
gs0.update(left=0.08, right=0.925,
top=0.95, bottom=0.33,
hspace=0.10, wspace=0.0)
gs1 = pl.GridSpec(1, 2)
gs1.update(left=0.08, right=0.925,
top=0.225, bottom=0.05,
hspace=0.0, wspace=0.025)
# create primary axes
ax0 = pl.subplot(gs0[0])
ax1 = pl.subplot(gs0[1])
ax0.pcolormesh(X, Y, C, vmin=0, vmax=75)
ax1.pcolormesh(X, Y, C, vmin=0, vmax=75)
# add map plot (inset axis)
loc_box = [0.8, 0.55, 0.20, 0.45]
ax0_inset = fig.add_axes(loc_box,
projection=ccrs.PlateCarree(),
aspect="auto",
facecolor="w",
frameon=True)
lat_array = np.arange(-20, 20)
lon_array = np.arange(-10, 10, 0.5)
ax0_inset.plot(lat_array, lon_array, "k-", lw=1)
ip = InsetPosition(ax0, loc_box)
ax0_inset.set_axes_locator(ip)
ax0_inset.coastlines(resolution="10m", linewidth=0.25, color="k")
ax0_inset.add_feature(cfeature.LAND)
llat, ulat = lat_array.min(), lat_array.max()
llon, ulon = lon_array.min(), lon_array.max()
llat = np.round(llat / 10) * 10
ulat = np.round(ulat / 10) * 10
llon = np.round(llon / 5) * 5
ulon = np.round(ulon / 5) * 5
ax0_inset.set_yticks(np.arange(llat, ulat, 20), minor=False)
ax0_inset.set_yticks(np.arange(llat, ulat, 10), minor=True)
ax0_inset.set_yticklabels(np.arange(llat, ulat, 20),
fontsize=8)
ax0_inset.yaxis.set_major_formatter(LatitudeFormatter())
ax0_inset.set_xticks(np.arange(llon, ulon, 5), minor=False)
ax0_inset.set_xticks(np.arange(llon, ulon, 1), minor=True)
ax0_inset.set_xticklabels(np.arange(llon, ulon, 5),
fontsize=8,
rotation=45)
ax0_inset.xaxis.set_major_formatter(LongitudeFormatter())
ax0_inset.grid()
ax0_inset.tick_params(which="both",
axis="both",
direction="in",
labelsize=8)
fig.show()
Is there a way to change the background color of ax0_inset so that these tick labels are legible? I tried changing the face_color to "w", but that did not work. Ideally, I want the same behavior as ax0.figure.set_facecolor("w"), but for the ax0_inset axis. Is this doable?
Following #Mr. T's comment suggestion, a work-around solution could be:
# insert transparent (or opaque) rectangle around inset_axes plot
# to make axes labels more visible
# make buffer variable to control amount of buffer around inset_axes
buffer = 0.1 # fractional axes coordinates
# use ax inset tuple coords in loc_box to add rectangle patch
# [left, bottom, width, height] (fractional axes coordinates)
fig.add_patch(plt.Rectangle((
loc_box[0]-buffer, loc_box[1]-buffer),
loc_box[2]+buffer, loc_box[3]+buffer,
linestyle="-", edgecolor="k", facecolor="w",
linewidth=1, alpha=0.75, zorder=5,
transform=ax0.transAxes))

How to properly draw hexagons with offset coordinates?

This is my code:
import matplotlib.pyplot as plt
from matplotlib.patches import RegularPolygon
import numpy as np
offCoord = [[-2,-2],[-1,-2],[0,-2],[1,-2],[2,-2]]
fig, ax = plt.subplots(1)
ax.set_aspect('equal')
for c in offCoord:
hex = RegularPolygon((c[0], c[1]), numVertices=6, radius=2./3., alpha=0.2, edgecolor='k')
ax.add_patch(hex)
plt.autoscale(enable = True)
plt.show()
Expected result vs actual result in the attached image
Please tell me why my hexagons are not lined up edge by edge but overlap each other?
What am I doing wrong?
Use law of cosines (for isosceles triangle with angle 120 degrees and sides r, r, and 1):
1 = r*r + r*r - 2*r*r*cos(2pi/3) = r*r + r*r + r*r = 3*r*r
r = sqrt(1/3)
This is the right code:
import matplotlib.pyplot as plt
from matplotlib.patches import RegularPolygon
import numpy as np
offCoord = [[-2,-2],[-1,-2],[0,-2],[1,-2],[2,-2]]
fig, ax = plt.subplots(1)
ax.set_aspect('equal')
for c in offCoord:
# fix radius here
hexagon = RegularPolygon((c[0], c[1]), numVertices=6, radius=np.sqrt(1/3), alpha=0.2, edgecolor='k')
ax.add_patch(hexagon)
plt.autoscale(enable = True)
plt.show()
Very simply, your geometry is wrong. You specified a radius of 2/3. Check your documentation for RegularPolygon; I think that you'll find the correct radius is 0.577 (sqrt(3) / 3) or something close to that.
Radius of regular hexagon equals its side. In that case, the proper offset should be:
offset = radius*3**0.5. If radius is 2/3, the offsets should be 1.1547k, where k=-2,-1...

How to draw a filled arc in matplotlib

In matplotlib, I would like draw an filled arc which looks like this:
The following code results in an unfilled line arc:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
fg, ax = plt.subplots(1, 1)
pac = mpatches.Arc([0, -2.5], 5, 5, angle=0, theta1=45, theta2=135)
ax.add_patch(pac)
ax.axis([-2, 2, -2, 2])
ax.set_aspect("equal")
fg.canvas.draw()
The documentation says that filled arcs are not possible.
What would be the best way to draw one?
#jeanrjc's solution almost gets you there, but it adds a completely unnecessary white triangle, which will hide other objects as well (see figure below, version 1).
This is a simpler approach, which only adds a polygon of the arc:
Basically we create a series of points (points) along the edge of the circle (from theta1 to theta2). This is already enough, as we can set the close flag in the Polygon constructor which will add the line from the last to the first point (creating a closed arc).
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
def arc_patch(center, radius, theta1, theta2, ax=None, resolution=50, **kwargs):
# make sure ax is not empty
if ax is None:
ax = plt.gca()
# generate the points
theta = np.linspace(np.radians(theta1), np.radians(theta2), resolution)
points = np.vstack((radius*np.cos(theta) + center[0],
radius*np.sin(theta) + center[1]))
# build the polygon and add it to the axes
poly = mpatches.Polygon(points.T, closed=True, **kwargs)
ax.add_patch(poly)
return poly
And then we apply it:
fig, ax = plt.subplots(1,2)
# #jeanrjc solution, which might hide other objects in your plot
ax[0].plot([-1,1],[1,-1], 'r', zorder = -10)
filled_arc((0.,0.3), 1, 90, 180, ax[0], 'blue')
ax[0].set_title('version 1')
# simpler approach, which really is just the arc
ax[1].plot([-1,1],[1,-1], 'r', zorder = -10)
arc_patch((0.,0.3), 1, 90, 180, ax=ax[1], fill=True, color='blue')
ax[1].set_title('version 2')
# axis settings
for a in ax:
a.set_aspect('equal')
a.set_xlim(-1.5, 1.5)
a.set_ylim(-1.5, 1.5)
plt.show()
Result (version 2):
You can use fill_between to achieve this
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
fg, ax = plt.subplots(1, 1)
r=2.
yoff=-1
x=np.arange(-1.,1.05,0.05)
y=np.sqrt(r-x**2)+yoff
ax.fill_between(x,y,0)
ax.axis([-2, 2, -2, 2])
ax.set_aspect("equal")
fg.canvas.draw()
Play around with r and yoff to move the arc
EDIT:
OK, so you want to be able to plot arbitrary angles? You just need to find the equation of the chord, rather than using a flat line like above. Here's a function to do just that:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
fg, ax = plt.subplots(1, 1)
col='rgbkmcyk'
def filled_arc(center,r,theta1,theta2):
# Range of angles
phi=np.linspace(theta1,theta2,100)
# x values
x=center[0]+r*np.sin(np.radians(phi))
# y values. need to correct for negative values in range theta=90--270
yy = np.sqrt(r-x**2)
yy = [-yy[i] if phi[i] > 90 and phi[i] < 270 else yy[i] for i in range(len(yy))]
y = center[1] + np.array(yy)
# Equation of the chord
m=(y[-1]-y[0])/(x[-1]-x[0])
c=y[0]-m*x[0]
y2=m*x+c
# Plot the filled arc
ax.fill_between(x,y,y2,color=col[theta1/45])
# Lets plot a whole range of arcs
for i in [0,45,90,135,180,225,270,315]:
filled_arc([0,0],1,i,i+45)
ax.axis([-2, 2, -2, 2])
ax.set_aspect("equal")
fg.savefig('filled_arc.png')
And here's the output:
Here's a simpler workaround. Use the hatch argument in your mpatches.Arc command. If you repeat symbols with the hatch argument it increases the density of the patterning. I find that if you use 6 dashes, '-', or 6 dots, '.' (others probably also work), then it solidly fills in the arc as desired. When I run this
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
plt.axes()
pac = mpatches.Arc([0, -2.5], 5, 5, 45, theta1=45, theta2=135, hatch = '......')
plt.gca().add_patch(pac)
pac.set_color('cyan')
plt.axis('equal')
plt.show()
I get this:
Arc filled with dense dot hatch and rotated 45 degrees just for show
You can draw a wedge, and then hide part of it with a triangle:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
def filled_arc(center, radius, theta1, theta2, ax, color):
circ = mpatches.Wedge(center, radius, theta1, theta2, fill=True, color=color)
pt1 = (radius * (np.cos(theta1*np.pi/180.)) + center[0],
radius * (np.sin(theta1*np.pi/180.)) + center[1])
pt2 = (radius * (np.cos(theta2*np.pi/180.)) + center[0],
radius * (np.sin(theta2*np.pi/180.)) + center[1])
pt3 = center
pol = mpatches.Polygon([pt1, pt2, pt3], color=ax.get_axis_bgcolor(),
ec=ax.get_axis_bgcolor(), lw=2 )
ax.add_patch(circ)
ax.add_patch(pol)
and then you can call it:
fig, ax = plt.subplots(1,2)
filled_arc((0,0), 1, 45, 135, ax[0], "blue")
filled_arc((0,0), 1, 0, 40, ax[1], "blue")
and you get:
or:
fig, ax = plt.subplots(1, 1)
for i in range(0,360,45):
filled_arc((0,0), 1, i, i+45, ax, plt.cm.jet(i))
and you get:
HTH
The command ax.get_axis_bgcolor() needs to be replaced by ax.get_fc() for newer matplotlib.

Categories