I'm trying to to colour the circular line that corresponds to the value of 0 in a polar chart. This is what I want to achieve:
On this related question (Shading a segment between two lines on polar axis (matplotlib)), ax.fill_between is used to fill the space between two values, but I'm looking for a way to colour just the circular line where the value for each variable is 0.
If anybody has any tips that would be most appreciated! I've inserted a minimal working example below if anybody fancies having a go.
import matplotlib.pyplot as plt
import pandas as pd
def make_spider(row, title, color):
import math
categories = list(df)
N = len(categories)
angles = [n / float(N) * 2 * math.pi for n in range(N)]
angles += angles[:1]
ax = plt.subplot(1, 5, row+1, polar=True)
plt.xticks(angles[:-1], categories, color='grey', size=8)
values = df.iloc[row].values.flatten().tolist()
values += values[:1]
ax.plot(angles, values, color=color, linewidth=2, linestyle='solid')
ax.fill(angles, values, color=color, alpha = .4)
plt.gca().set_rmax(.4)
my_dpi = 40
plt.figure(figsize=(1000/my_dpi, 1000/my_dpi), dpi=96)
my_palette = plt.cm.get_cmap('Set2', len(df.index)+1)
for row in range(0, len(df.index)):
make_spider( row = row, title='Cluster: ' + str(row), color=my_palette(row) )
Example dataframe here:
df = pd.DataFrame.from_dict({"no_rooms":{"0":-0.3470532925,"1":-0.082144001,"2":-0.082144001,"3":-0.3470532925,"4":-0.3470532925},"total_area":{"0":-0.1858487321,"1":-0.1685491141,"2":-0.1632483955,"3":-0.1769700284,"4":-0.0389887094},"car_park_spaces":{"0":-0.073703681,"1":-0.073703681,"2":-0.073703681,"3":-0.073703681,"4":-0.073703681},"house_price":{"0":-0.2416123064,"1":-0.2841806825,"2":-0.259622004,"3":-0.3529449824,"4":-0.3414842657},"pop_density":{"0":-0.1271390651,"1":-0.3105853643,"2":-0.2316607937,"3":-0.3297832328,"4":-0.4599021194},"business_rate":{"0":-0.1662745006,"1":-0.1426329043,"2":-0.1577528867,"3":-0.163560133,"4":-0.1099718326},"noqual_pc":{"0":-0.0251535462,"1":-0.1540641646,"2":-0.0204666924,"3":-0.0515740013,"4":-0.0445135996},"level4qual_pc":{"0":-0.0826103951,"1":-0.1777759951,"2":-0.114263357,"3":-0.1787044751,"4":-0.2709496389},"badhealth_pc":{"0":-0.105481688,"1":-0.1760349683,"2":-0.128215043,"3":-0.1560577648,"4":-0.1760349683}})
Probably a cheap hack based on the link you shared. The trick here is to simply use 360 degrees for fill_between and then use a very thin region around the circular line for 0 using margins such as -0.005 to 0.005. This way, you make sure the curve is centered around the 0 line. To make the line thicker/thinner you can increase/decrease this number. This can be straightforwardly extended to color all circular lines by putting it in a for loop.
ax.plot(angles, values, color=color, linewidth=2, linestyle='solid')
ax.fill(angles, values, color=color, alpha = .4)
ax.fill_between(np.linspace(0, 2*np.pi, 100), -0.005, 0.005, color='red', zorder=10) # <-- Added here
Other alternative could be to use a Circle patch as following
circle = plt.Circle((0, 0), 0.36, transform=ax.transData._b, fill=False, edgecolor='red', linewidth=2, zorder=10)
plt.gca().add_artist(circle)
but here I had to manually put 0.36 as the radius of the circle by playing around so as to put it exactly at the circular line for 0. If you know exactly the distance from the origin (center of the polar plot), you can put that number for exact position. At least for this case, 0.36 seems to be a good guess.
There is an easier option:
fig_radar.add_trace(go.Scatterpolar(
r = np.repeat(0, 360),
dtheta = 360,
mode = 'lines',
name = 'cirlce',
line_color = 'black',
line_shape="spline"
)
The addition of line_shape = "spline" makes it appear as a circle
dtheta divides the coordinates in so many parts (at least I understood it this way and it works)
Related
I was trying to make a Polar heatmap using the following code.
# Plotting the polar plot
from matplotlib.colorbar import ColorbarBase
from matplotlib.colors import LogNorm
import matplotlib.pyplot as plt
cmap = obspy_sequential
# Have defined the variables to be used for pointing to the coordinates
# baz is angular, slow is radial, abs_power is the value at every co-ordinate
# Choose number of fractions in plot (desirably 360 degree/N is an integer!)
N = 72
N2 = 30
abins = np.arange(N + 1) * 360. / N
sbins = np.linspace(0, 3, N2 + 1)
# Sum rel power in bins given by abins and sbins
hist, baz_edges, sl_edges = \
np.histogram2d(baz, slow, bins=[abins, sbins], weights=abs_power)
# Transform to radian
baz_edges = np.radians(baz_edges)
# Add polar and colorbar axes
fig = plt.figure(figsize=(8, 8))
cax = fig.add_axes([0.85, 0.2, 0.05, 0.5])
ax = fig.add_axes([0.10, 0.1, 0.70, 0.7], polar=True)
ax.set_theta_direction(-1)
ax.set_theta_zero_location("N")
dh = abs(sl_edges[1] - sl_edges[0])
dw = abs(baz_edges[1] - baz_edges[0])
# Circle through backazimuth
for i, row in enumerate(hist):
bars = ax.bar((i * dw) * np.ones(N2),
height=dh * np.ones(N2),
width=dw, bottom=dh * np.arange(N2),color=cmap(row / hist.max()))
ax.set_xticks(np.linspace(0, 2 * np.pi, 10, endpoint=False))
ax.set_yticklabels(velocity)
ax.set_ylim(0, 3)
[i.set_color('white') for i in ax.get_yticklabels()]
ColorbarBase(cax, cmap=cmap,
norm=LogNorm(vmin=hist.min(),vmax=hist.max()))
plt.show()
I am creating multiple plots like this and thus I need to extend the range of the colorbar beyond the maximum of the abs_power data range.
I tried changing the vmax and vmin to the maximum-minimum target numbers I want, but it plots out the exact same plot every single time. The maximum value on the colorbar keeps changing but the plot does not change. Why is this happening?
Here is how it looks,
Here the actual maximum power is way lesser than the maximum specified in the colorbar. Still a bright yellow spot is visible.
PS : I get this same plot for any vmax,vmin values I provide.
Changing the colorbar doesn't have an effect on the main plot. You'd need to change the formula used in color=cmap(row / hist.max()) to change the barplot. The 'norm' is just meant for this task. The norm maps the range of numbers to the interval [0, 1]. Every value that is mapped to a value higher than 1 (i.e. a value higher than hist.max() in the example), gets assigned the highest color.
To have the colorbar reflect the correct information, you'd need the same cmap and same norm for both the plot and the colorbar:
my_norm = LogNorm(vmin=hist.min(),vmax=hist.max())
for i, row in enumerate(hist):
bars = ax.bar((i * dw) * np.ones(N2),
height=dh * np.ones(N2),
width=dw, bottom=dh * np.arange(N2),color=cmap(my_norm(row)))
and
ColorbarBase(cax, cmap=cmap, norm=my_norm)
On the other hand, if you don't want the yellow color to show up, you could try something like my_norm = LogNorm(vmin=hist.min(), vmax=hist.max()*100) in the code above.
Instead of creating the colorbar via ColorbarBase, it can help to use a standard plt.colorbar(), but with a ScalarMappable that indicates the color map and the norm used. In case of a LogNorm this will show the ticks in log format.
from matplotlib.cm import ScalarMappable
plt.colorbar(ScalarMappable(cmap=cmap, norm=my_norm), ax=ax, cax=cax)
I'm drawing the following plot using Matplotlib:
import matplotlib.pyplot as mlp
import numpy.linalg as npl
def ploteig(self, erg:bool) -> None:
theta = np.arange(start=0, stop=2.0*np.pi, step=0.01)
r = np.ones(len(theta))
values, _ = npl.eig(self._p)
values = values.astype(complex)
x_unit_circle = r * np.cos(theta)
y_unit_circle = r * np.sin(theta)
x_eigenvalues = np.unique(np.append(values, np.complex(1.0)))
y_eigenvalues = np.zeros(len(x_eigenvalues))
has_slem = False
if erg:
values_abs = np.sort(np.abs(values))
values_ct1 = np.isclose(values_abs, 1.0)
if not np.all(values_ct1):
mu = values_abs[~values_ct1][-1]
if not np.isclose(mu, 0.0):
r *= mu;
x_slem_circle = r * np.cos(theta)
y_slem_circle = r * np.sin(theta)
has_slem = True
fig, ax = mlp.subplots()
ax.plot(x_unit_circle, y_unit_circle, color='red', linestyle='-', linewidth=3)
ax.plot(x_eigenvalues, y_eigenvalues, color='blue', linestyle='None', marker='*', markersize=10)
if has_slem:
ax.plot(x_slem_circle, y_slem_circle, color='red', linestyle='--', linewidth=1)
ax.grid(True)
ax.set_aspect('equal', 'datalim')
mlp.show()
When has_slem is True, then the slem circle is always smaller than the unit circle, hence the plot produces two concentric circles where the outer circle is given by (x_unit_circle,y_unit_circle) and the inner circle is given by (x_slem_circle,y_slem_circle).
What I would like to do is to fill the area between the two circles with a light red color. This is what I tried so far:
if has_slem:
ax.plot(x_slem_circle, y_slem_circle, color='red', linestyle='--', linewidth=1)
ax.fill_between(x_unit_circle, y_unit_circle, -y_unit_circle, color="red", alpha=0.2)
ax.fill_between(x_slem_circle, y_slem_circle, -y_slem_circle, color="white")
But this approach has two problems:
If the axes color is changed, the second fill_between call would produce a wrong fill based on white color.
The filled area looks a little bit misaligned with respect to the inner circle (there is a small white gap), as you can see on the screenshot below.
So, here comes my question: is there a better and more precise approach for filling the area between the two circles that allows me to bypass both problems?
On a totally unrelated note: is it ok to call mlp.show() inside the function? I don't know what are the best practices here... maybe it's better to return the figure handle and let the consumer decide when to pop it up?
pyplot.contourf is what you are looking for. Something that can go like this:
# inner radius
inner = 0.5
# the two circles
thetas = np.linspace(0,2*np.pi, 200)
# you don't need r = np.one(len(thetas))
x_unit_circle = np.cos(thetas)
y_unit_circle = np.sin(thetas)
x_eigens = x_unit_circle * inner
y_eigens = y_unit_circle * inner
xs = np.linspace(-1.1,1.1, 201)
ys = np.linspace(-1.1,1.1, 201)
# mesh for contours
xv,yv = np.meshgrid(xs,ys)
# generate the level map
r = xv**2 + yv**2
pyplot.figure(figsize=(8,8))
# plot the contours with two levels only
# notice the xv, yv parameters
pyplot.contourf(xv, yv, r, levels=[inner**2,1], colors=('r','g','b'))
# plot the two circles
pyplot.plot(x_unit_circle, y_unit_circle, color='b', linewidth=3)
pyplot.plot(x_eigens, y_eigens, color='g', linewidth=3, linestyle='--')
pyplot.show()
and result in different background:
I am trying to make use the polar plot projection to make a radar chart. I would like to know how to put only one grid line in bold (while the others should remain standard).
For my specific case, I would like to highlight the gridline associated to the ytick "0".
from matplotlib import pyplot as plt
import pandas as pd
import numpy as np
#Variables
sespi = pd.read_csv("country_progress.csv")
labels = sespi.country
progress = sespi.progress
angles=np.linspace(0, 2*np.pi, len(labels), endpoint=False)
#Concatenation to close the plots
progress=np.concatenate((progress,[progress[0]]))
angles=np.concatenate((angles,[angles[0]]))
#Polar plot
fig=plt.figure()
ax = fig.add_subplot(111, polar=True)
ax.plot(angles, progress, '.--', linewidth=1, c="g")
#ax.fill(angles, progress, alpha=0.25)
ax.set_thetagrids(angles * 180/np.pi, labels)
ax.set_yticklabels([-200,-150,-100,-50,0,50,100,150,200])
#ax.set_title()
ax.grid(True)
plt.show()
The gridlines of a plot are Line2D objects. Therefore you can't make it bold. What you can do (as shown, in part, in the other answer) is to increase the linewidth and change the colour but rather than plot a new line you can do this to the specified gridline.
You first need to find the index of the y tick labels which you want to change:
y_tick_labels = [-100,-10,0,10]
ind = y_tick_labels.index(0) # find index of value 0
You can then get a list of the gridlines using gridlines = ax.yaxis.get_gridlines(). Then use the index you found previously on this list to change the properties of the correct gridline.
Using the example from the gallery as a basis, a full example is shown below:
r = np.arange(0, 2, 0.01)
theta = 2 * np.pi * r
ax = plt.subplot(111, projection='polar')
ax.set_rmax(2)
ax.set_rticks([0.5, 1, 1.5, 2]) # less radial ticks
ax.set_rlabel_position(-22.5) # get radial labels away from plotted line
ax.grid(True)
y_tick_labels = [-100, -10, 0, 10]
ax.set_yticklabels(y_tick_labels)
ind = y_tick_labels.index(0) # find index of value 0
gridlines = ax.yaxis.get_gridlines()
gridlines[ind].set_color("k")
gridlines[ind].set_linewidth(2.5)
plt.show()
Which gives:
It is just a trick, but I guess you could just plot a circle and change its linewidth and color to whatever could be bold for you.
For example:
import matplotlib.pyplot as plt
import numpy as np
Yline = 0
Npoints = 300
angles = np.linspace(0,360,Npoints)*np.pi/180
line = 0*angles + Yline
ax = plt.subplot(111, projection='polar')
plt.plot(angles, line, color = 'k', linewidth = 3)
plt.ylim([-1,1])
plt.grid(True)
plt.show()
In this piece of code, I plot a line using plt.plot between any point of the two vectors angles and line. The former is actually all the angles between 0 and 2*np.pi. The latter is constant, and equal to the 'height' you want to plot that line Yline.
I suggest you try to decrease and increase Npoints while having a look to the documentaion of np.linspace() in order to understand your problem with the roundness of the circle.
Following on from my previous question I have the coordinates of the text label box in figure fraction coordinates and attempted to get the coordinates of the arrow patch in the same way.
But the coordinates I get do not correspond to the arrow, because when I plot a line over the same coordinates it doesn't lie on top of it:
import numpy as np
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
def f(x):
return 10 * np.sin(3*x)**4
x = np.linspace(0, 2*np.pi, 100)
y = f(x)
fig, ax = plt.subplots()
ax.plot(x,y)
xpt = 1.75
ypt = f(xpt)
xy = ax.transData.transform([xpt, ypt])
xy = fig.transFigure.inverted().transform(xy)
xytext = xy + [0.1, -0.1]
rdx, rdy = 0, 1
ann = ax.annotate('A point', xy=xy, xycoords='figure fraction',
xytext=xytext, textcoords='figure fraction',
arrowprops=dict(arrowstyle='->', connectionstyle="arc3",
relpos=(rdx, rdy)),
bbox=dict(fc='gray', edgecolor='k', alpha=0.5),
ha='left', va='top'
)
fig.canvas.draw()
leader_line_box = ann.arrow_patch.get_extents()
print(leader_line_box)
leader_line_box = fig.transFigure.inverted().transform(leader_line_box)
print(leader_line_box)
from matplotlib.lines import Line2D
line = Line2D(leader_line_box.T[0], leader_line_box.T[1],transform=fig.transFigure, lw=2, color='m')
ax.add_line(line)
plt.savefig('test.png')
How can I get the ((x0,y0), (x1,y1)) coordinates of the annotation arrow in figure fraction units and what has gone wrong in my attempt here?
The easiest way in this very specific case is to just draw the x-coordinates in reverse
line = Line2D(leader_line_box.T[0][::-1], leader_line_box.T[1],transform=fig.transFigure, lw=2, color='m')
If you need a more general solution,
verts = ann.arrow_patch.get_path()._vertices
tverts= fig.transFigure.inverted().transform(verts)
index = [0,2]
line = Line2D([tverts[index[0],0],tverts[index[1],0]], [tverts[index[0],1],tverts[index[1],1]],
transform=fig.transFigure, lw=2, color='m')
ax.add_line(line)
This will work for any arrow direction (pointing upwards or downwards, east or west) but is specific to the arrowprops arguments arrowstyle='->' and connectionstyle="arc3". Using different arrowstyle or connection style will require to set index to different values which can be found by chosing the appropriate indices from the array stored in verts.
In a very general case one can also look at the following:
box = ann.arrow_patch._posA_posB
tbox = fig.transFigure.inverted().transform(leader_line_box)
print tbox
line = Line2D(tbox.T[0], tbox.T[1],transform=fig.transFigure)
However this will get you the line between the annotated point and the text itself. In general this line might be different from the actual arrow, depending in the arrow style in use.
You're almost there, you have the coordinates of the bounding box of the arrow, which is the box drawn using the arrow as the diagonal. From that, we can find the head / tail coordinates.
The bounding box coordinates are given in the order [[left, bottom], [right, top]]. Here, the arrow head is at the top left, and tail is bottom right. So we can draw two lines to visually mark these. Replacing that section in your code with this:
from matplotlib.lines import Line2D
dl = 0.01 # some arbitrary length for the marker line
head = [leader_line_box.T[0][0], leader_line_box.T[1][1]]
line_head = Line2D([head[0],head[0]+dl], [head[1],head[1]+dl],
transform=fig.transFigure, lw=2, color='r') # mark head with red
ax.add_line(line_head)
tail = [leader_line_box.T[0][1], leader_line_box.T[1][0]]
line_tail = Line2D([tail[0],tail[0]+dl], [tail[1],tail[1]+dl],
transform=fig.transFigure, lw=2, color='g') # mark tail with green
ax.add_line(line_tail)
results in the following plot:
I'm plotting an azimuth-elevation curve on a polar plot where the elevation is the radial component. By default, Matplotlib plots the radial value from 0 in the center to 90 on the perimeter. I want to reverse that so 90 degrees is at the center. I tried setting the limits with a call to ax.set_ylim(90,0) but this results in a LinAlgError exception being thrown. ax is the axes object obtained from a call to add_axes.
Can this be done and, if so, what must I do?
Edit: Here is what I'm using now. The basic plotting code was taken from one of the Matplotlib examples
# radar green, solid grid lines
rc('grid', color='#316931', linewidth=1, linestyle='-')
rc('xtick', labelsize=10)
rc('ytick', labelsize=10)
# force square figure and square axes looks better for polar, IMO
width, height = matplotlib.rcParams['figure.figsize']
size = min(width, height)
# make a square figure
fig = figure(figsize=(size, size))
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], projection='polar', axisbg='#d5de9c')
# Adjust radius so it goes 90 at the center to 0 at the perimeter (doesn't work)
#ax.set_ylim(90, 0)
# Rotate plot so 0 degrees is due north, 180 is due south
ax.set_theta_zero_location("N")
obs.date = datetime.datetime.utcnow()
az,el = azel_calc(obs, ephem.Sun())
ax.plot(az, el, color='#ee8d18', lw=3)
obs.date = datetime.datetime.utcnow()
az,el = azel_calc(obs, ephem.Moon())
ax.plot(az, el, color='#bf7033', lw=3)
ax.set_rmax(90.)
grid(True)
ax.set_title("Solar Az-El Plot", fontsize=10)
show()
The plot that results from this is
I managed to put he radial axis inverted. I had to remap the radius, in order to match the new axis:
fig = figure()
ax = fig.add_subplot(1, 1, 1, polar=True)
def mapr(r):
"""Remap the radial axis."""
return 90 - r
r = np.arange(0, 90, 0.01)
theta = 2 * np.pi * r / 90
ax.plot(theta, mapr(r))
ax.set_yticks(range(0, 90, 10)) # Define the yticks
ax.set_yticklabels(map(str, range(90, 0, -10))) # Change the labels
Note that is just a hack, the axis is still with the 0 in the center and 90 in the perimeter. You will have to use the mapping function for all the variables that you are plotting.