Last active
October 25, 2022 18:19
-
-
Save camriddell/24d329bf7dde86c657ac6c7590222a93 to your computer and use it in GitHub Desktop.
Summary of some of the ways one can work with long labels in both matplotlib & bokeh. Based on an example created in ggplot2 (R) https://www.andrewheiss.com/blog/2022/06/23/long-labels-ggplot/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
*.html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from bokeh.layouts import column, layout | |
from bokeh.io import curdoc, show, export_png | |
from bokeh.models import ColumnDataSource, FactorRange, Plot, Div | |
from bokeh.models.labeling import NoOverlap | |
from bokeh.plotting import figure | |
from bokeh.themes import Theme | |
from pandas import read_csv | |
from math import pi | |
s = ( | |
read_csv('https://datavizs22.classes.andrewheiss.com/projects/04-exercise/data/EssentialConstruction.csv') | |
.groupby('CATEGORY')['CATEGORY'].count() | |
.sort_values(ascending=False) | |
) | |
# Set defaults for bokeh plots via theming | |
doc = curdoc() | |
doc.theme = Theme(json={'attrs': { | |
'Figure': {'width': 600, 'plot_height': 200, 'height': 250, 'toolbar_location': None}, | |
'Title': {'text_font_size': '16pt'}, | |
'Axis': {'major_label_text_font_size': '12pt'}, | |
'Grid': {'grid_line_color': None}, | |
'VBar': {'width': .9}, | |
'HBar': {'height': .9}, | |
}}) | |
# Bokeh needs categorical axes values specified in advance | |
# unless you manually build a Plot with your own | |
# CategoricalAxis, CategoricalTicker, CategoricalRange, & CategoricalScale | |
cds = ColumnDataSource(s.to_frame('count')) | |
factors = cds.data['CATEGORY'] | |
plots = layout([ | |
[figure(name='original', x_range=factors), figure(name='manual recoding', x_range=factors)], | |
[figure(name='wider', x_range=factors)], | |
[figure(name='rotate labels', x_range=factors), figure(name='swap x & y', y_range=factors)], | |
[figure(name='label policy', x_range=factors), figure(name='text wrap', x_range=factors)], | |
]) | |
## Original | |
p = plots.select_one({'name': 'original'}) | |
p.background_fill_color = '#EEEE9B' | |
p.vbar(x='CATEGORY', top='count', source=cds) | |
## Manual Recoding | |
from bokeh.models import FuncTickFormatter | |
p = plots.select_one({'name': 'manual recoding'}) | |
p.vbar(x='CATEGORY', top='count', source=cds) | |
new_names = { | |
'Approved Work': 'App. Work', | |
'Affordable Housing': 'Aff. House', | |
'Hospital / Health Care': 'Hosp\n& Health', | |
'Public Housing': 'Pub. Hous.', | |
'Homeless Shelter': 'Homeless\nShelter' | |
} | |
p.xaxis.formatter = FuncTickFormatter( # substitution happens on JavaScript side | |
args={'new_names': new_names}, | |
code=''' | |
return (tick in new_names) ? new_names[tick] : tick | |
''' | |
) | |
## Wider | |
p = plots.select_one({'name': 'wider'}) | |
p.vbar(x='CATEGORY', top='count', source=cds) | |
p.width = 1200 | |
## Swap x- & y- axes | |
p = plots.select_one({'name': 'swap x & y'}) | |
p.hbar(y='CATEGORY', right='count', source=cds) | |
p.y_range.factors = p.y_range.factors[::-1] | |
## Rotate tick labels | |
p = plots.select_one({'name': 'rotate labels'}) | |
p.vbar(x='CATEGORY', top='count', source=cds) | |
p.xaxis.major_label_orientation = pi / 8 | |
## Label Policy - No Overlap | |
p = plots.select_one({'name': 'label policy'}) | |
p.vbar(x='CATEGORY', top='count', source=cds) | |
p.xaxis.major_label_policy = NoOverlap() | |
## Text Wrap | |
from textwrap import fill | |
p = plots.select_one({'name': 'text wrap'}) | |
p.vbar(x='CATEGORY', top='count', source=cds) | |
names = {label: fill(label, width=10) for label in p.x_range.factors} | |
p.xaxis.formatter = FuncTickFormatter( | |
args={'new_names': names}, | |
code=''' | |
return (tick in new_names) ? new_names[tick] : tick | |
''' | |
) | |
for p in plots.select({'type': Plot}): | |
p.title = p.name.title() | |
final_layout = column( | |
Div(text='<h1>Dealing With Long Labels in Bokeh</h1>'), | |
plots | |
) | |
show(final_layout) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from matplotlib.pyplot import rc, show, setp, subplot_mosaic | |
from matplotlib import rcdefaults | |
from pandas import read_csv | |
rcdefaults() | |
rc('font', size=12) | |
rc('axes', titlesize=16, titlelocation='left') | |
rc('axes.spines', top=False, right=False) | |
mosaic = [ | |
['original', 'manually recode'], | |
['wider plot', 'wider plot'], | |
['swap x- and y-axes', 'rotate labels'], | |
['dodge labels', 'automatic breaks'] | |
] | |
fig, axd = subplot_mosaic( | |
mosaic, | |
figsize=(16, 12), | |
gridspec_kw={'wspace': .1, 'hspace': 0.6, 'right': .97} | |
) | |
s = ( | |
read_csv('https://datavizs22.classes.andrewheiss.com/projects/04-exercise/data/EssentialConstruction.csv') | |
.groupby('CATEGORY')['CATEGORY'].count() | |
.sort_values(ascending=False) | |
) | |
# Original | |
axd['original'].bar(s.index, s) | |
axd['original'].set_facecolor('#EEEE9B') | |
# Manually Recode | |
new_names = { | |
'Approved Work': 'App. Work', | |
'Affordable Housing': 'Aff. House', | |
'Hospital / Health Care': 'Hosp\n& Health', | |
'Public Housing': 'Pub. Hous.', | |
'Homeless Shelter': 'Homeless\nShelter' | |
} | |
axd['manually recode'].bar(s.rename(new_names).index, s) | |
# Wider plot | |
axd['wider plot'].bar(s.index, s) | |
# Swap x and y | |
axd['swap x- and y-axes'].barh(s.index, s) | |
axd['swap x- and y-axes'].invert_yaxis() | |
# Rotate | |
axd['rotate labels'].bar(s.index, s) | |
setp( | |
axd['rotate labels'].get_xticklabels(), | |
rotation=20, ha='right', va='top', rotation_mode='anchor' | |
) | |
## Scale down the axes height to fit rotated labels | |
bbox = axd['rotate labels'].get_position() | |
new_height = bbox.height * .8 | |
axd['rotate labels'].set_position([bbox.x0, bbox.y1 - new_height, bbox.width, new_height]) | |
# Dodge | |
axd['dodge labels'].bar(s.index, s) | |
setp(axd['dodge labels'].get_xticklabels()[1::2], y=-.15) | |
from textwrap import fill | |
axd['automatic breaks'].bar(s.index, s) | |
axd['automatic breaks'].set_xticks( | |
range(s.index.size), | |
labels=[fill(s, width=10) for s in s.index] | |
) | |
# Add title to all plots | |
for label, ax in axd.items(): | |
ax.set_title(label.title()) | |
fig.suptitle('Working With Long Labels in Matplotlib', y=.95, fontsize='xx-large') | |
fig.savefig('working_with_long_labels_matplotlib.png') | |
show() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment