Search code examples
pythonmatplotliblabelpolar-coordinatescartesian-coordinates

How to plot horizontal lines with text (i.e. a label) on a polar coordinates matplotlib plot? (Python)


I'm trying to label the nodes in my polar coordinates plot. There are 3 "axes" that are split and I have figured out how to use the quadrants to select which nodes to label. However, I can't figure out how to align these on the edge of the plot (i.e. axis_maximum). I have spent several hours trying to figure this out. My best option was to pad with . on the left or right but this was a fixed number and got messy when there were too many points. Also, this method went too far outside the "circular" nature of the plot when there were a lot of points. I did some trigonometry to figure out the lengths for everything but this was difficult to implement using text units such as ..

If anyone can help it would be greatly appreciated. I showed what the plot looks like below and then added in red what I am trying to implement. label in the mock figure corresponds to name_node in the for-loop. Ideally I would like to steer away from using characters like . and would rather use an actual matplotlib Line object so I can specify linestyle like : or -.

In summary, I would like to do the following:

  1. Add horizontal lines stretching from my "axis" to the outer edge of the plot (either the right or the left depending on the quadrant)
  2. At the end of the line in (1), I would like to add the name_node text.

EDIT:

  • I've added an attempt to overlay a cartesian axis and then plot the lines on this. No success.

import numpy as np
from numpy import array # I don't like this but it's for loading in the pd.DataFrame
import pandas as pd 
import matplotlib.pyplot as plt
df = pd.DataFrame({'node_positions_normalized': {'iris_100': 200.0, 'iris_101': 600.0, 'iris_102': 1000.0, 'iris_0': 200.0, 'iris_1': 600.0, 'iris_2': 1000.0, 'iris_50': 200.0, 'iris_51': 600.0, 'iris_52': 1000.0}, 'theta': {'iris_100': array([5.42070629, 6.09846678]), 'iris_101': array([5.42070629, 6.09846678]), 'iris_102': array([5.42070629, 6.09846678]), 'iris_0': array([1.23191608, 1.90967657]), 'iris_1': array([1.23191608, 1.90967657]), 'iris_2': array([1.23191608, 1.90967657]), 'iris_50': array([3.32631118, 4.00407168]), 'iris_51': array([3.32631118, 4.00407168]), 'iris_52': array([3.32631118, 4.00407168])}})
axis_maximum = df["node_positions_normalized"].max()
thetas = np.unique(np.stack(df["theta"].values).ravel())


def pol2cart(rho, phi):
    x = rho * np.cos(phi)
    y = rho * np.sin(phi)
    return(x, y)

def _get_quadrant_info(theta_representative):
    # 0/360
    if theta_representative == np.deg2rad(0):
        quadrant = 0
    # 90
    if theta_representative == np.deg2rad(90):
        quadrant = 90
    # 180
    if theta_representative == np.deg2rad(180):
        quadrant = 180
    # 270
    if theta_representative == np.deg2rad(270):
        quadrant = 270

    # Quadrant 1
    if np.deg2rad(0) < theta_representative < np.deg2rad(90):
        quadrant = 1
    # Quadrant 2
    if np.deg2rad(90) < theta_representative < np.deg2rad(180):
        quadrant = 2
    # Quadrant 3
    if np.deg2rad(180) < theta_representative < np.deg2rad(270):
        quadrant = 3
    # Quadrant 4
    if np.deg2rad(270) < theta_representative < np.deg2rad(360):
        quadrant = 4
    return quadrant
    
    
with plt.style.context("seaborn-white"):
    fig = plt.figure(figsize=(8,8))
    ax = plt.subplot(111, polar=True)
    ax_cartesian = fig.add_axes(ax.get_position(), frameon=False, polar=False)
    ax_cartesian.set_xlim(-axis_maximum, axis_maximum)
    ax_cartesian.set_ylim(-axis_maximum, axis_maximum)

    # Draw axes
    for theta in thetas:
        ax.plot([theta,theta], [0,axis_maximum], color="black")
        
    # Draw nodes
    for name_node, data in df.iterrows():
        r = data["node_positions_normalized"]
        for theta in data["theta"]:
            ax.scatter(theta, r, color="teal", s=150, edgecolor="black", linewidth=1, alpha=0.618)
        # Draw node labels
        quadrant = _get_quadrant_info(np.mean(data["theta"]))
 
        # pad on the right and push label to left
        if quadrant in {1,4}:
            theta_anchor_padding = min(data["theta"])
        # pad on left and push label to the right
        if quadrant in {2,3}:
            theta_anchor_padding = max(data["theta"])
            
        # Plot
        ax.text(
            s=name_node,
            x=theta_anchor_padding,
            y=r,
            horizontalalignment="center",
            verticalalignment="center",
        )
    
    ax.set_rlim((0,axis_maximum))
    
    # Convert polar to cartesian and plot on cartesian overlay?
    xf, yf = pol2cart(theta_anchor_padding, r) #fig.transFigure.inverted().transform(ax.transData.transform((theta_anchor_padding, r)))
    ax_cartesian.plot([xf, axis_maximum], [yf, yf])

enter image description here


Solution

  • You can use annotate instead of text, this allows you to specify the text coordinates and the text coordinate system independently of the point coordinates. We place the text in figure coordinates (0 to 1, see here for details). It's important to get the transformation from data to figure coordinates after the r limit is set.

    with plt.style.context("seaborn-white"):
        fig = plt.figure(figsize=(8,8))
        ax = plt.subplot(111, polar=True)
        ax.set_rlim((0,axis_maximum))
        ann_transf = ax.transData + fig.transFigure.inverted() 
    
        # Draw axes
        for theta in thetas:
            ax.plot([theta,theta], [0,axis_maximum], color="black")
        
        
        # Draw nodes
        for name_node, data in df.iterrows():
            r = data["node_positions_normalized"]
            for theta in data["theta"]:
                ax.scatter(theta, r, color="teal", s=150, edgecolor="black", linewidth=1, alpha=0.618)
            # Draw node labels
            quadrant = _get_quadrant_info(np.mean(data["theta"]))
     
            # pad on the right and push label to left
            if quadrant in {1,4}:
                theta_anchor_padding = min(data["theta"])
            # pad on left and push label to the right
            if quadrant in {2,3}:
                theta_anchor_padding = max(data["theta"])
                
            # Plot
            _,y = ann_transf.transform((theta_anchor_padding, r))
            ax.annotate(name_node, 
                        (theta_anchor_padding,r), 
                        (0.91 if quadrant in {1,4} else 0.01, y),
                        textcoords='figure fraction',
                        arrowprops=dict(arrowstyle='-', color='r'),
                        color='r',
                        verticalalignment='center'
            )
    

    enter image description here