Styling Matplotlib Graphs with Seaborn

In the previous article, I shared my setup for producing the graphs for research papers. However, recently when I was working on figures for a new paper, I discovered that my setup must be updated. The reason is that the new matplotlib version (since 3.6) produces a warning that the embedded seaborn styles are now deprecated. In this article, I provide the updates to the setup described in the previous article.

Table of Contents

In the previous article, we used the plt.style.use function to set the style, provided as a parameter, for the whole notebook:

# for papers
plt.style.use('seaborn-paper')
# for presentations
plt.style.use('seaborn-talk')
# for papers with colors distinguishable by colorblind people
plt.style.use('seaborn-colorblind') 
# HACK: for presentations with colors distinguishable by colorblind people
plt.style.use(['seaborn-colorblind', 'seaborn-talk'])

If you would use this function with a new version of matplotlib (version 3.6 and up), you would get a deprecation warning message:

MatplotlibDeprecationWarning: The seaborn styles shipped by Matplotlib are deprecated since 3.6, as they no longer correspond to the styles shipped by seaborn. However, they will remain available as 'seaborn-v0_8-<style>'. Alternatively, directly use the seaborn API instead.

This message provides a nice explanation of what has happened and suggests good tips on how to fix it. Let’s consider in detail how this can be done.

Approach 1: Old Styles Usage

You can continue to use the old seaborn styles the same way as it is suggested in the deprecation message. However, the name of the style should be changed a little bit by adding -v0_8- suffix, e.g., 'seaborn-v0_8-colorblind'. If you would run the following code in your notebook, you should not see the deprecation message anymore:

# using colorblind palette
plt.style.use('seaborn-v0_8-colorblind')
Matplotlib-based Approach
Matplotlib-based Approach

Figure 1 shows an example of the graph produced using this approach.

I would recommend using this approach if you want to maintain consistency with the graphs produced previously. For instance, if you resubmit your research paper and produce new graphs for a new version, you can use this approach to make all the figures look similar. This approach has an additional benefit that you do not need to introduce new dependencies for your project to produce graphs: having matplotlib is enough.

Approach 2: Using Seaborn Library Functionality

However, if you start a new project, I would recommend using a new approach because it provides more fine-grained control over the style configuration. Unfortunately, using this approach requires adding a new dependency to your project, the seaborn library, that will occupy some more hard-drive space. Luckily, this library is already among your dependencies if you do a data analysis project. Therefore, the first thing is to add this library as a new dependency. So as I use poetry for Python package management, adding a new library to a project is very easy:

$ poetry add seaborn

You can only add one this library for our test project because seaborn depends on the matplotlib, pandas and numpy packages that we use there. Now, to apply the 'seaborn-paper' style to all the graphs in your notebook, you have to substitute the plt.style.use('seaborn-v0_8-colorblind') function with the following calls:

import seaborn as sns

sns.set_style('ticks') # setting style
sns.set_context('paper') # setting context
sns.set_palette('colorblind') # setting palette
Seaborn-based Approach
Seaborn-based Approach

Figure 2 shows an example of the graph produced using this new approach. As you can see, the difference between Figure 1 and Figure 2 are minor: I can only see the difference in colors used for lines.

Let’s consider this approach in detail.

Setting Styles

The first function seaborn.set_style(style=None, rc=None) controls the general style of the graph. There are several predefined styles ('darkgrid', 'whitegrid', 'dark', 'white' and 'ticks') that can be used as values for the first style argument. It is also possible to provide a dictionary as the first argument that defines a custom style.

The seaborn.set_style(style=None, rc=None) function sets the style for the whole notebook. If you want to test the effects only for one graph, you have to use the seaborn.axes_style(style=None, rc=None) with context manager:

import math

available_styles = ['darkgrid', 'whitegrid', 'dark', 'white', 'ticks']
n_styles = len(available_styles)

fig = plt.figure(dpi=300, figsize=(12.8, 4*n_styles/2), tight_layout=True)
for i, style in enumerate(available_styles):
    with sns.axes_style(style):
        ax = fig.add_subplot(math.ceil(n_styles/2.0), 2, i+1)
        for indx, column_name in enumerate(['A', 'B', 'C', 'D', 'E', 'F', 'G']):
            ax.plot(ts_df['Date'], ts_df[column_name], label=column_name)
        ax.tick_params(axis='x', labelrotation = 90)
        ax.set(xlabel='Date', ylabel='Value')
        ax.legend(loc='center right', ncol=4)
        ax.set_title(style)

fig.show()

Figure 3 shows the result of this code execution. As you can see, the visually closest style to the one I previously used, is 'ticks'.

