Skip to content

Commit 7d0ae19

Browse files
committed
Add horizontal bar 9.20
1 parent a4aca8f commit 7d0ae19

File tree

4 files changed

+887
-2
lines changed

4 files changed

+887
-2
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ Here I have implemented few graphs from the book using Python and matplotlib lib
1616
![](images/Figure_0-5.png) |![](images/Figure_3-34.png)
1717
[Figure 4.9](horizontal-bar/figure-4-9.ipynb)|[Figure 5.13](vertical-bar/figure-5-13.ipynb)
1818
![](images/Figure_4-9.png) |![](images/Figure_5-13.png)
19-
[Figure 6.4](vertical-bar/figure-6-4.ipynb) |
20-
![](images/Figure_6-4.png) |
19+
[Figure 6.4](vertical-bar/figure-6-4.ipynb) |[Figure 9.20](vertical-bar/figure-9-20.ipynb)
20+
![](images/Figure_6-4.png) |![](images/Figure_9-20.png)
2121

2222
## Slopegraphs
2323
[Figure 9.32](slopegraph/figure-9-32.ipynb)|

horizontal-bar/figure-9-20.ipynb

Lines changed: 542 additions & 0 deletions
Large diffs are not rendered by default.

horizontal-bar/figure-9-20.py

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
#!/usr/bin/env python
2+
# coding: utf-8
3+
4+
import matplotlib
5+
from matplotlib import transforms, patches, pyplot as plt
6+
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
7+
import numpy as np
8+
import seaborn as sns
9+
10+
11+
# inline matplotlib plots
12+
get_ipython().run_line_magic('matplotlib', 'inline')
13+
14+
15+
# define colors
16+
GRAY7, GRAY9 = '#929497', '#BFBEBE'
17+
BLUE_GREEN = '#31859D'
18+
BLUE_GREEN_LIGHT ='#92CDDD'
19+
BLUE_DARK = '#1E497D'
20+
BLUE_LIGHT = '#95B3D7'
21+
ORANGE_LIGHT = '#FBC08F'
22+
RED3 = '#DE3A2F'
23+
24+
25+
def hex_to_rgb(hex_value):
26+
h = hex_value.lstrip('#')
27+
return tuple(int(h[i:i + 2], 16) / 255.0 for i in (0, 2, 4))
28+
29+
def print_colors(colors):
30+
rgb_colors = list(map(hex_to_rgb, colors))
31+
sns.palplot(rgb_colors)
32+
33+
34+
print_colors([
35+
GRAY7,
36+
GRAY9,
37+
BLUE_GREEN,
38+
BLUE_GREEN_LIGHT,
39+
BLUE_DARK,
40+
BLUE_LIGHT,
41+
ORANGE_LIGHT,
42+
RED3
43+
])
44+
45+
46+
# configure plot font family to Arial
47+
plt.rcParams['font.family'] = 'Arial'
48+
# configure mathtext bold and italic font family to Arial
49+
matplotlib.rcParams['mathtext.fontset'] = 'custom'
50+
matplotlib.rcParams['mathtext.bf'] = 'Arial:bold'
51+
matplotlib.rcParams['mathtext.it'] = 'Arial:italic'
52+
53+
54+
# Returns the x coordinates of a text element on a given axis of a given
55+
# figure.
56+
# Used to position elements on the canvas
57+
# Returns object with attributes:
58+
# x0 coordinate of the text element
59+
# x1 coordinate of the text element
60+
# y0 coordinate of the text element
61+
# y1 coordinate of the text element
62+
def get_text_coordinates(text_element, ax, fig):
63+
x0 = text_element.get_window_extent(fig.canvas.get_renderer()).x0
64+
x1 = text_element.get_window_extent(fig.canvas.get_renderer()).x1
65+
y0 = text_element.get_window_extent(fig.canvas.get_renderer()).y0
66+
y1 = text_element.get_window_extent(fig.canvas.get_renderer()).y1
67+
return {
68+
'x0': round(ax.transData.inverted().transform_point((x0, 0))[0], 2),
69+
'x1': round(ax.transData.inverted().transform_point((x1, 0))[0], 2),
70+
'y0': round(ax.transData.inverted().transform_point((0, y0))[1], 2),
71+
'y1': round(ax.transData.inverted().transform_point((0, y1))[1], 2)
72+
}
73+
74+
75+
# A to O
76+
features = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K' ,'L', 'M', 'N', 'O']
77+
feature_labels = [f'Feature {x}' for x in features]
78+
79+
80+
X_values = [
81+
[0, 1, 1, 11, 40, 47], # A
82+
[0, 2, 2, 13, 36, 47], # B
83+
[2, 2, 5, 24, 34, 33], # C
84+
[8, 1, 4, 21, 37, 29], # D
85+
[6, 1, 6, 23, 36, 28], # E
86+
[14, 1, 5, 20, 35, 25], # F
87+
[19, 2, 5, 15, 26, 33], # G
88+
[13, 1, 6, 23, 32, 25], # H
89+
[22, 2, 5, 17, 27, 27], # I
90+
[2, 8, 14, 24, 27, 25], # J
91+
[29, 1, 4, 17, 28, 21], # K
92+
[29, 1, 4, 23, 27, 16], # L
93+
[33, 3, 8, 25, 18, 13], # M
94+
[26, 9, 14, 24, 17, 10], # N
95+
[51, 1, 6, 15, 16, 11] # O
96+
]
97+
98+
99+
colors = [
100+
([BLUE_GREEN_LIGHT] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_DARK]*2), # A
101+
([BLUE_GREEN_LIGHT] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_DARK]*2), # B
102+
([BLUE_GREEN_LIGHT] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_LIGHT]*2), # C
103+
([BLUE_GREEN_LIGHT] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_LIGHT]*2), # D
104+
([BLUE_GREEN_LIGHT] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_LIGHT]*2), # E
105+
([BLUE_GREEN_LIGHT] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_LIGHT]*2), # F
106+
([BLUE_GREEN_LIGHT] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_LIGHT]*2), # G
107+
([BLUE_GREEN_LIGHT] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_LIGHT]*2), # H
108+
([BLUE_GREEN_LIGHT] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_LIGHT]*2), # I
109+
([BLUE_GREEN_LIGHT] + [RED3]*2 + [GRAY9] + [BLUE_LIGHT]*2), # J
110+
([BLUE_GREEN_LIGHT] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_LIGHT]*2), # K
111+
([BLUE_GREEN_LIGHT] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_LIGHT]*2), # L
112+
([BLUE_GREEN_LIGHT] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_LIGHT]*2), # M
113+
([BLUE_GREEN_LIGHT] + [RED3]*2 + [GRAY9] + [BLUE_LIGHT]*2), # N
114+
([BLUE_GREEN] + [ORANGE_LIGHT]*2 + [GRAY9] + [BLUE_LIGHT]*2) # 0
115+
]
116+
117+
colors
118+
119+
120+
feature_labels
121+
122+
123+
X_values_reversed_transposed = np.array(X_values[::-1]).T.tolist()
124+
colors_reversed_transposed = np.array(colors[::-1]).T.tolist()
125+
feature_labels_reversed = feature_labels[::-1]
126+
127+
128+
# create a horizontal stacked bar chart
129+
fig, ax = plt.subplots(figsize=(25, 7), # width, height in inches
130+
dpi=110) # resolution of the figure
131+
132+
# tune the subplot layout by setting sides of the figure
133+
fig.subplots_adjust(left=0.28, right=0.53, top=0.61, bottom=0.107)
134+
135+
widths = np.cumsum(X_values, axis=1)[::-1].T.tolist()
136+
137+
for i, (colname, color) in enumerate(zip(X_values_reversed_transposed, colors_reversed_transposed)):
138+
bars = ax.barh(feature_labels_reversed,
139+
colname,
140+
color=color,
141+
edgecolor='white',
142+
height=0.65,
143+
left=np.sum(X_values_reversed_transposed[:i], axis=0))
144+
145+
146+
for j, b in enumerate(bars):
147+
# 3% is too small to display the label
148+
if b.get_width() <= 3:
149+
continue
150+
151+
# We could annotate all values, but only need to draw attention
152+
# to these specific ones
153+
if color[j] not in [BLUE_DARK, RED3, BLUE_GREEN]:
154+
continue
155+
156+
width = widths[i][j]
157+
158+
plt.text(width - 0.7, b.get_y() + b.get_height() / 2,
159+
f'{b.get_width():.0f}%',
160+
ha='right',
161+
va='center',
162+
fontsize=6,
163+
color='white')
164+
165+
# set y ticks and labels
166+
ax.set_yticks(range(len(feature_labels_reversed)))
167+
ax.set_yticklabels(feature_labels_reversed, fontsize=10, fontweight='bold')
168+
169+
# set x axis limits
170+
ax.set_xlim(0, 100)
171+
172+
# set x ticks and labels
173+
ax.set_xticks([0, 25, 50, 75, 100])
174+
175+
ax.tick_params(top=False, bottom=False, left=False, labelbottom=False, labeltop=False)
176+
ax.tick_params(color=GRAY7)
177+
ax.spines['top'].set_visible(False)
178+
ax.spines['left'].set_visible(False)
179+
ax.spines['right'].set_visible(False)
180+
ax.spines['bottom'].set_visible(False)
181+
182+
# Legend
183+
legends_end_x1 = 100
184+
185+
legend_texts = [
186+
'- Have not used',
187+
'- Not satisfied at all',
188+
'- Not very satisfied',
189+
'- Somewhat satisfied',
190+
'- Very satisfied',
191+
'- Completely satisfied',
192+
]
193+
194+
legend_fontsize = 8
195+
196+
# Drawing the elements to get their size with respect to coordinates to
197+
# calculate the starting position given the end position and the desired spacing
198+
legends_length = 0
199+
for text in legend_texts:
200+
element = ax.text(0, 0, text, fontsize=legend_fontsize, fontweight='bold')
201+
legends_length += get_text_coordinates(element, ax=ax, fig=fig)['x1']
202+
element.set_visible(False)
203+
204+
spacing = 3
205+
206+
legends_start_x0 = legends_end_x1 - legends_length - spacing * (len(legend_texts) - 1)
207+
208+
legends_y = len(feature_labels_reversed) + 0.05
209+
210+
legend_colors = [BLUE_GREEN] + [RED3] * 2 + [GRAY9] + [BLUE_DARK] * 2
211+
212+
x = legends_start_x0
213+
214+
for text, color in zip(legend_texts, legend_colors):
215+
previous_element = ax.text(x, legends_y, text, fontsize=legend_fontsize, color=color, fontweight='bold')
216+
x = get_text_coordinates(previous_element, ax=ax, fig=fig)['x1'] + spacing
217+
218+
title_x0 = legends_start_x0 - 5
219+
220+
# Calculate spacing dynamically
221+
side_box_text_1 = 'Features A and B\ncontinue to top user\nsatisfaction'
222+
side_box_text_2 = 'Users are least\nsatisfied with\nFeatures J and N;\nwhat improvements\n' 'can we make here\nfor a better user\nexperience?'
223+
224+
# In the following text, I added two spaces to match the size of the above boxes.
225+
# Should be fixed by using a monospace font
226+
side_box_text_3 = 'Feature O is least\nused. What steps\n' 'can we proactively \n' 'take with existing\n' 'users to increase\n' 'utilization?'
227+
228+
linespacing = 1.3
229+
fontsize = 10
230+
231+
232+
side_boxes_height = 0
233+
__el = ax.text(0, 0, side_box_text_1, fontsize=fontsize, linespacing=linespacing)
234+
side_boxes_height += get_text_coordinates(__el, ax=ax, fig=fig)['y1']
235+
__el.set_visible(False)
236+
237+
__el = ax.text(0, 0, side_box_text_2, fontsize=fontsize, linespacing=linespacing)
238+
side_boxes_height += get_text_coordinates(__el, ax=ax, fig=fig)['y1']
239+
__el.set_visible(False)
240+
241+
__el = ax.text(0, 0, side_box_text_3, fontsize=fontsize, linespacing=linespacing)
242+
side_boxes_height += get_text_coordinates(__el, ax=ax, fig=fig)['y1']
243+
__el.set_visible(False)
244+
245+
246+
side_boxes_start_y0 = 0.07 # to handle the padding from the background color
247+
side_boxes_end_y1 = len(feature_labels_reversed) - 1
248+
249+
spacing = (side_boxes_end_y1 - side_boxes_start_y0 - side_boxes_height) / (3 - 1) # n_elements - 1
250+
251+
side_box_x = 102.5
252+
253+
y1 = get_text_coordinates(previous_element, ax=ax, fig=fig)['y1'] + spacing
254+
255+
previous_element = ax.text(side_box_x, side_boxes_start_y0, side_box_text_3,
256+
fontsize=fontsize, linespacing=linespacing, backgroundcolor=BLUE_GREEN,
257+
color='white')
258+
259+
y1 = get_text_coordinates(previous_element, ax=ax, fig=fig)['y1'] + spacing
260+
261+
previous_element = ax.text(side_box_x, y1, side_box_text_2,
262+
fontsize=fontsize, linespacing=linespacing, backgroundcolor=RED3,
263+
color='white')
264+
265+
y1 = get_text_coordinates(previous_element, ax=ax, fig=fig)['y1'] + spacing
266+
267+
ax.text(side_box_x, y1, side_box_text_1,
268+
fontsize=fontsize, linespacing=linespacing, backgroundcolor=BLUE_DARK,
269+
color='white')
270+
271+
272+
rightmost_x = get_text_coordinates(previous_element, ax=ax, fig=fig)['x1']
273+
274+
275+
title = ax.text(title_x0, len(feature_labels_reversed) + 3.15,
276+
'User satisfaction varies greatly by feature',
277+
fontsize=15, backgroundcolor='none', color='white',
278+
ha='left')
279+
280+
title_coordinates = get_text_coordinates(title, ax=ax, fig=fig)
281+
282+
# I cannot use background color with the title, because it does not extend up to
283+
# the rightmost x coordinate.
284+
# I could not do this with a bbox config.
285+
# I could not use a rectangle directly because it is not being drawn out of the
286+
# ax x/y limits.
287+
# I create another axis, ax2, and I create a rectangle there.
288+
title_x1 = title_coordinates['x1']
289+
title_y0 = title_coordinates['y0']
290+
title_y1 = title_coordinates['y1']
291+
292+
height = title_y1 - title_y0
293+
width = rightmost_x - title_x0
294+
295+
padding = 0.65 # space around the text
296+
297+
padding_coord = (height / 2 * padding)
298+
299+
ax2_x_min = title_x0 - padding_coord
300+
ax2_y_min = title_y0 - padding_coord
301+
ax2_y_max = title_y1 + padding_coord
302+
303+
ax2_height = ax2_y_max - ax2_y_min
304+
305+
tweak_coords = 1.45 # extend the box a bit to align with background color of side texts
306+
307+
ax2_width = width + 2 * padding_coord + tweak_coords
308+
ax2 = ax.inset_axes([ax2_x_min, ax2_y_min, ax2_width, ax2_height], transform=ax.transData, zorder=1)
309+
ax2.xaxis.set_visible(False)
310+
ax2.yaxis.set_visible(False)
311+
312+
ax2_x_max = ax2.get_xlim()[1]
313+
314+
ax2.spines['top'].set_visible(False)
315+
ax2.spines['left'].set_visible(False)
316+
ax2.spines['right'].set_visible(False)
317+
ax2.spines['bottom'].set_visible(False)
318+
319+
rect = patches.Rectangle((0, 0), ax2_x_max, ax2_y_max, linewidth=1, edgecolor=GRAY7, facecolor=GRAY7)
320+
ax2.add_patch(rect)
321+
322+
# subtitle
323+
ax.text(title_x0, len(feature_labels_reversed) + 1.50,
324+
'Product X User Satisfaction: '
325+
'$\\bf{Features}$',
326+
fontsize=10,
327+
color='#4B4B4B')
328+
329+
# footnote
330+
ax.text(title_x0, -2,
331+
'Reponses based on survey question "How satisfied have you been with each'
332+
' of these features?"\n'
333+
'Need more details here to help put this data into context: How many'
334+
' people completed survey? What proportion of users does this represent?\n'
335+
'Do those who completed survey look like the overall population'
336+
' demographic wise? When was the survey conducted?',
337+
fontsize=6, linespacing=1.3)
338+
339+
plt.show()
340+
341+
342+
343+

images/Figure_9-20.png

106 KB
Loading

0 commit comments

Comments
 (0)