Search code examples
pythonplotlypolar-coordinatespolar-plot

Annotation in a plotly polar plot


I'm trying to add annotations arrows to a plotly polar plot, but that produces another underlying cartesian plot:

arrow added to cartesian plot not polar

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: enter image description here


Solution

  • 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()
    

    This shows this as wanted: polar plot with arrows annotation in plotly

    In bonus, a hover on arrow start gives more information on data.