All Seaborn Graph Styles
All Seaborn Graph Styles

Note that using the second argument rc of the function, it is possible to adjust some rc parameters of the graph. You can use the seaborn.axes_style(style=None, rc=None) function (without providing any argument values) to see the list of all available controls:

sns.set_style('ticks') 
sns.axes_style()
Output

{'axes.facecolor': 'white',
 'axes.edgecolor': '.15',
 'axes.grid': False,
 'axes.axisbelow': True,
 'axes.labelcolor': '.15',
 'figure.facecolor': 'white',
 'grid.color': '.8',
 'grid.linestyle': '-',
 'text.color': '.15',
 'xtick.color': '.15',
 'ytick.color': '.15',
 'xtick.direction': 'out',
 'ytick.direction': 'out',
 'lines.solid_capstyle': <CapStyle.round: 'round'>,
 'patch.edgecolor': 'w',
 'patch.force_edgecolor': True,
 'image.cmap': 'rocket',
 'font.family': ['sans-serif'],
 'font.sans-serif': ['Arial',
  'DejaVu Sans',
  'Liberation Sans',
  'Bitstream Vera Sans',
  'sans-serif'],
 'xtick.bottom': True,
 'xtick.top': False,
 'ytick.left': True,
 'ytick.right': False,
 'axes.spines.left': True,
 'axes.spines.bottom': True,
 'axes.spines.right': True,
 'axes.spines.top': True}

For instance, the following code can be used to add ticks to the upper spine (see Figure 4):

with sns.axes_style(style='ticks', rc={'xtick.top': True}):
    fig, ax = plt.subplots()
    for indx, column_name in enumerate(['A', 'B', 'C', 'D', 'E', 'F', 'G']):
        ax.plot(ts_df['Date'], ts_df[column_name], label=column_name)
    ax.tick_params(axis='x', labelrotation = 90)
    ax.set(xlabel='Date', ylabel='Value')
    ax.legend(loc='center right', ncol=4)

fig.show()
Custom RC Parameters
Custom RC Parameters

The list of rc keys that can be changed using this approach is defined in the _style_keys dictionary in the seaborn library.

Setting Context

The second function, seaborn.set_context(context=None, font_scale=1, rc=None), sets the parameters that control the scaling of plot elements in the whole notebook. Similarly to set_style, the first argument of this function accepts either several predefined contexts ('notebook', 'paper', 'talk', and 'poster') or a dictionary that defines a custom context. It is also possible to scale additionally only fonts using the second font_scale argument value (the actual font sizes will be multiplied to the value provided as the second argument).

This function also has an accompanying seaborn.plotting_context(context=None, font_scale=1, rc=None). Contrary to seaborn.set_context(context=None, font_scale=1, rc=None), which sets the scaling parameters for all the graphs in the notebook, this one can be used to apply scaling to one particular graph using the context manager. Let’s use this function to see how different predefined scales look like:

available_contexts = ['paper', 'notebook', 'talk', 'poster']
n_contexts = len(available_contexts)

fig = plt.figure(dpi=300, figsize=(12.8, 4*n_contexts/2), tight_layout=True)
for i, ctx in enumerate(available_contexts):
    with sns.plotting_context(ctx):
        ax = fig.add_subplot(math.ceil(n_contexts/2.0), 2, i+1)
        for indx, column_name in enumerate(['A', 'B', 'C', 'D', 'E', 'F', 'G']):
            ax.plot(ts_df['Date'], ts_df[column_name], label=column_name)
        ax.tick_params(axis='x', labelrotation = 90)
        ax.set(xlabel='Date', ylabel='Value')
        ax.legend(loc='center right', ncol=4)
        ax.set_title(f'Context: {ctx}')

fig.show()

Figure 5 shows the result of this code. As you can see, with our figure size (6.4, 4), which we use to plot figures, only two contexts can be used for producing figures for papers: paper and notebook. If you create figures for posters or presentations, you have to use larger figure sizes.

All Seaborn Scaling Contexts
All Seaborn Scaling Contexts

It is also possible to fine-tune some of the parameter values using a dictionary with custom rc arguments. Similar to seaborn.axes_style(style=None, rc=None), if you call the seaborn.plotting_context(context=None, font_scale=1, rc=None) function without providing the arguments, you will get the values of all parameters:

sns.set_context('notebook') 
sns.plotting_context()
Output

