I'm trying to add annotations arrows to a plotly polar plot, but that produces another underlying cartesian plot:
Question 1: is there a way to add annotation to polar plots? This page of 2019 suggest that not, is it still the current status?: https://community.plotly.com/t/adding-annotations-to-polar-scatterplots/704
Question 2: is there a hack to produce arrows on a polar plot? (hide aligned overlying transparent cartesian plot? low level draw of arrows with lines in polar plot?)
The code producing the above image is here:
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
df = pd.DataFrame()
fig = px.scatter_polar(
df,
theta=None,
r=None,
range_theta=[-180, 180],
start_angle=0,
direction="counterclockwise",
template="plotly_white",
)
x_end = [1, 2, 2]
y_end = [3, 5, 4]
x_start = [0, 1, 3]
y_start = [4, 4, 4]
list_of_all_arrows = []
for x0, y0, x1, y1 in zip(x_end, y_end, x_start, y_start):
arrow = go.layout.Annotation(dict(
x=x0,
y=y0,
xref="x", yref="y",
text="",
showarrow=True,
axref="x", ayref='y',
ax=x1,
ay=y1,
arrowhead=3,
arrowwidth=1.5,
arrowcolor='rgb(255,51,0)', )
)
list_of_all_arrows.append(arrow)
fig.update_layout(annotations=list_of_all_arrows)
fig.show()
Edit:
In fine, I would like to have something like that, with annotation relative to polar plot:
As it seems there is still no way to add annotations to polar plot, I did it myself using really scatter plot (cartesian grid), hidden, on which I plotted lines to represent a polar grid.
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
def polar2z(r, theta):
# theta is given in trigo convention, from 0° = X axis
imZ = np.array(r) * np.exp(1j * np.array(theta))
return np.real(imZ), np.imag(imZ)
def polar_graph_with_arrows(r_max, r_min, theta, colors):
df = pd.DataFrame()
figDLC = px.scatter(
df,
x=None,
y=None,
template="plotly_white")
rm_max = 1.1 # margin radius
# --- Draw polar grid ---
# radial grid and labels
list_of_labels = []
figDLC.update_layout(margin=dict(l=10, r=10, b=10, t=40)) # https://stackoverflow.com/questions/60913366/how-to-annotate-a-point-outside-the-plot-itself
for t in np.arange(0, 360, 30):
x_min, y_min = 0, 0
x_max, y_max = polar2z(1, t * np.pi / 180.)
figDLC.add_trace(go.Scatter(
x=[x_min, x_max],
y=[y_min, y_max],
mode='lines',
name='_',
line={'width': 1},
line_color='LightGrey',
hoverinfo='skip'))
offsetx, offsety = polar2z(rm_max, t * np.pi / 180.) # radial offset of 0.05
label_t = "{:d}°".format(np.mod(t+180, 360) - 180) # set t between [-180, 180[
label = go.layout.Annotation(dict(
text=label_t,
xref="x", yref="y", # axes-positioned annotation
font=dict(color="black", size=12),
x=offsetx, y=offsety, showarrow=False))
list_of_labels.append(label)
# circles grid
t = np.arange(0, 360 + 5, 5)
# secondary lines
for r in np.arange(0, 1 + 0.25, 0.25):
x, y = polar2z(r, t * np.pi / 180.)
figDLC.add_trace(go.Scatter(
x=x,
y=y,
mode='lines',
name='_',
line={'width': 1},
line_color='LightGrey',
hoverinfo='skip'))
# secondary lines
for r in np.arange(0, 1 + 0.25, 0.25):
x, y = polar2z(r, t * np.pi / 180.)
figDLC.add_trace(go.Scatter(
x=x,
y=y,
mode='lines',
name='_',
line={'width': 1},
line_color='LightGrey',
hoverinfo='skip'))
# --- add arrows ---
list_of_all_arrows = []
# theta shall be given in trigo convention, from 0° = X axis
x_min, y_min = polar2z(r_min, theta * np.pi / 180.)
x_max, y_max = polar2z(r_max, theta * np.pi / 180.)
for x0, y0, x1, y1, theta, color, name in zip(x_min, y_min, x_max, y_max, t, colors, r_max):
arrow = go.layout.Annotation(dict(
x=x0,
y=y0,
xref="x", yref="y",
text="",
name=name,
showarrow=True,
axref="x", ayref='y',
ax=x1,
ay=y1,
arrowhead=3,
arrowwidth=1.5 * 2,
arrowcolor=str(color),
)
)
list_of_all_arrows.append(arrow)
# update custom data depending on frame
theta_cd = theta
# add a single point marker to get hover info
figDLC.add_trace(go.Scatter(
x=[x1],
y=[y1],
name=name,
marker={'color': str(color), 'size': 12},
customdata=np.stack(([theta_cd], [name]), axis=-1),
hovertemplate="<br>".join([
"dir: %{customdata[0]:d}°",
"r: %{customdata[1]}"])
))
figDLC.update_layout(hovermode='closest', # default, may be "x" | "y" | "closest" | False | "x unified" | "y unified"
hoverdistance=100) # default 20 (px), distance at which points are detected
# switch off real cartesian axes
figDLC.update_xaxes(showgrid=False, zerolinecolor='LightGrey', visible=False)
figDLC.update_yaxes(showgrid=False, zerolinecolor='LightGrey', visible=False)
figDLC.update_layout(
title="Polar arrows",
showlegend=False
)
figDLC.update_yaxes(
scaleanchor="x",
scaleratio=1,
range=(-rm_max, rm_max),
constrain='domain'
)
figDLC.update_xaxes(
range=(-rm_max, rm_max),
constrain='domain'
)
figDLC.update_layout(annotations=list_of_labels + list_of_all_arrows)
return figDLC
if __name__ == "__main__":
# user parameters for 3 arrows:
r_max = np.array([0.5, 0.7, 0.9]) # radius of arrow start
r_min = 0.025 # radius of arrow tip:
# -r_max for an arrow going through the center,
# - 0.025 (or any small value) for an arrow pointing to the center
theta = np.array([15, 20, 35]) # orientation in degrees
colors = ['Gold', 'deepskyblue', 'DarkBlue'] # colors may also be in rgb: 'rgb(51,255,51)'
fig = polar_graph_with_arrows(r_max, r_min, theta, colors)
fig.show()
In bonus, a hover on arrow start gives more information on data.