| 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) |