Skip to content

Instantly share code, notes, and snippets.

@camriddell
Last active October 25, 2022 18:19
Show Gist options
  • Save camriddell/24d329bf7dde86c657ac6c7590222a93 to your computer and use it in GitHub Desktop.
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/
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)
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