# How do I create a complex Radar Chart?

So, I want to create a Player Profile Radar Chart something like this:

Not only the scale of each variable different, but also I want a reversed scale for some statistics like the 'dispossessed' stat, where less actually means good.

One solution for the variable scale for each statistic maybe is setting a benchmark and then calculating a score on a scale of 100?

But, How do I display the actual numbers on the chart then? Also, how do I get the reversed scale for some of the statistics.

Currently working in Excel. What is the most powerful tool to create a complex chart like this?

• Can you give an example of a dataset that you are trying to visualize? Currently, your question is vague. Providing an example dataset and a corresponding plot you would like to see would help. Also, providing external links (specifically from transient websites like twitter) is discouraged, so try describing it as best as you can in the question itself. – Nitesh Jun 11 '15 at 21:05
• Excel is the best (visually most beautiful one)! you can find implementations in python or other languages but they are not as great as excel. I tried a month ago! – Kasra Manshaei Jun 15 '15 at 1:43
• Kyler's solution is awesome, but incomplete. The code above only plots points on 6 axes... The value of 20 for the "Inverted 3%" axis does not plot when I run this. – user16242 Feb 13 '16 at 0:05

Wow, this was bit challenging but I was able to make one of these plots in python. The two main components are:

code:

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns # improves plot aesthetics

def _invert(x, limits):
"""inverts a value x on a scale from
limits[0] to limits[1]"""
return limits[1] - (x - limits[0])

def _scale_data(data, ranges):
"""scales data[1:] to ranges[0],
inverts if the scale is reversed"""
for d, (y1, y2) in zip(data[1:], ranges[1:]):
assert (y1 <= d <= y2) or (y2 <= d <= y1)
x1, x2 = ranges[0]
d = data[0]
if x1 > x2:
d = _invert(d, (x1, x2))
x1, x2 = x2, x1
sdata = [d]
for d, (y1, y2) in zip(data[1:], ranges[1:]):
if y1 > y2:
d = _invert(d, (y1, y2))
y1, y2 = y2, y1
sdata.append((d-y1) / (y2-y1)
* (x2 - x1) + x1)
return sdata

def __init__(self, fig, variables, ranges,
n_ordinate_levels=6):
angles = np.arange(0, 360, 360./len(variables))

label = "axes{}".format(i))
for i in range(len(variables))]
l, text = axes[0].set_thetagrids(angles,
labels=variables)
[txt.set_rotation(angle-90) for txt, angle
in zip(text, angles)]
for ax in axes[1:]:
ax.patch.set_visible(False)
ax.grid("off")
ax.xaxis.set_visible(False)
for i, ax in enumerate(axes):
grid = np.linspace(*ranges[i],
num=n_ordinate_levels)
gridlabel = ["{}".format(round(x,2))
for x in grid]
if ranges[i][0] > ranges[i][1]:
grid = grid[::-1] # hack to invert grid
# gridlabels aren't reversed
gridlabel[0] = "" # clean up origin
ax.set_rgrids(grid, labels=gridlabel,
angle=angles[i])
#ax.spines["polar"].set_visible(False)
ax.set_ylim(*ranges[i])
# variables for plotting
self.angle = np.deg2rad(np.r_[angles, angles[0]])
self.ranges = ranges
self.ax = axes[0]
def plot(self, data, *args, **kw):
sdata = _scale_data(data, self.ranges)
self.ax.plot(self.angle, np.r_[sdata, sdata[0]], *args, **kw)
def fill(self, data, *args, **kw):
sdata = _scale_data(data, self.ranges)
self.ax.fill(self.angle, np.r_[sdata, sdata[0]], *args, **kw)

# example data
variables = ("Normal Scale", "Inverted Scale", "Inverted 2",
"Normal Scale 2", "Normal 3", "Normal 4 %", "Inverted 3 %")
data = (1.76, 1.1, 1.2,
4.4, 3.4, 86.8, 20)
ranges = [(0.1, 2.3), (1.5, 0.3), (1.3, 0.5),
(1.7, 4.5), (1.5, 3.7), (70, 87), (100, 10)]
# plotting
fig1 = plt.figure(figsize=(6, 6))
plt.show()

• When i introduce a second data set i get an unexpected result in the entries that are having a reverse range when the range is greater than the values from the data set – Giorgos Synetos Jan 8 at 13:16

Here is an R version:

The codes here seem outdated for ggplot2: 2.0.0

Try my package zmisc: devtools:install_github("jerryzhujian9/ezmisc")

After you install it, you will be able to run:

df = mtcars
df\$model = rownames(mtcars)

