15
15
just make up some data for little Johnny Doe.
16
16
"""
17
17
18
+ from collections import namedtuple
18
19
import numpy as np
19
20
import matplotlib .pyplot as plt
20
- from matplotlib .ticker import MaxNLocator
21
- from collections import namedtuple
22
21
23
- np .random .seed (42 )
24
22
25
23
Student = namedtuple ('Student' , ['name' , 'grade' , 'gender' ])
26
- Score = namedtuple ('Score' , ['score ' , 'percentile' ])
24
+ Score = namedtuple ('Score' , ['value' , 'unit ' , 'percentile' ])
27
25
28
- # GLOBAL CONSTANTS
29
- test_names = ['Pacer Test' , 'Flexed Arm\n Hang' , 'Mile Run' , 'Agility' ,
30
- 'Push Ups' ]
31
- test_units = dict (zip (test_names , ['laps' , 'sec' , 'min:sec' , 'sec' , '' ]))
32
26
33
-
34
- def attach_ordinal (num ):
27
+ def to_ordinal (num ):
35
28
"""Convert an integer to an ordinal string, e.g. 2 -> '2nd'."""
36
29
suffixes = {str (i ): v
37
30
for i , v in enumerate (['th' , 'st' , 'nd' , 'rd' , 'th' ,
@@ -43,118 +36,68 @@ def attach_ordinal(num):
43
36
return v + suffixes [v [- 1 ]]
44
37
45
38
46
- def format_score (score , test ):
39
+ def format_score (score ):
47
40
"""
48
41
Create score labels for the right y-axis as the test name followed by the
49
42
measurement unit (if any), split over two lines.
50
43
"""
51
- unit = test_units [test ]
52
- if unit :
53
- return f'{ score } \n { unit } '
54
- else : # If no unit, don't include a newline, so that label stays centered.
55
- return score
56
-
44
+ return f'{ score .value } \n { score .unit } ' if score .unit else str (score .value )
57
45
58
- def format_ycursor (y ):
59
- y = int (y )
60
- if y < 0 or y >= len (test_names ):
61
- return ''
62
- else :
63
- return test_names [y ]
64
46
65
-
66
- def plot_student_results (student , scores , cohort_size ):
67
- fig , ax1 = plt .subplots (figsize = (9 , 7 )) # Create the figure
68
- fig .subplots_adjust (left = 0.115 , right = 0.88 )
47
+ def plot_student_results (student , scores_by_test , cohort_size ):
48
+ fig , ax1 = plt .subplots (figsize = (9 , 7 ), constrained_layout = True )
69
49
fig .canvas .manager .set_window_title ('Eldorado K-8 Fitness Chart' )
70
50
71
- pos = np .arange (len (test_names ))
72
-
73
- rects = ax1 .barh (pos , [scores [k ].percentile for k in test_names ],
74
- align = 'center' ,
75
- height = 0.5 ,
76
- tick_label = test_names )
77
-
78
51
ax1 .set_title (student .name )
52
+ ax1 .set_xlabel (
53
+ 'Percentile Ranking Across {grade} Grade {gender}s\n '
54
+ 'Cohort Size: {cohort_size}' .format (
55
+ grade = to_ordinal (student .grade ),
56
+ gender = student .gender .title (),
57
+ cohort_size = cohort_size ))
58
+
59
+ test_names = list (scores_by_test .keys ())
60
+ percentiles = [score .percentile for score in scores_by_test .values ()]
61
+
62
+ rects = ax1 .barh (test_names , percentiles , align = 'center' , height = 0.5 )
63
+ # Partition the percentile values to be able to draw large numbers in
64
+ # white within the bar, and small numbers in black outside the bar.
65
+ large_percentiles = [to_ordinal (p ) if p > 40 else '' for p in percentiles ]
66
+ small_percentiles = [to_ordinal (p ) if p <= 40 else '' for p in percentiles ]
67
+ ax1 .bar_label (rects , small_percentiles ,
68
+ padding = 5 , color = 'black' , fontweight = 'bold' )
69
+ ax1 .bar_label (rects , large_percentiles ,
70
+ padding = - 32 , color = 'white' , fontweight = 'bold' )
79
71
80
72
ax1 .set_xlim ([0 , 100 ])
81
- ax1 .xaxis . set_major_locator ( MaxNLocator ( 11 ) )
73
+ ax1 .set_xticks ([ 0 , 10 , 20 , 30 , 40 , 50 , 60 , 70 , 80 , 90 , 100 ] )
82
74
ax1 .xaxis .grid (True , linestyle = '--' , which = 'major' ,
83
75
color = 'grey' , alpha = .25 )
84
-
85
- # Plot a solid vertical gridline to highlight the median position
86
- ax1 .axvline (50 , color = 'grey' , alpha = 0.25 )
76
+ ax1 .axvline (50 , color = 'grey' , alpha = 0.25 ) # median position
87
77
88
78
# Set the right-hand Y-axis ticks and labels
89
79
ax2 = ax1 .twinx ()
90
-
91
- # Set the tick locations and labels
92
- ax2 .set_yticks (
93
- pos , labels = [format_score (scores [k ].score , k ) for k in test_names ])
94
80
# Set equal limits on both yaxis so that the ticks line up
95
81
ax2 .set_ylim (ax1 .get_ylim ())
82
+ # Set the tick locations and labels
83
+ ax2 .set_yticks (
84
+ np .arange (len (scores_by_test )),
85
+ labels = [format_score (score ) for score in scores_by_test .values ()])
96
86
97
87
ax2 .set_ylabel ('Test Scores' )
98
88
99
- xlabel = ('Percentile Ranking Across {grade} Grade {gender}s\n '
100
- 'Cohort Size: {cohort_size}' )
101
- ax1 .set_xlabel (xlabel .format (grade = attach_ordinal (student .grade ),
102
- gender = student .gender .title (),
103
- cohort_size = cohort_size ))
104
-
105
- rect_labels = []
106
- # Lastly, write in the ranking inside each bar to aid in interpretation
107
- for rect in rects :
108
- # Rectangle widths are already integer-valued but are floating
109
- # type, so it helps to remove the trailing decimal point and 0 by
110
- # converting width to int type
111
- width = int (rect .get_width ())
112
-
113
- rank_str = attach_ordinal (width )
114
- # The bars aren't wide enough to print the ranking inside
115
- if width < 40 :
116
- # Shift the text to the right side of the right edge
117
- xloc = 5
118
- # Black against white background
119
- clr = 'black'
120
- align = 'left'
121
- else :
122
- # Shift the text to the left side of the right edge
123
- xloc = - 5
124
- # White on magenta
125
- clr = 'white'
126
- align = 'right'
127
-
128
- # Center the text vertically in the bar
129
- yloc = rect .get_y () + rect .get_height () / 2
130
- label = ax1 .annotate (
131
- rank_str , xy = (width , yloc ), xytext = (xloc , 0 ),
132
- textcoords = "offset points" ,
133
- horizontalalignment = align , verticalalignment = 'center' ,
134
- color = clr , weight = 'bold' , clip_on = True )
135
- rect_labels .append (label )
136
-
137
- # Make the interactive mouse over give the bar title
138
- ax2 .fmt_ydata = format_ycursor
139
- # Return all of the artists created
140
- return {'fig' : fig ,
141
- 'ax' : ax1 ,
142
- 'ax_right' : ax2 ,
143
- 'bars' : rects ,
144
- 'perc_labels' : rect_labels }
145
-
146
-
147
- student = Student ('Johnny Doe' , 2 , 'boy' )
148
- scores = dict (zip (
149
- test_names ,
150
- (Score (v , p ) for v , p in
151
- zip (['7' , '48' , '12:52' , '17' , '14' ],
152
- np .round (np .random .uniform (0 , 100 , len (test_names )), 0 )))))
153
- cohort_size = 62 # The number of other 2nd grade boys
154
-
155
- arts = plot_student_results (student , scores , cohort_size )
156
- plt .show ()
157
89
90
+ student = Student (name = 'Johnny Doe' , grade = 2 , gender = 'Boy' )
91
+ scores_by_test = {
92
+ 'Pacer Test' : Score (7 , 'laps' , percentile = 37 ),
93
+ 'Flexed Arm\n Hang' : Score (48 , 'sec' , percentile = 95 ),
94
+ 'Mile Run' : Score ('12:52' , 'min:sec' , percentile = 73 ),
95
+ 'Agility' : Score (17 , 'sec' , percentile = 60 ),
96
+ 'Push Ups' : Score (14 , '' , percentile = 16 ),
97
+ }
98
+
99
+ plot_student_results (student , scores_by_test , cohort_size = 62 )
100
+ plt .show ()
158
101
159
102
#############################################################################
160
103
#
@@ -164,5 +107,5 @@ def plot_student_results(student, scores, cohort_size):
164
107
# in this example:
165
108
#
166
109
# - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar`
167
- # - `matplotlib.axes.Axes.annotate ` / `matplotlib.pyplot.annotate `
110
+ # - `matplotlib.axes.Axes.bar_label ` / `matplotlib.pyplot.bar_label `
168
111
# - `matplotlib.axes.Axes.twinx` / `matplotlib.pyplot.twinx`
0 commit comments