|
import numpy as np |
|
import textwrap |
|
|
|
class ComplexRadar(): |
|
""" |
|
Create a complex radar chart with different scales for each variable |
|
Parameters |
|
---------- |
|
fig : figure object |
|
A matplotlib figure object to add the axes on |
|
variables : list |
|
A list of variables |
|
ranges : list |
|
A list of tuples (min, max) for each variable |
|
n_ring_levels: int, defaults to 5 |
|
Number of ordinate or ring levels to draw |
|
show_scales: bool, defaults to True |
|
Indicates if we the ranges for each variable are plotted |
|
format_cfg: dict, defaults to None |
|
A dictionary with formatting configurations |
|
""" |
|
def __init__(self, fig, variables, ranges, n_ring_levels=5, show_scales=True, format_cfg=None): |
|
|
|
|
|
self.format_cfg = { |
|
|
|
|
|
'axes_args': {}, |
|
|
|
|
|
'rgrid_tick_lbls_args': {'fontsize':8}, |
|
|
|
|
|
'rad_ln_args': {}, |
|
|
|
|
|
'angle_ln_args': {}, |
|
|
|
'incl_endpoint':False, |
|
|
|
'theta_tick_lbls':{'va':'top', 'ha':'center'}, |
|
'theta_tick_lbls_txt_wrap':15, |
|
'theta_tick_lbls_brk_lng_wrds':False, |
|
'theta_tick_lbls_pad':25, |
|
|
|
|
|
'outer_ring':{'visible':True, 'color':'#d6d6d6'} |
|
} |
|
|
|
if format_cfg is not None: |
|
self.format_cfg = { k:(format_cfg[k]) if k in format_cfg.keys() else (self.format_cfg[k]) |
|
for k in self.format_cfg.keys()} |
|
|
|
|
|
|
|
|
|
angles = np.arange(0, 360, 360./len(variables)) |
|
axes = [fig.add_axes([0.1,0.1,0.9,0.9], |
|
polar=True, |
|
label = "axes{}".format(i), |
|
**self.format_cfg['axes_args']) for i in range(len(variables)+1)] |
|
|
|
|
|
for ax in axes: |
|
ax.set_theta_zero_location('N') |
|
ax.set_theta_direction(-1) |
|
ax.set_axisbelow(True) |
|
|
|
|
|
for i, ax in enumerate(axes): |
|
|
|
|
|
j = 0 if (i==0 or i==1) else i-1 |
|
ax.set_ylim(*ranges[j]) |
|
|
|
grid = np.linspace(*ranges[j], num=n_ring_levels, |
|
endpoint=self.format_cfg['incl_endpoint']) |
|
gridlabel = ["{}".format(round(x,2)) for x in grid] |
|
gridlabel[0] = "" |
|
lines, labels = ax.set_rgrids(grid, |
|
labels=gridlabel, |
|
angle=angles[j], |
|
**self.format_cfg['rgrid_tick_lbls_args'] |
|
) |
|
|
|
ax.set_ylim(*ranges[j]) |
|
ax.spines["polar"].set_visible(False) |
|
ax.grid(visible=False) |
|
|
|
if show_scales == False: |
|
ax.set_yticklabels([]) |
|
|
|
|
|
for ax in axes[1:]: |
|
ax.patch.set_visible(False) |
|
ax.xaxis.set_visible(False) |
|
|
|
|
|
self.angle = np.deg2rad(np.r_[angles, angles[0]]) |
|
self.ranges = ranges |
|
self.ax = axes[0] |
|
self.ax1 = axes[1] |
|
self.plot_counter = 0 |
|
|
|
|
|
|
|
self.ax.yaxis.grid(**self.format_cfg['rad_ln_args']) |
|
|
|
self.ax.spines['polar'].set(**self.format_cfg['outer_ring']) |
|
|
|
self.ax.xaxis.grid(**self.format_cfg['angle_ln_args']) |
|
|
|
|
|
|
|
self.ax1.axis('off') |
|
self.ax1.set_zorder(9) |
|
|
|
|
|
l, text = self.ax.set_thetagrids(angles, labels=variables) |
|
|
|
|
|
labels = [t.get_text() for t in self.ax.get_xticklabels()] |
|
labels = ['\n'.join(textwrap.wrap(l, self.format_cfg['theta_tick_lbls_txt_wrap'], |
|
break_long_words=self.format_cfg['theta_tick_lbls_brk_lng_wrds'])) for l in labels] |
|
self.ax.set_xticklabels(labels, **self.format_cfg['theta_tick_lbls']) |
|
|
|
for t,a in zip(self.ax.get_xticklabels(),angles): |
|
if a == 0: |
|
t.set_ha('center') |
|
elif a > 0 and a < 180: |
|
t.set_ha('left') |
|
elif a == 180: |
|
t.set_ha('center') |
|
else: |
|
t.set_ha('right') |
|
|
|
self.ax.tick_params(axis='both', pad=self.format_cfg['theta_tick_lbls_pad']) |
|
|
|
|
|
def _scale_data(self, data, ranges): |
|
"""Scales data[1:] to ranges[0]""" |
|
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] |
|
sdata = [d] |
|
for d, (y1, y2) in zip(data[1:], ranges[1:]): |
|
sdata.append((d-y1) / (y2-y1) * (x2 - x1) + x1) |
|
return sdata |
|
|
|
def plot(self, data, *args, **kwargs): |
|
"""Plots a line""" |
|
sdata = self._scale_data(data, self.ranges) |
|
self.ax1.plot(self.angle, np.r_[sdata, sdata[0]], *args, **kwargs) |
|
self.plot_counter = self.plot_counter+1 |
|
|
|
def fill(self, data, *args, **kwargs): |
|
"""Plots an area""" |
|
sdata = self._scale_data(data, self.ranges) |
|
self.ax1.fill(self.angle, np.r_[sdata, sdata[0]], *args, **kwargs) |
|
|
|
def use_legend(self, *args, **kwargs): |
|
"""Shows a legend""" |
|
self.ax1.legend(*args, **kwargs) |
|
|
|
def set_title(self, title, pad=25, **kwargs): |
|
"""Set a title""" |
|
self.ax.set_title(title,pad=pad, **kwargs) |