How do I curve text in a polar plot? - python-3.x

Hello Matplotlib Experts,
How do I curve text in a matplotlib polar plot? In my attempt below, my code rotates each char individually, but doing so would remove the natural spacing of each font. Can somebody describe a solution for passing ax.text in matplotlib?
import numpy as np
import matplotlib as mpl
import matplotlib.pylab as plt
def curveText(text, height, minTheta, maxTheta, ax):
interval = np.arange(minTheta, maxTheta, .022)
if( maxTheta <= np.pi):
progression = interval[::-1]
rotation = interval[::-1] - np.arctan(np.tan(np.pi/2))
else:
progression = interval
rotation = interval - np.arctan(np.tan(np.pi/2)) - np.pi
## Render each letter individually
for i, rot, t in zip(progression, rotation, text):
ax.text(i, height, t, fontsize=11,rotation=np.degrees(rot), ha='center', va='center')
def buildCircularHeatMap( data=None, label=None, cmaps=None, categorymap=None, vmin=0, vmax=None ):
(xDim, yDim) = data.shape
if cmaps == None:
cmaps = [mpl.cm.get_cmap()] * yDim
BOTTOM = xDim / 100 * 120
#FONTSIZE = 1 if xDim/100*8 < 1 else xDim/100*8
theta = np.linspace(0.0, 2 * np.pi - 5 * np.pi/180, xDim, endpoint=False)
width = (2*np.pi - 5 * np.pi/180)/xDim
ax = plt.subplot(111, polar=True)
ax.grid(False)
ax.set_yticklabels([])
ax.set_xticklabels([])
categorysum = np.zeros(len(categorymap))
for x in label:
categorysum[int(float( x )) - 1] += 1
categorysum = categorysum/np.sum(categorysum)*2*np.pi
## Build Face Color Values
for i in range(yDim):
cmap_scalar = mpl.cm.ScalarMappable(cmap=cmaps[i])
cmap_scalar.set_clim(vmin=vmin, vmax=vmax)
facecolor = cmap_scalar.to_rgba(data[:,i])
_ = ax.text(2 * np.pi - 5 * np.pi/180, BOTTOM+i*10, str(i), fontsize=11, rotation=np.degrees(270))
bars = ax.bar(theta, np.ones(xDim)*10, width=width, bottom=BOTTOM+i*10)
for j, b in enumerate(bars):
b.set_facecolor( facecolor[j] )
## Build CCS Label
for th, l, bar in zip(theta, label, bars):
rot = np.arctan(np.tan(th))
ax.text(th,BOTTOM+yDim*10+bar.get_height()+5, l, rotation_mode='anchor',
rotation=np.degrees(rot), fontsize=11, ha='center', va='center')
## Build Category Label
categoryColor = np.asarray([int(float(c)) for c in label])
bars = ax.bar(theta, np.ones(xDim)*20, width=width, bottom=BOTTOM+yDim*10 + 30)
for j, b in enumerate(bars):
b.set_facecolor(np.asarray([0.0,0.0,0.0]))
if categoryColor[j] % 2 == 0:
b.set_alpha(0.07)
else:
b.set_alpha(0.0)
for i in range(len(categorymap)):
c = i + 1
t = theta[categoryColor==c]
mi = np.min(t)
ma = np.max(t)
rad = (ma-mi)/2+mi
curveText(categorymap[c], BOTTOM+yDim*10+40, mi, ma, ax)
if __name__ == "__main__":
categorymap={
1: "Infectious & parasitic dieases",
2: "Neoplasms",
3: "Endocrine; nutritional; and metabolic diseases and immunity disorders",
4: "Diseases of the blood and blood-forming organs",
5: "Mental Illness",
6: "Nervous system disorders",
7: "Circulatory disorders",
8: "Respiratory disorders",
9: "Digestive disorders",
10: "Genitourinary disorders",
11: "Complications of pregnancy; childbirth; and the puerperium",
12: "Skin and subcutaneous tissue disorder",
13: "Musculoskeletal system and connective tissue disorder",
14: "Congenital anomalies",
15: "Certain conditions originating in the perinatal period",
16: "Injury and poisoning",
17: "Ill-defined status",
18: "Unclassified"
}
data = np.random.standard_normal((180, 3))
colormaps = [mpl.cm.get_cmap("Reds"), mpl.cm.get_cmap("Oranges"), mpl.cm.get_cmap("Greens"), mpl.cm.get_cmap("Blues")]
labels = sorted([ '{:.2f}'.format(np.abs(i)) for i in np.random.random_sample(180) * 18 + 1 ])
fig = plt.figure(figsize=(11,11))
buildCircularHeatMap(data=data, label=labels, cmaps=colormaps, categorymap=categorymap)
plt.show()
In the link below, Thomas's answer seems only applicable for cartesian coordinates and my current attempt should be similar to Daan.
Curved text rendering in matplotlib

