1

I am visualizing the results of a survey. The answers are long and I would like to fit them entirely into the graph. Therefore, I would be very grateful if you could point me to a way to have multi-line xticklabels, or include the xticklabels in a legend on the side as seen in this example:

enter image description here

Because otherwise I would have to make the graph very wide to fit the entire answer. My current code and the resulting plot look as follows:

import seaborn as sns
from textwrap import wrap

sns.set(style="dark")
catp = (sns.catplot(data=results, x='1',
                    kind='count',
                    hue_order=results.sort_values('1')['1'],
                    palette='crest',
                    height=3.3,
                    aspect=17.4/7)
        .set(xlabel=None,
             ylabel='Number of Participants',
             title="\n".join(wrap("Question 1: Out of the three options, please choose the one you would prefer your fully autonomous car to choose, if you sat in it.", 90)))
)
plt.tight_layout()
catp.ax.set_yticks((0,10,20,30,40))
for p in catp.ax.patches:
    percentage = '{:.1f}%'.format(100 * p.get_height()/92)
    x = p.get_x() + p.get_width() / 2 - 0.05
    y = p.get_y() + p.get_height() + 0.3
    catp.ax.annotate(percentage, (x, y), size = 12)
plt.show()

enter image description here

Best regards!

Edit: You can create a sample dataframe with this code:

import pandas as pd
import numpy as np
from itertools import chain

x = (np.repeat('Brake and crash into the bus', 37),
np.repeat('Steer into the passing car on the left', 22),
np.repeat('Steer into the right hand sidewall', 39))

results = pd.DataFrame({'1': list(chain(*x))})
Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Zwiebak
  • 344
  • 1
  • 11

1 Answers1

3
  • Extract xticklabels and fix them with wrap as you did with the title
  • matplotlib 3.4.2 now comes with .bar_label to more easily annotate bars
    • See this answer for customizing the bar annotation labels.
  • The height and aspect of the figure will still require some adjusting depending on wrap width.
  • An alternate solution is to fix the values in the dataframe:
    • df['1'] = df['1'].apply(lambda row: '\n'.join(wrap(row, 30)))
    • for col in df.columns: df[col] = df[col].apply(lambda row: '\n'.join(wrap(row, 30))) for all columns.
  • The list comprehension for labels uses an assignment expression (:=), which requires python >= 3.8. This can be rewritten as a standard for loop.
    • labels = [f'{v.get_height()/len(df)*100:0.1f}%' for v in c] works without an assignment expression, but doesn't check if the bar height is 0.
  • Tested in python 3.8.11, pandas 1.3.2, matplotlib 3.4.2, seaborn 0.11.2
import seaborn as sns
from textwrap import wrap
from itertools import chain
import pandas as pd
import numpy as np

# sample dataframe
x = (np.repeat('Brake and crash into the bus, which will result in the killing of the children on the bus, but save your life', 37),
np.repeat('Steer into the passing car on the left, pushing it into the wall, saving your life, but killing passengers in the other car', 22),
np.repeat('Steer into the right hand sidewall, killing you but saving the lives of all other passengers', 39))

df = pd.DataFrame({'1': list(chain(*x))})

# plotting
sns.set(style="dark")
catp = (sns.catplot(data=df, x='1',
                    kind='count',
                    hue_order=df.sort_values('1')['1'],
                    palette='crest',
                    height=5,
                    aspect=17.4/7)
        .set(xlabel=None,
             ylabel='Number of Participants',
             title="\n".join(wrap("Question 1: Out of the three options, please choose the one you would prefer your fully autonomous car to choose, if you sat in it.", 90)))
)
plt.tight_layout()
catp.ax.set_yticks((0,10,20,30,40))

for ax in catp.axes.ravel():

    # extract labels
    labels = ax.get_xticklabels()
    # fix the labels
    for v in labels:
        text = v.get_text()
        text = '\n'.join(wrap(text, 30))
        v.set_text(text)
    # set the new labels
    ax.set_xticklabels(labels)
    # annotate the bars
    for c in ax.containers:

        # create a custom annotation: percent of total
        labels = [f'{w/len(df)*100:0.1f}%' if (w := v.get_height()) > 0 else '' for v in c]
        
        ax.bar_label(c, labels=labels, label_type='edge')

enter image description here

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158