2

I am making a polar plot where the ticks are not uniformly distributed around the circle. There are a few very good Q&A pairs that deal with uniformly distributed answers, and they all use a divide up the circle approach. E.g. this.

I'd like to know if it's possible to use the transform that's baked into the label to rotate the text the way I'd like to put it.

I can sort of do this, but I can't work out how to anchor it properly. The code that is doing it is here:

for tick in plt.xticks()[1]:
    tick._transform = tick._transform + mpl.transforms.Affine2D().rotate_deg_around(0, 0, 10)

which gives an output like this:

enter image description here

Whereas I'd like an output like this:

enter image description here

(from the above linked question)

Obviously I'd need a 90° rotation, not a 10° one, but 90° rotates it off the canvas.

Is this approach possible, or do I need to reassess my strategy?

The full code block is here:

import random

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

one_person = {
    "Human": {
        "Collaboration": 4,
        "Growth Mindset": 3,
        "Inclusion": 5,
        "Project and Studio Life": 2,
    },
    "Tectonics": {
        "Office Manual and Procedures": 3,
        "Documentation Standards": 3,
        "Site Stage Services": 2,
        "External and Public Domain Works": 2,
        "Structure": 3,
        "Enclosure": 2,
        "Waterproofing (int. and ext.)": 3,
        "Interiors": 1,
        "Structure and Services": 2,
    },
    "Technology": {
        "Bluebeam": 2,
        "Confluence": 3,
        "Drawing on screens": 0,
        "dRofus": 0,
        "Excel": 2,
        "Grasshopper": 1,
        "InDesign": 2,
        "Outlook": 2,
        "Python": 5,
        "Rhino": 1,
        "Teams": 2,
        "Timesheets and expenses": 3,
    },
    "Regenerative": {
        "REgenerative Design": 3,
        "Materials and Embodied Carbon practice": 1,
        "Materials and Embodied Carbon analysis": 2,
        "Energy": 3,
        "Resilience": 1,
        "Rating Systems": 2,
    },
    "Design": {
        "Predesign - Briefing, Stakeholder Engagement & Establishing Project Values": 2,
        "Predesign - Feasibility Studies And Strategic Organisational Planning": 3,
        "Initiating Design": 2,
        "Conserving Design": 3,
        "Design Communication - Written": 2,
        "Design Communication - Visual": 4,
        "Design Communication - Verbal": 3,
    },
    "Connecting with country": {"Connecting with Country": 2},
}
colours = [
    "b",  # blue.
    "g",  # green.
    "r",  # red.
    "c",  # cyan.
    "m",  # magenta.
    "y",  # yellow.
    "k",  # black.
    # "w",  # white.
]


def draw_radar(data, colour_letters, person_name=""):
    """Draw the graph.

    Based substanitally on this SO thread:
    https://stackoverflow.com/questions/60563106/complex-polar-plot-in-matplotlib
    """
    # not really sure why -1, but if you don't you get an empty segment
    num_areas = len(data) - 1
    running_total = 0
    thetas = {}
    for key, value in data.items():
        this_area_num_points = len(value)
        this_area_theta = ((2 * np.pi) / num_areas) / (this_area_num_points)
        thetas[key] = []
        for i in range(len(value)):
            thetas[key].append((i * this_area_theta) + running_total)
        running_total += (2 * np.pi) / num_areas

    labels = []
    for key, value in data.items():
        for area, score in value.items():
            labels.append(f"{score} {key}: {area}")

    for name, theta_list in thetas.items():
        individual_scores = list(data[name].values())
        colour = random.choice(colour_letters)
        if len(theta_list) > 1:
            plt.polar(theta_list, individual_scores, c=colour, label=name)
        elif len(theta_list) == 1:
            plt.scatter(theta_list, individual_scores, c=colour, label=name)
    plt.yticks(np.arange(-5, 5), [""] * 5 + list(range(5)))
    plt.xticks(
        np.concatenate(tuple(list(thetas.values()))),
        labels,
        transform_rotates_text=True,
    )
    for tick in plt.xticks()[1]:
        tick._transform = tick._transform + mpl.transforms.Affine2D().rotate_deg_around(
            0, 0, 10
        )
    if person_name:
        plt.title = f"Competency for {person_name}"
    plt.savefig("radar.png")


draw_radar(one_person, colours)
Ben
  • 12,614
  • 4
  • 37
  • 69

1 Answers1

3

Rather than using Matplotlib transforms, you can use ax.get_xticklabels(), & then iteratively set the rotation of each label with label.set_rotation(θ) ¹

¹ Where the angle, θ, is derived from the polar data associated with each label; which is effectively equivalent to the x-dimension in the polar plot.

For example, modifying the code you provide, as shown below:

import random

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

