5

I have a radar chart. Need to change the grid from circle-form to pentagon-form. Currently, I have this output:

enter image description here

Whereas I expect smth like this:

enter image description here

Here's the info about the system i have: Windows 10 (64-bit); Python - 3.8.0 (32-bit); matplotlib - 3.3.3. This question i've seen here: How to make a polygon radar (spider) chart in python but it doesn't resolve my question. But it doesn't work for me and i can't understand why. I can simply copy the code, but the result is - outer border changes to pentagon-form, but the inner gridlines remain circular. But it works for other people!

The code of the program is below:

import numpy as np

import matplotlib.pyplot as plt
from matplotlib.patches import Circle, RegularPolygon
from matplotlib.path import Path
from matplotlib.projections.polar import PolarAxes
from matplotlib.projections import register_projection
from matplotlib.spines import Spine
from matplotlib.transforms import Affine2D


def radar_factory(num_vars, frame='circle'):
    """Create a radar chart with `num_vars` axes.

    This function creates a RadarAxes projection and registers it.

    Parameters
    ----------
    num_vars : int
        Number of variables for radar chart.
    frame : {'circle' | 'polygon'}
        Shape of frame surrounding axes.

    """
    # calculate evenly-spaced axis angles
    theta = np.linspace(0, 2*np.pi, num_vars, endpoint=False)

    class RadarAxes(PolarAxes):

        name = 'radar'

        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            # rotate plot such that the first axis is at the top
            self.set_theta_zero_location('N')

        def fill(self, *args, closed=True, **kwargs):
            """Override fill so that line is closed by default"""
            return super().fill(closed=closed, *args, **kwargs)

        def plot(self, *args, **kwargs):
            """Override plot so that line is closed by default"""
            lines = super().plot(*args, **kwargs)
            for line in lines:
                self._close_line(line)

        def _close_line(self, line):
            x, y = line.get_data()
            # FIXME: markers at x[0], y[0] get doubled-up
            if x[0] != x[-1]:
                x = np.concatenate((x, [x[0]]))
                y = np.concatenate((y, [y[0]]))
                line.set_data(x, y)

        def set_varlabels(self, labels):
            self.set_thetagrids(np.degrees(theta), labels)

        def _gen_axes_patch(self):
            # The Axes patch must be centered at (0.5, 0.5) and of radius 0.5
            # in axes coordinates.
            if frame == 'circle':
                return Circle((0.5, 0.5), 0.5)
            elif frame == 'polygon':
                return RegularPolygon((0.5, 0.5), num_vars, radius=0.5, edgecolor="k")
            else:
                raise ValueError("unknown value for 'frame': %s" % frame)

        def draw(self, renderer):
            """ Draw. If frame is polygon, make gridlines polygon-shaped """
            if frame == 'polygon':
                gridlines = self.yaxis.get_gridlines()
                for gl in gridlines:
                    gl.get_path()._interpolation_steps = num_vars
            super().draw(renderer)

        def _gen_axes_spines(self):
            if frame == 'circle':
                return super()._gen_axes_spines()
            elif frame == 'polygon':
                # spine_type must be 'left'/'right'/'top'/'bottom'/'circle'.
                spine = Spine(axes=self,
                              spine_type='circle',
                              path=Path.unit_regular_polygon(num_vars))
                # unit_regular_polygon gives a polygon of radius 1 centered at
                # (0, 0) but we want a polygon of radius 0.5 centered at (0.5,
                # 0.5) in axes coordinates.
                spine.set_transform(Affine2D().scale(.5).translate(.5, .5)
                                    + self.transAxes)
                return {'polar': spine}
            else:
                raise ValueError("unknown value for 'frame': %s" % frame)

    register_projection(RadarAxes)
    return theta


data = [['O1', 'O2', 'O3', 'O4', 'O5'],
        ('Title', [
                    [4, 3.5, 4, 2, 3,], 
                    [1.07, 5.95, 2.04, 1.05, 0.00,], 
                  ]
        )]