ez.radarmap(df, "model", stats="mean", lwd=1, angle=0, fontsize=0.6, facet=T, facetfontsize=1, color=id, linetype=NULL)
ez.radarmap(df, "model", stats="none", lwd=1, angle=0, fontsize=1.5, facet=F, facetfontsize=1, color=id, linetype=NULL)


Here is a small modification of Kyler Brown's solution for Python that also allows negative values on the polar axes (which are currently not officially supported by matplotlib), basically by simply removing the check for negative values from set_rgrids:

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns # improves plot aesthetics

def _invert(x, limits):
"""inverts a value x on a scale from
limits[0] to limits[1]"""
return limits[1] - (x - limits[0])

def _scale_data(data, ranges):
"""scales data[1:] to ranges[0],
inverts if the scale is reversed"""
# for d, (y1, y2) in zip(data[1:], ranges[1:]):
for d, (y1, y2) in zip(data, ranges):
assert (y1 <= d <= y2) or (y2 <= d <= y1)

x1, x2 = ranges[0]
d = data[0]

if x1 > x2:
d = _invert(d, (x1, x2))
x1, x2 = x2, x1

sdata = [d]

for d, (y1, y2) in zip(data[1:], ranges[1:]):
if y1 > y2:
d = _invert(d, (y1, y2))
y1, y2 = y2, y1

sdata.append((d-y1) / (y2-y1) * (x2 - x1) + x1)

return sdata

def set_rgrids(self, radii, labels=None, angle=None, fmt=None,
**kwargs):
"""
Set the radial locations and labels of the *r* grids.
The labels will appear at radial distances *radii* at the
given *angle* in degrees.
*labels*, if not None, is a len(radii) list of strings of the
labels to use at each radius.
If *labels* is None, the built-in formatter will be used.
Return value is a list of tuples (*line*, *label*), where
*line* is :class:~matplotlib.lines.Line2D instances and the
*label* is :class:~matplotlib.text.Text instances.
kwargs are optional text properties for the labels:
%(Text)s
ACCEPTS: sequence of floats
"""
# Make sure we take into account unitized data
# if rmin <= 0:
#     raise ValueError('radial grids must be strictly positive')

if labels is not None:
self.set_yticklabels(labels)
elif fmt is not None:
self.yaxis.set_major_formatter(FormatStrFormatter(fmt))
if angle is None:
angle = self.get_rlabel_position()
self.set_rlabel_position(angle)
for t in self.yaxis.get_ticklabels():
t.update(kwargs)
return self.yaxis.get_gridlines(), self.yaxis.get_ticklabels()

def __init__(self, fig, variables, ranges,
n_ordinate_levels=6):
angles = np.arange(0, 360, 360./len(variables))

label = "axes{}".format(i))
for i in range(len(variables))]
l, text = axes[0].set_thetagrids(angles,
labels=variables)
[txt.set_rotation(angle-90) for txt, angle
in zip(text, angles)]
for ax in axes[1:]:
ax.patch.set_visible(False)
ax.grid("off")
ax.xaxis.set_visible(False)
for i, ax in enumerate(axes):
grid = np.linspace(*ranges[i],
num=n_ordinate_levels)
gridlabel = ["{}".format(round(x,2))
for x in grid]
if ranges[i][0] > ranges[i][1]:
grid = grid[::-1] # hack to invert grid
# gridlabels aren't reversed
gridlabel[0] = "" # clean up origin
# ax.set_rgrids(grid, labels=gridlabel, angle=angles[i])
set_rgrids(ax, grid, labels=gridlabel, angle=angles[i])
#ax.spines["polar"].set_visible(False)
ax.set_ylim(*ranges[i])
# variables for plotting
self.angle = np.deg2rad(np.r_[angles, angles[0]])
self.ranges = ranges
self.ax = axes[0]
def plot(self, data, *args, **kw):
sdata = _scale_data(data, self.ranges)
self.ax.plot(self.angle, np.r_[sdata, sdata[0]], *args, **kw)
def fill(self, data, *args, **kw):
sdata = _scale_data(data, self.ranges)
self.ax.fill(self.angle, np.r_[sdata, sdata[0]], *args, **kw)

# example data
variables = ("Normal Scale", "Inverted Scale", "Inverted 2",
"Normal Scale 2", "Normal 3", "Normal 4 %", "Inverted 3 %")
data = (-1.76, 1.1, 1.2,
4.4, 3.4, 86.8, 20)
ranges = [(-5, 3), (1.5, 0.3), (1.3, 0.5),
(1.7, 4.5), (1.5, 3.7), (70, 87), (100, -50)]
# plotting
fig1 = plt.figure(figsize=(6, 6))