one_person = {
    "Human": {
        "Collaboration": 4,
        "Growth Mindset": 3,
        "Inclusion": 5,
        "Project and Studio Life": 2,
    },
    "Tectonics": {
        "Office Manual and Procedures": 3,
        "Documentation Standards": 3,
        "Site Stage Services": 2,
        "External and Public Domain Works": 2,
        "Structure": 3,
        "Enclosure": 2,
        "Waterproofing (int. and ext.)": 3,
        "Interiors": 1,
        "Structure and Services": 2,
    },
    "Technology": {
        "Bluebeam": 2,
        "Confluence": 3,
        "Drawing on screens": 0,
        "dRofus": 0,
        "Excel": 2,
        "Grasshopper": 1,
        "InDesign": 2,
        "Outlook": 2,
        "Python": 5,
        "Rhino": 1,
        "Teams": 2,
        "Timesheets and expenses": 3,
    },
    "Regenerative": {
        "REgenerative Design": 3,
        "Materials and Embodied Carbon practice": 1,
        "Materials and Embodied Carbon analysis": 2,
        "Energy": 3,
        "Resilience": 1,
        "Rating Systems": 2,
    },
    "Design": {
        "Predesign - Briefing, Stakeholder Engagement & Establishing Project Values": 2,
        "Predesign - Feasibility Studies And Strategic Organisational Planning": 3,
        "Initiating Design": 2,
        "Conserving Design": 3,
        "Design Communication - Written": 2,
        "Design Communication - Visual": 4,
        "Design Communication - Verbal": 3,
    },
    "Connecting with country": {"Connecting with Country": 2},
}
colours = [
    "b",  # blue.
    "g",  # green.
    "r",  # red.
    "c",  # cyan.
    "m",  # magenta.
    "y",  # yellow.
    "k",  # black.
]


def draw_radar(data, colour_letters, person_name=""):
    """Draw the graph.

    Based substanitally on this SO thread:
    https://stackoverflow.com/questions/60563106/complex-polar-plot-in-matplotlib
    """

    fig, ax = plt.subplots(
        subplot_kw={"projection": "polar"}, figsize=(10, 10)
    )
    num_areas = len(data) - 1
    running_total = 0
    thetas = {}
    for key, value in data.items():
        this_area_num_points = len(value)
        this_area_theta = ((2 * np.pi) / num_areas) / (this_area_num_points)
        thetas[key] = []
        for i in range(len(value)):
            thetas[key].append((i * this_area_theta) + running_total)
        running_total += (2 * np.pi) / num_areas

    labels = []
    for key, value in data.items():
        for area, score in value.items():
            labels.append(f"{score} {key}: {area}")

    for name, theta_list in thetas.items():
        individual_scores = list(data[name].values())
        colour = colour_letters.pop()  # random.choice(colour_letters)
        if len(theta_list) > 1:
            ax.plot(theta_list, individual_scores, c=colour, label=name)
        elif len(theta_list) == 1:
            ax.scatter(theta_list, individual_scores, c=colour, label=name)
    ax.set_yticks(np.arange(-5, 5), [""] * 5 + list(range(5)))
    ax.set_xticks(
        np.concatenate(tuple(list(thetas.values()))), labels,
    )

    plt.gcf().canvas.draw()
    max_label_len = max(list(map(len, labels)))
    t_labels = []
    for label in ax.get_xticklabels():
        x, y = label.get_position()
        text = label.get_text()
        angle = x
        y_adjust = (len(text) / max_label_len) * 0.8
        if text.endswith("Country"):
            x_adjust = 0.05
            angle += 0.05
        else:
            x_adjust = 0
        lab = ax.text(
            x + x_adjust,
            y - y_adjust,
            label.get_text(),
            transform=label.get_transform(),
            ha=label.get_ha(),
            va=label.get_va(),
        )
        if np.cos(angle) < 0:
            angle = angle + np.pi
        angle = np.rad2deg(angle)
        lab.set_rotation(angle)
        t_labels.append(lab)
    ax.set_xticklabels([])
    plt.show()


draw_radar(one_person, colours)

results in: Matplotlib polar plot with 'theta' angle-specific rotated tick labels

You may want to adjust the settings for:

  1. the figsize (in the defining of the fig and ax Figure and Axes objects), and correspondingly,
  2. the y_adjust factor (set to 0.8 above).

The general logic is to find the plotted theta angle for each polar data value, and then use that angle value itself as a tick-specific rotation for the label text. The np.cos code checks and appropriately rotates upside-down labels. And each label is moved outwards (i.e. negatively in the y-dimension) by an amount based on its normalized label text length (with respect to the max-length label), resulting in all labels more or less being precisely situated outside the circle (i.e., the longer the text label, the more it has to be, and is, moved).

Also, there was one label (which ends with 'Country'), that overlaps another - for that one I custom move it upwards and slightly bend its angle of tick label rotation to reflect that it actually belongs to / should point to data with the same theta angle on the polar graph as that of the tick label's data right below it.

John Collins
  • 2,067
  • 9
  • 17
  • I'd like to upvote but the formatting of the first paragraph is, I think, horrible. – gboffi Aug 18 '23 at 07:34
  • @gboffi I've cleaned up the intro to the answer; perhaps that is better – John Collins Aug 18 '23 at 11:25
  • I'm sorry, but it was better before (you have removed not only the formatting, but also useful information). I advice you to rollback! And yes, that's me... – gboffi Aug 18 '23 at 11:34