Skip to content

Commit 3bc2cbd

Browse files
authored
Merge pull request matplotlib#20648 from timhoffm/doc-barchart-demp
Simplify barchart_demo
2 parents 579475b + 69805d2 commit 3bc2cbd

File tree

1 file changed

+44
-101
lines changed

1 file changed

+44
-101
lines changed

examples/statistics/barchart_demo.py

Lines changed: 44 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,16 @@
1515
just make up some data for little Johnny Doe.
1616
"""
1717

18+
from collections import namedtuple
1819
import numpy as np
1920
import matplotlib.pyplot as plt
20-
from matplotlib.ticker import MaxNLocator
21-
from collections import namedtuple
2221

23-
np.random.seed(42)
2422

2523
Student = 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

Comments
 (0)