As #Makdous suggested above, Curved text rendering in matplotlib is a nice implementation of the problem. I read through the code, and you're right, it is in cartesian coordinates, but I think you could just modify it a bit and get it working using these formulas:
You can also use this one line function I wrote:
from typing import Tuple
from math import sqrt, degrees, atan2
def cartesian_to_polar(x: float, y: float)-> Tuple[float, float]:
return sqrt(x**2 + y ** 2), degrees(atan2(y,x))
Or, if you have polar coordinates and want to make it work with the script linked in the other response, you can use this:
from math import cos, sin, radians
def polar_to_cartesian(r: float, theta: float)-> Tuple[float, float]:
return r * cos(radians(theta)), r * sin(radians(theta))
Depending on how you implemented it, you could feed it in the coordinates you have, then convert it appropriately to arrive at cartesian coordinates and run the linked script, then convert the points back to polar coordinates and plot it.

Related

Python polar bar chart - Remove degrees & color one ring

I have been trying to create a polar bar chart in python for quite some time. After some research I managed to get the results that I wanted. Well, almost. There're still a couple thing that I don't know how to do.
I include my code:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import FixedLocator
from operator import add
#DATA MANIPULATION
dataset = pd.read_csv('Controls.csv', delimiter=";")
dataset.astype({'Rating':'float'})
#print(dataset.dtypes)
categories = dataset['Category'].drop_duplicates()
controls = dataset['Control'].drop_duplicates()
categ_avg = []
control_average = []
#Average for controls
for category in categories:
avg = 0
for index, item in dataset.iterrows():
if item['Category'] == category:
avg += item['Rating']
categ_avg.append(avg)
avg = 0
for control in controls:
avg = 0
for index, item in dataset.iterrows():
if item['Control'] == control:
avg += item['Rating']
control_average.append(avg)
avg = 0
average = [total / 5 for total in categ_avg]
avgdf = pd.DataFrame({
'Category' : categories,
#'Controls' : controls,
'Average' : average
})
#PLOTTING
#Compute pie slices which is the number of unique controls
N = len(controls)
#theta = np.linspace(0, 2 * np.pi, N, endpoint=False)
theta = [0]
for cat in categories:
if cat == 'CAT-A':
theta.append( theta[-1] + (2 * np.pi/N * 2) )
else:
theta.append( theta[-1] + (2*np.pi / N) )
print(theta)
#Compute the filling axis
mid_theta = []
for cat in categories:
if cat == 'CAT-A':
mid_theta.append( 2 * np.pi/N )
else:
mid_theta.append( 2 * np.pi / N / 2 )
mid_theta = list(map(add,theta, mid_theta))
print(mid_theta)
radii = avgdf['Average']
#width = theta[1] - theta[0]
width = []
for i in range(0, len(avgdf['Average'])):
width.append(theta[i+1] - theta[i])
fig = plt.figure()
fig.patch.set_facecolor('white')
fig.patch.set_alpha(0.5)
#Draw X labels
ax = fig.add_subplot(111, projection='polar')
ax.set_xticks(theta)
# Draw ylabels
ax.set_rlabel_position(0)
ax.set_yticks([1, 2, 3, 4, 5])
ax.set_yticklabels(["1", "2", "3", "4", "5"], color="black", size=8)
ax.set_ylim(0, 5)
#colors = plt.cm.hsv(theta/2/np.pi)
bars = ax.bar(mid_theta, radii, width=width, bottom=0.0)
#Labels
for bar, angle, label in zip(bars, mid_theta, avgdf["Category"]):
# Labels are rotated. Rotation must be specified in degrees :(
rotation = np.rad2deg(angle)
# Flip some labels upside down
alignment = ""
if angle >= np.pi/2 and angle < 3*np.pi/2:
alignment = "right"
rotation = rotation + 180
else:
alignment = "left"
# Finally add the labels
ax.text(
x=angle,
y=5.5,
s=label,
ha=alignment,
va='center')
#Use custom colors and opacity
for r, bar in zip(avgdf['Average'], bars):
bar.set_facecolor(plt.cm.viridis(r/5.))
bar.set_alpha(0.5)
plt.show()
When I execute it I obtain the following graph: Resulting graph
What I'm trying to achieve is:
I would like to color the ring number 4 in green.
I would like to remove the degrees from the outer ring. I only want to see my categories not the 0, 144º...
I really appreciate the help.
Thanks you.
Create a list of colours with as many colours as you have polar bars.
c = ['blue', 'blue', 'blue', 'green', 'blue', 'blue']
bars = ax.bar(
x=angles,
height=heights,
width=width,
color=c,
linewidth=2,
edgecolor="white")