{'font.size': 12.0,
 'axes.labelsize': 12.0,
 'axes.titlesize': 12.0,
 'xtick.labelsize': 11.0,
 'ytick.labelsize': 11.0,
 'legend.fontsize': 11.0,
 'legend.title_fontsize': 12.0,
 'axes.linewidth': 1.25,
 'grid.linewidth': 1.0,
 'lines.linewidth': 1.5,
 'lines.markersize': 6.0,
 'patch.linewidth': 1.0,
 'xtick.major.width': 1.25,
 'ytick.major.width': 1.25,
 'xtick.minor.width': 1.0,
 'ytick.minor.width': 1.0,
 'xtick.major.size': 6.0,
 'ytick.major.size': 6.0,
 'xtick.minor.size': 4.0,
 'ytick.minor.size': 4.0}

The list of keys that can be changed using this approach is defined in the _context_keys dictionary in the seaborn library.

Setting Color Palette

Finally, the third function, seaborn.set_palette(palette, n_colors=None, desat=None, color_codes=False), sets color palette for all the graphs in the notebook. Similarly to the functions described before, it also has an accompanaying function seaborn.color_palette(palette=None, n_colors=None, desat=None, as_cmap=False) that can be used to set a color palette for only one graph.

As the first argument, this function accepts seaborn or matplotlib predefined color palette names. The list of the predefined seaborn palette names is limited to the values 'colorblind', 'deep', 'muted', 'bright', 'pastel', and 'dark'. We can plot them using the following code. Figure 6 shows the result of this code execution.

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

palettes = ['colorblind', 'deep', 'muted', 'bright', 'pastel', 'dark']
n_palettes = len(palettes)

fig = plt.figure(layout='tight', figsize=(6.4, n_palettes))
for i, pal in enumerate(palettes, start=1):
    palette = sns.color_palette(pal)
    n = len(palette)
    ax = fig.add_subplot(n_palettes, 1, i)
    ax.imshow(np.arange(n).reshape(1, n),
              cmap=mpl.colors.ListedColormap(list(palette)),
              interpolation="nearest", aspect="equal")
    ax.set_xticks(np.arange(n) - .5)
    ax.set_yticks([-.5, .5])
    # Ensure nice border between colors
    ax.set_xticklabels(["" for _ in range(n)])
    # The proper way to set no ticks
    ax.yaxis.set_major_locator(ticker.NullLocator())
    ax.set_title(f'Palette: {pal}')

fig.show()
Seaborn Color Palettes
Seaborn Color Palettes

The list of the predefined matplotlib color palette names can be obtained from the colormap registry using the plt.colormaps() call:

print(plt.colormaps())
Output

['magma',
 'inferno',
 'plasma',
 'viridis',
 'cividis',
 'twilight',
 'twilight_shifted',
 'turbo',
 'Blues',
 'BrBG',
 'BuGn',
 'BuPu',
 'CMRmap',
 'GnBu',
 'Greens',
 'Greys',
 'OrRd',
 'Oranges',
 'PRGn',
 'PiYG',
 'PuBu',
 'PuBuGn',
 'PuOr',
 'PuRd',
 'Purples',
 'RdBu',
 'RdGy',
 'RdPu',
 'RdYlBu',
 'RdYlGn',
 'Reds',
 'Spectral',
 'Wistia',
 'YlGn',
 'YlGnBu',
 'YlOrBr',
 'YlOrRd',
 'afmhot',
 'autumn',
 'binary',
 'bone',
 'brg',
 'bwr',
 'cool',
 'coolwarm',
 'copper',
 'cubehelix',
 'flag',
 'gist_earth',
 'gist_gray',
 'gist_heat',
 'gist_ncar',
 'gist_rainbow',
 'gist_stern',
 'gist_yarg',
 'gnuplot',
 'gnuplot2',
 'gray',
 'hot',
 'hsv',
 'jet',
 'nipy_spectral',
 'ocean',
 'pink',
 'prism',
 'rainbow',
 'seismic',
 'spring',
 'summer',
 'terrain',
 'winter',
 'Accent',
 'Dark2',
 'Paired',
 'Pastel1',
 'Pastel2',
 'Set1',
 'Set2',
 'Set3',
 'tab10',
 'tab20',
 'tab20b',
 'tab20c',
 'magma_r',
 'inferno_r',
 'plasma_r',
 'viridis_r',
 'cividis_r',
 'twilight_r',
 'twilight_shifted_r',
 'turbo_r',
 'Blues_r',
 'BrBG_r',
 'BuGn_r',
 'BuPu_r',
 'CMRmap_r',
 'GnBu_r',
 'Greens_r',
 'Greys_r',
 'OrRd_r',
 'Oranges_r',
 'PRGn_r',
 'PiYG_r',
 'PuBu_r',
 'PuBuGn_r',
 'PuOr_r',
 'PuRd_r',
 'Purples_r',
 'RdBu_r',
 'RdGy_r',
 'RdPu_r',
 'RdYlBu_r',
 'RdYlGn_r',
 'Reds_r',
 'Spectral_r',
 'Wistia_r',
 'YlGn_r',
 'YlGnBu_r',
 'YlOrBr_r',
 'YlOrRd_r',
 'afmhot_r',
 'autumn_r',
 'binary_r',
 'bone_r',
 'brg_r',
 'bwr_r',
 'cool_r',
 'coolwarm_r',
 'copper_r',
 'cubehelix_r',
 'flag_r',
 'gist_earth_r',
 'gist_gray_r',
 'gist_heat_r',
 'gist_ncar_r',
 'gist_rainbow_r',
 'gist_stern_r',
 'gist_yarg_r',
 'gnuplot_r',
 'gnuplot2_r',
 'gray_r',
 'hot_r',
 'hsv_r',
 'jet_r',
 'nipy_spectral_r',
 'ocean_r',
 'pink_r',
 'prism_r',
 'rainbow_r',
 'seismic_r',
 'spring_r',
 'summer_r',
 'terrain_r',
 'winter_r',
 'Accent_r',
 'Dark2_r',
 'Paired_r',
 'Pastel1_r',
 'Pastel2_r',
 'Set1_r',
 'Set2_r',
 'Set3_r',
 'tab10_r',
 'tab20_r',
 'tab20b_r',
 'tab20c_r',
 'rocket',
 'rocket_r',
 'mako',
 'mako_r',
 'icefire',
 'icefire_r',
 'vlag',
 'vlag_r',
 'flare',
 'flare_r',
 'crest',
 'crest_r']

