1""" 2====================================== 3Radar chart (aka spider or star chart) 4====================================== 5 6This example creates a radar chart, also known as a spider or star chart [1]_. 7 8Although this example allows a frame of either 'circle' or 'polygon', polygon 9frames don't have proper gridlines (the lines are circles instead of polygons). 10It's possible to get a polygon grid by setting GRIDLINE_INTERPOLATION_STEPS in 11matplotlib.axis to the desired number of vertices, but the orientation of the 12polygon is not aligned with the radial axes. 13 14.. [1] http://en.wikipedia.org/wiki/Radar_chart 15""" 16import numpy as np 17 18import matplotlib.pyplot as plt 19from matplotlib.path import Path 20from matplotlib.spines import Spine 21from matplotlib.projections.polar import PolarAxes 22from matplotlib.projections import register_projection 23 24 25def radar_factory(num_vars, frame='circle'): 26 """Create a radar chart with `num_vars` axes. 27 28 This function creates a RadarAxes projection and registers it. 29 30 Parameters 31 ---------- 32 num_vars : int 33 Number of variables for radar chart. 34 frame : {'circle' | 'polygon'} 35 Shape of frame surrounding axes. 36 37 """ 38 # calculate evenly-spaced axis angles 39 theta = np.linspace(0, 2*np.pi, num_vars, endpoint=False) 40 41 def draw_poly_patch(self): 42 # rotate theta such that the first axis is at the top 43 verts = unit_poly_verts(theta + np.pi / 2) 44 return plt.Polygon(verts, closed=True, edgecolor='k') 45 46 def draw_circle_patch(self): 47 # unit circle centered on (0.5, 0.5) 48 return plt.Circle((0.5, 0.5), 0.5) 49 50 patch_dict = {'polygon': draw_poly_patch, 'circle': draw_circle_patch} 51 if frame not in patch_dict: 52 raise ValueError('unknown value for `frame`: %s' % frame) 53 54 class RadarAxes(PolarAxes): 55 56 name = 'radar' 57 # use 1 line segment to connect specified points 58 RESOLUTION = 1 59 # define draw_frame method 60 draw_patch = patch_dict[frame] 61 62 def __init__(self, *args, **kwargs): 63 super(RadarAxes, self).__init__(*args, **kwargs) 64 # rotate plot such that the first axis is at the top 65 self.set_theta_zero_location('N') 66 67 def fill(self, *args, **kwargs): 68 """Override fill so that line is closed by default""" 69 closed = kwargs.pop('closed', True) 70 return super(RadarAxes, self).fill(closed=closed, *args, **kwargs) 71 72 def plot(self, *args, **kwargs): 73 """Override plot so that line is closed by default""" 74 lines = super(RadarAxes, self).plot(*args, **kwargs) 75 for line in lines: 76 self._close_line(line) 77 78 def _close_line(self, line): 79 x, y = line.get_data() 80 # FIXME: markers at x[0], y[0] get doubled-up 81 if x[0] != x[-1]: 82 x = np.concatenate((x, [x[0]])) 83 y = np.concatenate((y, [y[0]])) 84 line.set_data(x, y) 85 86 def set_varlabels(self, labels): 87 self.set_thetagrids(np.degrees(theta), labels) 88 89 def _gen_axes_patch(self): 90 return self.draw_patch() 91 92 def _gen_axes_spines(self): 93 if frame == 'circle': 94 return PolarAxes._gen_axes_spines(self) 95 # The following is a hack to get the spines (i.e. the axes frame) 96 # to draw correctly for a polygon frame. 97 98 # spine_type must be 'left', 'right', 'top', 'bottom', or `circle`. 99 spine_type = 'circle' 100 verts = unit_poly_verts(theta + np.pi / 2) 101 # close off polygon by repeating first vertex 102 verts.append(verts[0]) 103 path = Path(verts) 104 105 spine = Spine(self, spine_type, path) 106 spine.set_transform(self.transAxes) 107 return {'polar': spine} 108 109 register_projection(RadarAxes) 110 return theta 111 112 113def unit_poly_verts(theta): 114 """Return vertices of polygon for subplot axes. 115 116 This polygon is circumscribed by a unit circle centered at (0.5, 0.5) 117 """ 118 x0, y0, r = [0.5] * 3 119 verts = [(r*np.cos(t) + x0, r*np.sin(t) + y0) for t in theta] 120 return verts 121 122 123def example_data(): 124 # The following data is from the Denver Aerosol Sources and Health study. 125 # See doi:10.1016/j.atmosenv.2008.12.017 126 # 127 # The data are pollution source profile estimates for five modeled 128 # pollution sources (e.g., cars, wood-burning, etc) that emit 7-9 chemical 129 # species. The radar charts are experimented with here to see if we can 130 # nicely visualize how the modeled source profiles change across four 131 # scenarios: 132 # 1) No gas-phase species present, just seven particulate counts on 133 # Sulfate 134 # Nitrate 135 # Elemental Carbon (EC) 136 # Organic Carbon fraction 1 (OC) 137 # Organic Carbon fraction 2 (OC2) 138 # Organic Carbon fraction 3 (OC3) 139 # Pyrolized Organic Carbon (OP) 140 # 2)Inclusion of gas-phase specie carbon monoxide (CO) 141 # 3)Inclusion of gas-phase specie ozone (O3). 142 # 4)Inclusion of both gas-phase species is present... 143 data = [ 144 ['Sulfate', 'Nitrate', 'EC', 'OC1', 'OC2', 'OC3', 'OP', 'CO', 'O3'], 145 ('Basecase', [ 146 [0.88, 0.01, 0.03, 0.03, 0.00, 0.06, 0.01, 0.00, 0.00], 147 [0.07, 0.95, 0.04, 0.05, 0.00, 0.02, 0.01, 0.00, 0.00], 148 [0.01, 0.02, 0.85, 0.19, 0.05, 0.10, 0.00, 0.00, 0.00], 149 [0.02, 0.01, 0.07, 0.01, 0.21, 0.12, 0.98, 0.00, 0.00], 150 [0.01, 0.01, 0.02, 0.71, 0.74, 0.70, 0.00, 0.00, 0.00]]), 151 ('With CO', [ 152 [0.88, 0.02, 0.02, 0.02, 0.00, 0.05, 0.00, 0.05, 0.00], 153 [0.08, 0.94, 0.04, 0.02, 0.00, 0.01, 0.12, 0.04, 0.00], 154 [0.01, 0.01, 0.79, 0.10, 0.00, 0.05, 0.00, 0.31, 0.00], 155 [0.00, 0.02, 0.03, 0.38, 0.31, 0.31, 0.00, 0.59, 0.00], 156 [0.02, 0.02, 0.11, 0.47, 0.69, 0.58, 0.88, 0.00, 0.00]]), 157 ('With O3', [ 158 [0.89, 0.01, 0.07, 0.00, 0.00, 0.05, 0.00, 0.00, 0.03], 159 [0.07, 0.95, 0.05, 0.04, 0.00, 0.02, 0.12, 0.00, 0.00], 160 [0.01, 0.02, 0.86, 0.27, 0.16, 0.19, 0.00, 0.00, 0.00], 161 [0.01, 0.03, 0.00, 0.32, 0.29, 0.27, 0.00, 0.00, 0.95], 162 [0.02, 0.00, 0.03, 0.37, 0.56, 0.47, 0.87, 0.00, 0.00]]), 163 ('CO & O3', [ 164 [0.87, 0.01, 0.08, 0.00, 0.00, 0.04, 0.00, 0.00, 0.01], 165 [0.09, 0.95, 0.02, 0.03, 0.00, 0.01, 0.13, 0.06, 0.00], 166 [0.01, 0.02, 0.71, 0.24, 0.13, 0.16, 0.00, 0.50, 0.00], 167 [0.01, 0.03, 0.00, 0.28, 0.24, 0.23, 0.00, 0.44, 0.88], 168 [0.02, 0.00, 0.18, 0.45, 0.64, 0.55, 0.86, 0.00, 0.16]]) 169 ] 170 return data 171 172 173if __name__ == '__main__': 174 N = 9 175 theta = radar_factory(N, frame='polygon') 176 177 data = example_data() 178 spoke_labels = data.pop(0) 179 180 fig, axes = plt.subplots(figsize=(9, 9), nrows=2, ncols=2, 181 subplot_kw=dict(projection='radar')) 182 fig.subplots_adjust(wspace=0.25, hspace=0.20, top=0.85, bottom=0.05) 183 184 colors = ['b', 'r', 'g', 'm', 'y'] 185 # Plot the four cases from the example data on separate axes 186 for ax, (title, case_data) in zip(axes.flatten(), data): 187 ax.set_rgrids([0.2, 0.4, 0.6, 0.8]) 188 ax.set_title(title, weight='bold', size='medium', position=(0.5, 1.1), 189 horizontalalignment='center', verticalalignment='center') 190 for d, color in zip(case_data, colors): 191 ax.plot(theta, d, color=color) 192 ax.fill(theta, d, facecolor=color, alpha=0.25) 193 ax.set_varlabels(spoke_labels) 194 195 # add legend relative to top-left plot 196 ax = axes[0, 0] 197 labels = ('Factor 1', 'Factor 2', 'Factor 3', 'Factor 4', 'Factor 5') 198 legend = ax.legend(labels, loc=(0.9, .95), 199 labelspacing=0.1, fontsize='small') 200 201 fig.text(0.5, 0.965, '5-Factor Solution Profiles Across Four Scenarios', 202 horizontalalignment='center', color='black', weight='bold', 203 size='large') 204 205 plt.show() 206