Place and insert plane image along path using matplotlib

My code is a fair bit more advanced, but in simple terms I am looking to place and rotate an image of a plane along a path using matplotlib. Ideally I would be able to select the angle and how far along the path the image should be placed. Any ideas? My ideal output would be something like this (ignoring the coordinates I already fixed that in my real code).
Image of Norway used:
Code
import matplotlib.pyplot as plt
import matplotlib.image as img
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def x2map(x, x_scale):
return x * x_scale
def y2map(y, y_scale):
return (1 - y) * y_scale
if __name__ == "__main__":
image_url = "Norge2.png"
# Obtains the scaling for the figure
map = img.imread(image_url)
fig, ax = plt.subplots()
im = ax.imshow(map)
_, x_scale = plt.xlim()
y_scale, _ = plt.ylim()
# Fixes the axis to 0-1 and 0-1
positions_x = [i * x_scale / 10 for i in range(0, 11)]
positions_y = [i * y_scale / 10 for i in range(0, 11)]
labels = [i / 10 for i in range(0, 11)]
ax.set_xticks(positions_x)
ax.set_xticklabels([i / 10 for i in range(0, 11)])
ax.set_yticks(positions_y)
ax.set_yticklabels([(10 - i) / 10 for i in range(0, 11)])
route_color = "red"
route_ls = "-"
city_marker ="o"
city_color = "red"
A = [x2map(0.125,x_scale), y2map(0.14,y_scale)]
B = [x2map(0.772,x_scale), y2map(0.92,y_scale)]
plt.plot(
[A[0], B[0]], [A[1], B[1]], marker='o', color=route_color, ls=route_ls
)
plt.show()

Setting multiple sets of minor ticks with Matplotlib

I am trying to customize my x-axis with minor ticks but I want to bolden or lengthen the middle minor tick. So basically I want a major, middle, and minor tick for my subplot. I tried using tick_params but that overwrites the previous modifications to the ticks. Is there another way to achieve this?
The end result should have 4 minor ticks and 2 semi-minor ticks between the labeled numbers.
Bonus points if there is a way to label the semi-minor ticks.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,
AutoMinorLocator)
t = np.arange(0.0, 100.0, 0.1)
s = np.sin(0.1 * np.pi * t) * np.exp(-t * 0.01)
fig, ax = plt.subplots()
ax.plot(t, s)
#Try to set 4 minor ticks
ax.xaxis.set_minor_locator(AutoMinorLocator(4))
ax.tick_params(which='minor', length=2, color='k')
#Set 2 minor ticks that are larger
ax.xaxis.set_minor_locator(AutoMinorLocator(2))
ax.tick_params(which='minor', length=6, color='k')
#Make major ticks even larger
ax.tick_params(which='major', length=12, color='k')
plt.show()
Two ways. The first one is messy and it's probably better to avoid it, but it doesn't hurt to include it here. The second one, as suggested by ImportanceOfBeingErnest and is based on How to add third level of ticks in python matplotlib
fig, ax = plt.subplots()
ax.plot(t, s)
ax.xaxis.set_minor_locator(AutoMinorLocator(9))
ax.tick_params(which='major', length=12, color='k')
minor_labels = list()
i = 0
l = -2
sum_l = 1.125
for line in ax.xaxis.get_ticklines(minor=True):
if i == 0:
line.set_markersize(4)
minor_labels.append('')
i += 1
l += sum_l
elif i == 2:
line.set_markersize(4)
minor_labels.append('')
i += 1
l += sum_l
elif i == 4 or i == 6 or i == 10 or i == 12 or i == 16:
line.set_markersize(4)
minor_labels.append('')
i += 1
l += sum_l
elif i == 18:
line.set_markersize(4)
minor_labels.append('')
i -= 15
l += sum_l
elif i == 8 or i == 14:
line.set_markersize(8)
minor_labels.append(l)
i += 1
l += sum_l
else:
i += 1
l += sum_l
print(minor_labels)
ax.set_xticklabels(minor_labels,minor=True)
plt.setp(ax.xaxis.get_minorticklabels(), rotation=45)
plt.setp(ax.xaxis.get_ticklabels(), rotation=45)
plt.show()
ImportanceOfBeingErnest's solution seems to follow this logic — maybe he had something else in mind —It looks nicer, but I haven't been able to reproduce the minor / semi-minor tick combination. Perhaps with some more time you will be able to do what you intend by tweaking it.
t = np.arange(0.0, 100.0, 0.1)
s = np.sin(0.1 * np.pi * t) * np.exp(-t * 0.01)
fig, ax = plt.subplots()
plt.plot(t, s)
ax2 = ax.twiny()
ax2.plot(t, s)
ax2.xaxis.set_ticks_position('bottom')
ax.xaxis.set_minor_locator(ticker.FixedLocator(np.arange(5, 100.01, 5)))
ax.tick_params(which='minor', length=8, color='k')
ax2.xaxis.set_major_locator(ticker.FixedLocator([]))
ax2.xaxis.set_minor_locator(ticker.FixedLocator(np.arange(1.25, 100.01,1.25)))
ax2.tick_params(which='minor', length=4, color='k')
ax.tick_params(which='major', length=12, color='k')
ax.set_xticklabels(np.arange(5, 100.01, 5),minor=True)
plt.setp(ax.xaxis.get_minorticklabels(), rotation=45)
plt.setp(ax.xaxis.get_ticklabels(), rotation=45)
plt.show()

