1515just make up some data for little Johnny Doe.
1616"""
1717
18+ from collections import namedtuple
1819import numpy as np
1920import matplotlib .pyplot as plt
20- from matplotlib .ticker import MaxNLocator
21- from collections import namedtuple
2221
23- np .random .seed (42 )
2422
2523Student = namedtuple ('Student' , ['name' , 'grade' , 'gender' ])
26- Score = namedtuple ('Score' , ['score ' , 'percentile' ])
24+ Score = namedtuple ('Score' , ['value' , 'unit ' , 'percentile' ])
2725
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' , '' ]))
3226
33-
34- def attach_ordinal (num ):
27+ def to_ordinal (num ):
3528 """Convert an integer to an ordinal string, e.g. 2 -> '2nd'."""
3629 suffixes = {str (i ): v
3730 for i , v in enumerate (['th' , 'st' , 'nd' , 'rd' , 'th' ,
@@ -43,118 +36,68 @@ def attach_ordinal(num):
4336 return v + suffixes [v [- 1 ]]
4437
4538
46- def format_score (score , test ):
39+ def format_score (score ):
4740 """
4841 Create score labels for the right y-axis as the test name followed by the
4942 measurement unit (if any), split over two lines.
5043 """
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 )
5745
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 ]
6446
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 )
6949 fig .canvas .manager .set_window_title ('Eldorado K-8 Fitness Chart' )
7050
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-
7851 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' )
7971
8072 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 ] )
8274 ax1 .xaxis .grid (True , linestyle = '--' , which = 'major' ,
8375 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
8777
8878 # Set the right-hand Y-axis ticks and labels
8979 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 ])
9480 # Set equal limits on both yaxis so that the ticks line up
9581 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 ()])
9686
9787 ax2 .set_ylabel ('Test Scores' )
9888
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 ()
15789
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 ()
158101
159102#############################################################################
160103#
@@ -164,5 +107,5 @@ def plot_student_results(student, scores, cohort_size):
164107# in this example:
165108#
166109# - `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 `
168111# - `matplotlib.axes.Axes.twinx` / `matplotlib.pyplot.twinx`
0 commit comments