|
11 | 11 | import numpy as np
|
12 | 12 | import matplotlib.pyplot as plt
|
13 | 13 | from matplotlib.ticker import MaxNLocator
|
| 14 | +from collections import namedtuple |
14 | 15 |
|
15 |
| -student = 'Johnny Doe' |
16 |
| -grade = 2 |
17 |
| -gender = 'boy' |
18 |
| -cohortSize = 62 # The number of other 2nd grade boys |
| 16 | +Student = namedtuple('Student', ['name', 'grade', 'gender']) |
| 17 | +Score = namedtuple('Score', ['score', 'percentile']) |
19 | 18 |
|
20 |
| -numTests = 5 |
| 19 | +# GLOBAL CONSTANTS |
21 | 20 | testNames = ['Pacer Test', 'Flexed Arm\n Hang', 'Mile Run', 'Agility',
|
22 | 21 | 'Push Ups']
|
23 |
| -testMeta = ['laps', 'sec', 'min:sec', 'sec', ''] |
24 |
| -scores = ['7', '48', '12:52', '17', '14'] |
25 |
| -rankings = np.round(np.random.uniform(0, 1, numTests)*100, 0) |
26 |
| - |
27 |
| - |
28 |
| -fig, ax1 = plt.subplots(figsize=(9, 7)) |
29 |
| -plt.subplots_adjust(left=0.115, right=0.88) |
30 |
| -fig.canvas.set_window_title('Eldorado K-8 Fitness Chart') |
31 |
| -pos = np.arange(numTests) + 0.5 # Center bars on the Y-axis ticks |
32 |
| -rects = ax1.barh(pos, rankings, align='center', height=0.5, color='m') |
33 |
| - |
34 |
| -ax1.axis([0, 100, 0, 5]) |
35 |
| -plt.yticks(pos, testNames) |
36 |
| -ax1.set_title('Johnny Doe') |
37 |
| -plt.text(50, -0.5, 'Cohort Size: ' + str(cohortSize), |
38 |
| - horizontalalignment='center', size='small') |
39 |
| - |
40 |
| -# Set the right-hand Y-axis ticks and labels and set X-axis tick marks at the |
41 |
| -# deciles |
42 |
| -ax2 = ax1.twinx() |
43 |
| -ax2.plot([100, 100], [0, 5], 'white', alpha=0.1) |
44 |
| -ax2.xaxis.set_major_locator(MaxNLocator(11)) |
45 |
| -xticks = plt.setp(ax2, xticklabels=['0', '10', '20', '30', '40', '50', '60', |
46 |
| - '70', '80', '90', '100']) |
47 |
| -ax2.xaxis.grid(True, linestyle='--', which='major', color='grey', |
48 |
| - alpha=0.25) |
49 |
| -# Plot a solid vertical gridline to highlight the median position |
50 |
| -plt.plot([50, 50], [0, 5], 'grey', alpha=0.25) |
51 |
| - |
52 |
| -# Build up the score labels for the right Y-axis by first appending a carriage |
53 |
| -# return to each string and then tacking on the appropriate meta information |
54 |
| -# (i.e., 'laps' vs 'seconds'). We want the labels centered on the ticks, so if |
55 |
| -# there is no meta info (like for pushups) then don't add the carriage return to |
56 |
| -# the string |
57 |
| - |
58 |
| - |
59 |
| -def withnew(i, scr): |
60 |
| - if testMeta[i] != '': |
61 |
| - return '%s\n' % scr |
| 22 | +testMeta = dict(zip(testNames, ['laps', 'sec', 'min:sec', 'sec', ''])) |
| 23 | + |
| 24 | + |
| 25 | +def attach_ordinal(num): |
| 26 | + """helper function to add ordinal string to integers |
| 27 | +
|
| 28 | + 1 -> 1st |
| 29 | + 56 -> 56th |
| 30 | + """ |
| 31 | + suffixes = dict((str(i), v) for i, v in |
| 32 | + enumerate(['th', 'st', 'nd', 'rd', 'th', |
| 33 | + 'th', 'th', 'th', 'th', 'th'])) |
| 34 | + |
| 35 | + v = str(num) |
| 36 | + # special case early teens |
| 37 | + if v in {'11', '12', '13'}: |
| 38 | + return v + 'th' |
| 39 | + return v + suffixes[v[-1]] |
| 40 | + |
| 41 | + |
| 42 | +def format_score(scr, test): |
| 43 | + """ |
| 44 | + Build up the score labels for the right Y-axis by first |
| 45 | + appending a carriage return to each string and then tacking on |
| 46 | + the appropriate meta information (i.e., 'laps' vs 'seconds'). We |
| 47 | + want the labels centered on the ticks, so if there is no meta |
| 48 | + info (like for pushups) then don't add the carriage return to |
| 49 | + the string |
| 50 | + """ |
| 51 | + md = testMeta[test] |
| 52 | + if md: |
| 53 | + return '{}\n{}'.format(scr, md) |
62 | 54 | else:
|
63 | 55 | return scr
|
64 | 56 |
|
65 |
| -scoreLabels = [withnew(i, scr) for i, scr in enumerate(scores)] |
66 |
| -scoreLabels = [i + j for i, j in zip(scoreLabels, testMeta)] |
67 |
| -# set the tick locations |
68 |
| -ax2.set_yticks(pos) |
69 |
| -# set the tick labels |
70 |
| -ax2.set_yticklabels(scoreLabels) |
71 |
| -# make sure that the limits are set equally on both yaxis so the ticks line up |
72 |
| -ax2.set_ylim(ax1.get_ylim()) |
73 |
| - |
74 |
| - |
75 |
| -ax2.set_ylabel('Test Scores') |
76 |
| -# Make list of numerical suffixes corresponding to position in a list |
77 |
| -# 0 1 2 3 4 5 6 7 8 9 |
78 |
| -suffixes = ['th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th'] |
79 |
| -ax2.set_xlabel('Percentile Ranking Across ' + str(grade) + suffixes[grade] |
80 |
| - + ' Grade ' + gender.title() + 's') |
81 |
| - |
82 |
| -# Lastly, write in the ranking inside each bar to aid in interpretation |
83 |
| -for rect in rects: |
84 |
| - # Rectangle widths are already integer-valued but are floating |
85 |
| - # type, so it helps to remove the trailing decimal point and 0 by |
86 |
| - # converting width to int type |
87 |
| - width = int(rect.get_width()) |
88 |
| - |
89 |
| - # Figure out what the last digit (width modulo 10) so we can add |
90 |
| - # the appropriate numerical suffix (e.g., 1st, 2nd, 3rd, etc) |
91 |
| - lastDigit = width % 10 |
92 |
| - # Note that 11, 12, and 13 are special cases |
93 |
| - if (width == 11) or (width == 12) or (width == 13): |
94 |
| - suffix = 'th' |
95 |
| - else: |
96 |
| - suffix = suffixes[lastDigit] |
97 | 57 |
|
98 |
| - rankStr = str(width) + suffix |
99 |
| - if (width < 5): # The bars aren't wide enough to print the ranking inside |
100 |
| - xloc = width + 1 # Shift the text to the right side of the right edge |
101 |
| - clr = 'black' # Black against white background |
102 |
| - align = 'left' |
| 58 | +def format_ycursor(y): |
| 59 | + y = int(y) |
| 60 | + if y < 0 or y >= len(testNames): |
| 61 | + return '' |
103 | 62 | else:
|
104 |
| - xloc = 0.98*width # Shift the text to the left side of the right edge |
105 |
| - clr = 'white' # White on magenta |
106 |
| - align = 'right' |
107 |
| - |
108 |
| - # Center the text vertically in the bar |
109 |
| - yloc = rect.get_y() + rect.get_height()/2.0 |
110 |
| - ax1.text(xloc, yloc, rankStr, horizontalalignment=align, |
111 |
| - verticalalignment='center', color=clr, weight='bold') |
112 |
| - |
113 |
| -plt.show() |
| 63 | + return testNames[y] |
| 64 | + |
| 65 | + |
| 66 | +def plot_student_results(student, scores, cohort_size): |
| 67 | + # create the figure |
| 68 | + fig, ax1 = plt.subplots(figsize=(9, 7)) |
| 69 | + fig.subplots_adjust(left=0.115, right=0.88) |
| 70 | + fig.canvas.set_window_title('Eldorado K-8 Fitness Chart') |
| 71 | + |
| 72 | + pos = np.arange(len(testNames)) + 0.5 # Center bars on the Y-axis ticks |
| 73 | + |
| 74 | + rects = ax1.barh(pos, [scores[k].percentile for k in testNames], |
| 75 | + align='center', |
| 76 | + height=0.5, color='m', |
| 77 | + tick_label=testNames) |
| 78 | + |
| 79 | + ax1.set_title(student.name) |
| 80 | + |
| 81 | + ax1.set_xlim([0, 100]) |
| 82 | + ax1.xaxis.set_major_locator(MaxNLocator(11)) |
| 83 | + ax1.xaxis.grid(True, linestyle='--', which='major', |
| 84 | + color='grey', alpha=.25) |
| 85 | + |
| 86 | + # Plot a solid vertical gridline to highlight the median position |
| 87 | + ax1.axvline(50, color='grey', alpha=0.25) |
| 88 | + # set X-axis tick marks at the deciles |
| 89 | + cohort_label = ax1.text(.5, -.07, 'Cohort Size: {}'.format(cohort_size), |
| 90 | + horizontalalignment='center', size='small', |
| 91 | + transform=ax1.transAxes) |
| 92 | + |
| 93 | + # Set the right-hand Y-axis ticks and labels |
| 94 | + ax2 = ax1.twinx() |
| 95 | + |
| 96 | + scoreLabels = [format_score(scores[k].score, k) for k in testNames] |
| 97 | + |
| 98 | + # set the tick locations |
| 99 | + ax2.set_yticks(pos) |
| 100 | + # make sure that the limits are set equally on both yaxis so the |
| 101 | + # ticks line up |
| 102 | + ax2.set_ylim(ax1.get_ylim()) |
| 103 | + |
| 104 | + # set the tick labels |
| 105 | + ax2.set_yticklabels(scoreLabels) |
| 106 | + |
| 107 | + ax2.set_ylabel('Test Scores') |
| 108 | + |
| 109 | + ax2.set_xlabel(('Percentile Ranking Across ' |
| 110 | + '{grade} Grade {gender}s').format( |
| 111 | + grade=attach_ordinal(student.grade), |
| 112 | + gender=student.gender.title())) |
| 113 | + |
| 114 | + rect_labels = [] |
| 115 | + # Lastly, write in the ranking inside each bar to aid in interpretation |
| 116 | + for rect in rects: |
| 117 | + # Rectangle widths are already integer-valued but are floating |
| 118 | + # type, so it helps to remove the trailing decimal point and 0 by |
| 119 | + # converting width to int type |
| 120 | + width = int(rect.get_width()) |
| 121 | + |
| 122 | + rankStr = attach_ordinal(width) |
| 123 | + # The bars aren't wide enough to print the ranking inside |
| 124 | + if (width < 5): |
| 125 | + # Shift the text to the right side of the right edge |
| 126 | + xloc = width + 1 |
| 127 | + # Black against white background |
| 128 | + clr = 'black' |
| 129 | + align = 'left' |
| 130 | + else: |
| 131 | + # Shift the text to the left side of the right edge |
| 132 | + xloc = 0.98*width |
| 133 | + # White on magenta |
| 134 | + clr = 'white' |
| 135 | + align = 'right' |
| 136 | + |
| 137 | + # Center the text vertically in the bar |
| 138 | + yloc = rect.get_y() + rect.get_height()/2.0 |
| 139 | + label = ax1.text(xloc, yloc, rankStr, horizontalalignment=align, |
| 140 | + verticalalignment='center', color=clr, weight='bold', |
| 141 | + clip_on=True) |
| 142 | + rect_labels.append(label) |
| 143 | + |
| 144 | + # make the interactive mouse over give the bar title |
| 145 | + ax2.fmt_ydata = format_ycursor |
| 146 | + # return all of the artists created |
| 147 | + return {'fig': fig, |
| 148 | + 'ax': ax1, |
| 149 | + 'ax_right': ax2, |
| 150 | + 'bars': rects, |
| 151 | + 'perc_labels': rect_labels, |
| 152 | + 'cohort_label': cohort_label} |
| 153 | + |
| 154 | +student = Student('Johnny Doe', 2, 'boy') |
| 155 | +scores = dict(zip(testNames, |
| 156 | + (Score(v, p) for v, p in |
| 157 | + zip(['7', '48', '12:52', '17', '14'], |
| 158 | + np.round(np.random.uniform(0, 1, |
| 159 | + len(testNames))*100, 0))))) |
| 160 | +cohort_size = 62 # The number of other 2nd grade boys |
| 161 | + |
| 162 | +arts = plot_student_results(student, scores, cohort_size) |
0 commit comments