Animating multiple Circles in each frames in Python

I am trying to create the animation in this video using Python. But I stuck on the very first step. Till now I've created a Circle and a point rotating around its circumference. My code is given below. Now I want to plot the y values corresponding to x=np.arange(0, I*np.pi, 0.01) along the x-axis (as shown in update() function in the code). For this I have to define another function to plot these x and y and pass that function inside a new animation.FuncAnimation().
Is there any way to plot everything using only the update() function?
Note I have found a code of this animation in here. But it is written in Java!
My Code
import matplotlib.pyplot as plt
from matplotlib import animation
import numpy as np
W = 6.5
H = 2
radius = 1
I = 2
T = 3
N = 2
plt.style.use(['ggplot', 'dark_background'])
def create_circle(x, y, r):
circle = plt.Circle((x, y), radius=r, fill=False, alpha=0.7, color='w')
return circle
def create_animation():
fig = plt.figure()
ax = plt.axes(xlim=(-2, W + 2), ylim=(-H, H))
circle = create_circle(0, 0, radius)
ax.add_patch(circle)
line1, = ax.plot(0, 1, marker='o', markersize=3, color='pink', alpha=0.7)
def update(theta):
x = radius * np.cos(theta)
y = radius * np.sin(theta)
line1.set_data([0, x], [0, y])
return line1,
anim = []
anim.append(animation.FuncAnimation(fig, update,
frames=np.arange(0, I * np.pi, 0.01),
interval=10, repeat=True))
# anim.append(animation.FuncAnimation(fig, update_line, len(x),
# fargs=[x, y, line, line1], interval=10))
plt.grid(False)
plt.gca().set_aspect('equal')
plt.gca().spines['left'].set_visible(False)
plt.gca().spines['top'].set_visible(False)
plt.gca().spines['right'].set_visible(False)
plt.gca().spines['bottom'].set_visible(False)
plt.gca().set_xticks([])
plt.gca().set_yticks([])
plt.show()
if __name__ == '__main__':
create_animation()
Edit. I've improved the task by defining a global variable pos and changing the update() function in the following manner ...The animation now looks better but still having bugs!
Improved Portion
plot, = ax.plot([], [], color='w', alpha=0.7)
level = np.arange(0, I * np.pi, 0.01)
num = []
frames = []
for key, v in enumerate(level):
num.append(key)
frames.append(v)
def update(theta):
global pos
x = radius * np.cos(theta)
y = radius * np.sin(theta)
wave.append(y)
plot.set_data(np.flip(level[:pos] + T), wave[:pos])
line1.set_data([0, x], [0, y])
pos += 1
return line1, plot,
Edit Till now I've done the following:
def update(theta):
global pos
x, y = 0, 0
for i in range(N):
prev_x = x
prev_y = y
n = 2 * i + 1
rad = radius * (4 / (n * np.pi))
x += rad * np.cos(n * theta)
y += rad * np.sin(n * theta)
wave.append(y)
circle = create_circle(prev_x, prev_y, rad)
ax.add_patch(circle)
plot.set_data(np.flip(level[:pos] + T), wave[:pos])
line2.set_data([x, T], [y, y])
line1.set_data([prev_x, x], [prev_y, y])
pos += 1
return line1, plot, line2,
Output
Please help to correct this animation. Or, is there any efficient way to do this animation?
Edit Well, now the animation is partially working. But there is a little issue: In my code (inside the definition of update()) I have to add circles centered at (prev_x, prev_y) of radius defined as rad for each frame. For this reason I try to use a for loop in the definition of update() but then all the circles remains in the figure (see the output below). But I want one circle in each frame with the centre and radius as mentioned above. Also the same problem is with the plot. I try to use ax.clear() inside the for loop but it didn't work.