N = len(data[0])
theta = radar_factory(N, frame='polygon')                                     # polygon  !!!

spoke_labels = data.pop(0)
title, case_data = data[0]
fig, ax = plt.subplots(figsize=(5, 5), subplot_kw=dict(projection='radar'))
fig.subplots_adjust(top=0.85, bottom=0.05)
ax.set_rgrids([0, 1, 2.0, 3.0, 4.0, 5.0, 6])
ax.set_title(title,  position=(0.5, 1.1), ha='center')

for d in case_data:
    line = ax.plot(theta, d)
    ax.fill(theta, d,  alpha=0.25)
ax.set_varlabels(spoke_labels)

plt.show()


Mr. T
  • 11,960
  • 10
  • 32
  • 54
KarapetEV
  • 51
  • 3
  • 2
    I voted to reopen this question. While the linked script by ImportanceOfBeingEarnest works as expected with Python3.6/matplotlib3.2.2, I also get a different output with Python3.8/matplolib3.3.3 for the inner grid lines. – Mr. T Dec 31 '20 at 12:00
  • So, the problem is in python version? Seems like it works only on python version 3.7 and earlier. – KarapetEV Dec 31 '20 at 12:52
  • No, I just have matplotlib 3.2.2 stored in Python3.6. Most likely some matplotlib version changes, maybe how the script hooks `gl.get_path()._interpolation_steps = num_vars` into the renderer. There were some change related to the renderer but I am not familiar with the details. – Mr. T Dec 31 '20 at 12:56
  • @JohanC sorry, I also think that the question should be reopened. – S. Nick Dec 31 '20 at 13:14
  • 1
    Yup, downgrading to matplotlib 3.2.2 restores the previous behavior. Something in the upgrade to 3.3.3 has changed how the polar plot gridlines are generated, and now `_interpolation_steps ` is overridden by a standard value. Tbf, the matplotlib documentation says [`This attribute is primarily an implementation detail and is not intended for public use.`](https://matplotlib.org/3.3.3/api/path_api.html?highlight=matplotlib%20path#module-matplotlib.path) – Mr. T Dec 31 '20 at 14:25
  • Thanks everyone, downgrading matplotlib helped me. How can i put the axes labels by the vertical axis (O1 in my example)? – KarapetEV Jan 01 '21 at 20:54
  • `ax.set_rlabel_position(0)` but then it will overlap with the gridline. – Mr. T Jan 02 '21 at 17:02
  • Thanks a lot. My questions are resolved. – KarapetEV Jan 02 '21 at 20:46
  • 1
    Is there any way to fix this without downgrading matplotlib? – Daniel Saad Feb 07 '21 at 21:54
  • Only downgrading helped me. – KarapetEV Feb 12 '21 at 07:26
  • 1
    Did anyone figure out how to do this without downgrading? – Superbman Jun 17 '21 at 13:59

2 Answers2

1

I don't have enough reputation to add a comment so I'll put this down as an answer. An update to the code that can serve as a workaround and works on Matplotlib > 3.5 has been recently added to issue 19981 by prohde. You can check it here: https://github.com/matplotlib/matplotlib/issues/19981

Joao Neves
  • 61
  • 2
0

As mentioned by @joao-neves the docs will be updated in this PR. The working code for your example would be

import numpy as np

import matplotlib.pyplot as plt
from matplotlib.patches import Circle, RegularPolygon
from matplotlib.path import Path
from matplotlib.projections.polar import PolarAxes
from matplotlib.projections import register_projection
from matplotlib.spines import Spine
from matplotlib.transforms import Affine2D


def radar_factory(num_vars, frame='circle'):
    """Create a radar chart with `num_vars` axes.

    This function creates a RadarAxes projection and registers it.

    Parameters
    ----------
    num_vars : int
        Number of variables for radar chart.
    frame : {'circle' | 'polygon'}
        Shape of frame surrounding axes.

    """
    # calculate evenly-spaced axis angles
    theta = np.linspace(0, 2*np.pi, num_vars, endpoint=False)
    
    class RadarTransform(PolarAxes.PolarTransform):
        def transform_path_non_affine(self, path):
            # Paths with non-unit interpolation steps correspond to gridlines,
            # in which case we force interpolation (to defeat PolarTransform's
            # autoconversion to circular arcs).
            if path._interpolation_steps > 1:
                path = path.interpolated(num_vars)
            return Path(self.transform(path.vertices), path.codes)

    class RadarAxes(PolarAxes):

        name = 'radar'
        PolarTransform = RadarTransform

        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            # rotate plot such that the first axis is at the top
            self.set_theta_zero_location('N')

        def fill(self, *args, closed=True, **kwargs):
            """Override fill so that line is closed by default"""
            return super().fill(closed=closed, *args, **kwargs)

        def plot(self, *args, **kwargs):
            """Override plot so that line is closed by default"""
            lines = super().plot(*args, **kwargs)
            for line in lines:
                self._close_line(line)

        def _close_line(self, line):
            x, y = line.get_data()
            # FIXME: markers at x[0], y[0] get doubled-up
            if x[0] != x[-1]:
                x = np.concatenate((x, [x[0]]))
                y = np.concatenate((y, [y[0]]))
                line.set_data(x, y)

        def set_varlabels(self, labels):
            self.set_thetagrids(np.degrees(theta), labels)

        def _gen_axes_patch(self):
            # The Axes patch must be centered at (0.5, 0.5) and of radius 0.5
            # in axes coordinates.
            if frame == 'circle':
                return Circle((0.5, 0.5), 0.5)
            elif frame == 'polygon':
                return RegularPolygon((0.5, 0.5), num_vars, radius=0.5, edgecolor="k")
            else:
                raise ValueError("unknown value for 'frame': %s" % frame)

        def draw(self, renderer):
            """ Draw. If frame is polygon, make gridlines polygon-shaped """
            if frame == 'polygon':
                gridlines = self.yaxis.get_gridlines()
                for gl in gridlines:
                    gl.get_path()._interpolation_steps = num_vars
            super().draw(renderer)

        def _gen_axes_spines(self):
            if frame == 'circle':
                return super()._gen_axes_spines()
            elif frame == 'polygon':
                # spine_type must be 'left'/'right'/'top'/'bottom'/'circle'.
                spine = Spine(axes=self,
                              spine_type='circle',
                              path=Path.unit_regular_polygon(num_vars))
                # unit_regular_polygon gives a polygon of radius 1 centered at
                # (0, 0) but we want a polygon of radius 0.5 centered at (0.5,
                # 0.5) in axes coordinates.
                spine.set_transform(Affine2D().scale(.5).translate(.5, .5)
                                    + self.transAxes)
                return {'polar': spine}
            else:
                raise ValueError("unknown value for 'frame': %s" % frame)

    register_projection(RadarAxes)
    return theta


data = [['O1', 'O2', 'O3', 'O4', 'O5'],
        ('Title', [
                    [4, 3.5, 4, 2, 3,], 
                    [1.07, 5.95, 2.04, 1.05, 0.00,], 
                  ]
        )]

N = len(data[0])
theta = radar_factory(N, frame='polygon')                                     

spoke_labels = data.pop(0)
title, case_data = data[0]
fig, ax = plt.subplots(figsize=(5, 5), subplot_kw=dict(projection='radar'))
fig.subplots_adjust(top=0.85, bottom=0.05)
ax.set_rgrids([0, 1, 2.0, 3.0, 4.0, 5.0, 6])
ax.set_title(title,  position=(0.5, 1.1), ha='center')

for d in case_data:
    line = ax.plot(theta, d)
    ax.fill(theta, d, alpha=0.25, label='_nolegend_')
ax.set_varlabels(spoke_labels)

plt.show()

enter image description here

Yann Dubois
  • 1,195
  • 15
  • 16