It is possible to plot them using the following code:

palettes = plt.colormaps()
n_palettes = len(palettes)

fig = plt.figure(layout='tight', figsize=(6.4, n_palettes))
for i, pal in enumerate(palettes, start=1):
    try:
        palette = sns.color_palette(pal)
        n = len(palette)
        ax = fig.add_subplot(n_palettes, 1, i)
        ax.imshow(np.arange(n).reshape(1, n),
                cmap=mpl.colors.ListedColormap(list(palette)),
                interpolation="nearest", aspect="equal")
        ax.set_xticks(np.arange(n) - .5)
        ax.set_yticks([-.5, .5])
        # Ensure nice border between colors
        ax.set_xticklabels(["" for _ in range(n)])
        # The proper way to set no ticks
        ax.yaxis.set_major_locator(ticker.NullLocator())
        ax.set_title(f'Palette: {pal}')
    except ValueError as e:
        print(f'Palette {pal} is not available')

fig.show()

Figure 7 (hidden behind the spoiler) shows the results of this code execution.

Matplotlib Color Palettes

Matplotlib Color Palettes
Matplotlib Color Palettes

Note that these palettes have a predefined number of colors: seaborn palettes currently specify 10 different colors, while majority of matplotlib colormaps have 6 distinct colors. With the seaborn.color_palette(palette=None, n_colors=None, desat=None, as_cmap=False) function, it is also possible to generate higher number of distinct colors that are evenly spaced in the “HUSL” (Figure 8) or “HLS” (Figure 9) system:

max_color_number = 15

fig = plt.figure(layout='tight', figsize=(6.4, max_color_number))
for i in range(1, max_color_number+1):
    palette = sns.color_palette(palette='husl', n_colors=i) # or substitute to 'hls' for HLS
    ax = fig.add_subplot(max_color_number, 1, i)
    ax.imshow(np.arange(i).reshape(1, i),
              cmap=mpl.colors.ListedColormap(list(palette)),
              interpolation="nearest", aspect="equal")
    ax.set_xticks(np.arange(i) - .5)
    ax.set_yticks([-.5, .5])
    # Ensure nice border between colors
    ax.set_xticklabels(["" for _ in range(i)])
    # The proper way to set no ticks
    ax.yaxis.set_major_locator(ticker.NullLocator())

fig.show()
Husl Color Palette

Husl Color Palette
Husl Color Palette

Hls Color Palette

Hls Color Palette
Hls Color Palette

More details about the color palettes can be found in the seaborn tutorial and this article

Mixing All Together

Instead of using the three functions that I have just described, it is possible to use only one seaborn.set_theme(context='notebook', style='darkgrid', palette='deep', font='sans-serif', font_scale=1, color_codes=True, rc=None):

sns.set_theme(context='paper', style='ticks', palette='colorblind')

Code

I have also created a notebook that shows the examples from this article. As usual, you can find it in the accompanying repository.

Related