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