Rectangle/Rectangle Collision Detection

I am trying to solve an issue when two rectangles intersect/overlap each other. when this happens, i want to know if intersection is True or False. I found a solution, however it is written in C or C++. I want to write these code in Python.
Here is the source: http://www.jeffreythompson.org/collision-detection/rect-rect.php
This is literally the first line of python code I've ever written (I do know C++ however)
def rectRect(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h):
# are the sides of one rectangle touching the other?
return r1x + r1w >= r2x and \ # r1 right edge past r2 left
r1x <= r2x + r2w and \ # r1 left edge past r2 right
r1y + r1h >= r2y and \ # r1 top edge past r2 bottom
r1y <= r2y + r2h # r1 bottom edge past r2 top
IMHO rectRect is a really bad name for the function, I kept it from the linked code however.
Following is simple class that can perform both rectangle-rectangle intersection as well as point to rectangle intersection. The difference between earlier solution is that following snippet allows even detection of rotated rectangles.
import numpy as np
import matplotlib.pyplot as plt
class Rectangle:
def __init__(self, center: np.ndarray, dims: np.ndarray, angle: float):
self.corners = self.get_rect_points(center, dims, angle)
self.area = dims[0] * dims[1]
#staticmethod
def get_rect_points(center: np.ndarray, dims: np.ndarray, angle: float):
"""
returns four corners of the rectangle.
bottom left is the first conrner, from there it goes
counter clockwise.
"""
center = np.asarray(center)
length, breadth = dims
angle = np.deg2rad(angle)
corners = np.array([[-length/2, -breadth/2],
[length/2, -breadth/2],
[length/2, breadth/2],
[-length/2, breadth/2]])
rot = np.array([[np.cos(angle), np.sin(angle)], [-np.sin(angle), np.cos(angle)]])
corners = rot.dot(corners.T) + center[:, None]
return corners.T
def is_point_in_collision(self, p: np.ndarray):
"""
check if a point is in collision with the rectangle.
"""
def area_triangle(a, b, c):
return abs((b[0] * a[1] - a[0] * b[1]) + (c[0] * b[1] - b[0] * c[1]) + (a[0] * c[1] - c[0] * a[1])) / 2
area = 0
area += area_triangle(self.corners[0], p, self.corners[3])
area += area_triangle(self.corners[3], p, self.corners[2])
area += area_triangle(self.corners[2], p, self.corners[1])
area += area_triangle(self.corners[1], p, self.corners[0])
return area > self.area
def is_intersect(self, rect_2: Rectangle):
"""
check if any of the four corners of the
rectangle is in collision
"""
if not np.all([self.is_point_in_collision(c) for c in rect_2.corners]):
return True
return False
def plot_rect(p1, p2, p3, p4, color='r'):
ax.plot([p1[0], p2[0]], [p1[1], p2[1]], color)
ax.plot([p2[0], p3[0]], [p2[1], p3[1]], color)
ax.plot([p3[0], p4[0]], [p3[1], p4[1]], color)
ax.plot([p4[0], p1[0]], [p4[1], p1[1]], color)
mid_point = 0.5 * (p1 + p3)
plt.scatter(mid_point[0], mid_point[1], marker='*')
plt.xlim([-1, 1])
plt.ylim([-1, 1])
Following are two samples:
Sample 1:
ax = plt.subplot(111)
st = Rectangle((0.067, 0.476),(0.61, 0.41), 90)
gripper = Rectangle((-0.367, 0.476),(0.21,0.16), 45)
plot_rect(*st.corners)
plot_rect(*gripper.corners)
plt.show()
print(f"gripper and rectangle intersect: {st.is_intersect(gripper)}")
Sample 2:
ax = plt.subplot(111)
st = Rectangle((0.067, 0.476),(0.61, 0.41), 90)
gripper = Rectangle((-0.167, 0.476),(0.21,0.16), 45)
plot_rect(*st.corners)
plot_rect(*gripper.corners)
plt.show()
print(f"gripper and rectangle intersect: {st.is_intersect(gripper)}")

Resources