Source code for proplot.demos

#!/usr/bin/env python3
Functions for enumerating and contrasting the available colors and fonts.
import os
import numpy as np
import cycler
import matplotlib.font_manager as mfonts
import matplotlib.colors as mcolors
from . import ui
from . import constructor
from . import colors as pcolors
from .utils import to_rgb, to_xyz
from .config import rc, _get_data_paths, BASE_COLORS, XKCD_COLORS, OPEN_COLORS
from .internals import ic  # noqa: F401
from .internals import docstring, _not_none

__all__ = [

    'base': list(BASE_COLORS),
    'opencolor': list(OPEN_COLORS),
    'xkcd': list(XKCD_COLORS),
    'css4': list(mcolors.CSS4_COLORS),

    # Nice colormaps that get shown by default
    # NOTE: No longer rename colorbrewer greys map, just redirect 'grays'
    # to 'greys' in colormap database.
    'Grayscale': (  # assorted origin, but they belong together
        'Greys', 'Mono', 'MonoCycle',
    'Matplotlib sequential': (
        'viridis', 'plasma', 'inferno', 'magma', 'cividis',
    'Matplotlib cyclic': (
    'Seaborn sequential': (
        'Rocket', 'Mako',
    'Seaborn diverging': (
        'IceFire', 'Vlag',
    'ProPlot sequential': (
    'ProPlot diverging': (
        'Div', 'NegPos', 'DryWet',
    'Other diverging': (
        'ColdHot', 'CoolWarm', 'BR',
    'cmOcean sequential': (
        'Oxy', 'Thermal', 'Dense', 'Ice', 'Haline',
        'Deep', 'Algae', 'Tempo', 'Speed', 'Turbid', 'Solar', 'Matter',
    'cmOcean diverging': (
        'Balance', 'Delta', 'Curl',
    'cmOcean cyclic': (
    'Scientific colour maps sequential': (
        'batlow', 'oleron',
        'devon', 'davos', 'oslo', 'lapaz', 'acton',
        'lajolla', 'bilbao', 'tokyo', 'turku', 'bamako', 'nuuk',
        'hawaii', 'buda', 'imola',
    'Scientific colour maps diverging': (
        'roma', 'broc', 'cork', 'vik', 'berlin', 'lisbon', 'tofino',
    'Scientific colour maps cyclic': (
        'romaO', 'brocO', 'corkO', 'vikO',
    'ColorBrewer2.0 sequential': (
        'Purples', 'Blues', 'Greens', 'Oranges', 'Reds',
        'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu',
        'PuBu', 'PuBuGn', 'BuGn', 'GnBu', 'YlGnBu', 'YlGn'
    'ColorBrewer2.0 diverging': (
        'Spectral', 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGY',
        'RdBu', 'RdYlBu', 'RdYlGn',
    'SciVisColor blues': (
        'Blue1', 'Blue2', 'Blue3', 'Blue4', 'Blue5',
        'Blue6', 'Blue7', 'Blue8', 'Blue9', 'Blue10', 'Blue11',
    'SciVisColor greens': (
        'Green1', 'Green2', 'Green3', 'Green4', 'Green5',
        'Green6', 'Green7', 'Green8',
    'SciVisColor oranges': (
        'Orange1', 'Orange2', 'Orange3', 'Orange4', 'Orange5',
        'Orange6', 'Orange7', 'Orange8',
    'SciVisColor browns': (
        'Brown1', 'Brown2', 'Brown3', 'Brown4', 'Brown5',
        'Brown6', 'Brown7', 'Brown8', 'Brown9',
    'SciVisColor reds and purples': (
        'RedPurple1', 'RedPurple2', 'RedPurple3', 'RedPurple4',
        'RedPurple5', 'RedPurple6', 'RedPurple7', 'RedPurple8',
    # Builtin colormaps that re hidden by default. Some are really bad, some
    # are segmented maps that should be cycles, and some are just uninspiring.
    'MATLAB': (
        'bone', 'cool', 'copper', 'autumn', 'flag', 'prism',
        'jet', 'hsv', 'hot', 'spring', 'summer', 'winter', 'pink', 'gray',
    'GNUplot': (
        'gnuplot', 'gnuplot2', 'ocean', 'afmhot', 'rainbow',
    'GIST': (
        'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar',
        'gist_rainbow', 'gist_stern', 'gist_yarg',
    'Other': (
        'binary', 'bwr', 'brg',  # appear to be custom matplotlib
        'cubehelix', 'Wistia', 'CMRmap',  # individually released
        'seismic', 'terrain', 'nipy_spectral',  # origin ambiguous
        'tab10', 'tab20', 'tab20b', 'tab20c',  # merged colormap cycles

    'Matplotlib defaults': (
        'default', 'classic',
    'Matplotlib stylesheets': (
        # NOTE: Do not include 'solarized' because colors are terrible for
        # colorblind folks.
        'colorblind', 'colorblind10', 'tableau', 'ggplot', '538', 'seaborn', 'bmh',
    'ColorBrewer2.0 qualitative': (
        'Accent', 'Dark2',
        'Paired', 'Pastel1', 'Pastel2',
        'Set1', 'Set2', 'Set3',
        'tab10', 'tab20', 'tab20b', 'tab20c',
    'Other qualitative': (
        'FlatUI', 'Qual1', 'Qual2',

docstring.snippets['show.colorbars'] = """
length : float or str, optional
    The length of the colorbars. Units are interpreted by
width : float or str, optional
    The width of the colorbars. Units are interpreted by
docstring.snippets['color.categories'] = ', '.join(
    f'``{cat!r}``' for cat in COLORS_TABLE
docstring.snippets['cmap.categories'] = ', '.join(
    f'``{cat!r}``' for cat in CMAPS_TABLE
docstring.snippets['cycle.categories'] = ', '.join(
    f'``{cat!r}``' for cat in CYCLES_TABLE

def _draw_bars(
    cmaps, *, source, unknown='User', categories=None,
    length=4.0, width=0.2, N=None
    Draw colorbars for "colormaps" and "color cycles". This is called by
    `show_cycles` and `show_cmaps`.
    # Translate local names into database of colormaps
    # NOTE: Custom cmap database raises nice error if colormap name is unknown
    i = 1
    database = pcolors.ColormapDatabase({})  # subset to be drawn
    for cmap in cmaps:
        if isinstance(cmap, cycler.Cycler):
            name = getattr(cmap, 'name', '_no_name')
            cmap = mcolors.ListedColormap(cmap.by_key()['color'], name)
        elif isinstance(cmap, (list, tuple)):
            name = '_no_name'
            cmap = mcolors.ListedColormap(cmap, name)
        elif isinstance(cmap, mcolors.Colormap):
            name =
            name = cmap
            cmap = pcolors._cmap_database[cmap]
        if name in database:
            name = f'{name}_{i}'  # e.g. _no_name_2
            i += 1
        database[name] = cmap

    # Categorize the input names
    cmapdict = {}
    names_all = list(map(str.lower, database.keys()))
    names_known = list(map(str.lower, sum(map(list, source.values()), [])))
    names_unknown = [name for name in names_all if name not in names_known]
    if unknown and names_unknown:
        cmapdict[unknown] = names_unknown
    for cat, names in source.items():
        names_cat = [name for name in names if name.lower() in names_all]
        if names_cat:
            cmapdict[cat] = names_cat

    # Filter out certain categories
    if categories is None:
        categories = source.keys() - {'MATLAB', 'GNUplot', 'GIST', 'Other'}
    if any(cat not in source for cat in categories):
        raise ValueError(
            f'Invalid categories {categories!r}. Options are: '
            + ', '.join(map(repr, source)) + '.'
    for cat in (*cmapdict,):
        if cat not in categories and cat != unknown:

    # Draw figure
    naxs = len(cmapdict) + sum(map(len, cmapdict.values()))
    fig, axs = ui.subplots(
        nrows=naxs, axwidth=length, axheight=width,
        share=0, hspace=0.03,
    iax = -1
    nheads = nbars = 0  # for deciding which axes to plot in
    for cat, names in cmapdict.items():
        nheads += 1
        for imap, name in enumerate(names):
            iax += 1
            if imap + nheads + nbars > naxs:
            ax = axs[iax]
            if imap == 0:  # allocate this axes for title
                iax += 1
                ax = axs[iax]
            cmap = database[name]
            if N is not None:
                cmap = cmap.copy(N=N)
                cmap, loc='fill',
                orientation='horizontal', locator='null', linewidth=0
                0 - (rc['axes.labelpad'] / 72) / length, 0.45, name,
                ha='right', va='center', transform='axes',
            if imap == 0:
                ax.set_title(cat, weight='bold')
        nbars += len(names)
    return fig, axs

[docs]def show_channels( *args, N=100, saturation=True, rgb=False, minhue=0, maxsat=500, width=100, axwidth=1.7 ): """ Show how arbitrary colormap(s) vary with respect to the hue, chroma, luminance, HSL saturation, and HPL saturation channels, and optionally the red, blue and green channels. Adapted from `this example \ <>`__. Parameters ---------- *args : colormap-spec, optional Positional arguments are colormap names or objects. Default is :rc:`image.cmap`. N : int, optional The number of markers to draw for each colormap. rgb : bool, optional Whether to also show the red, green, and blue channels in the bottom row. Default is ``True``. saturation : bool, optional Whether to show the HSL and HPL saturation channels alongside the raw chroma. minhue : float, optional The minimum hue. This lets you rotate the hue plot cyclically. maxsat : float, optional The maximum saturation. Use this to truncate large saturation values. width : int, optional The width of each colormap line in points. axwidth : int or str, optional The width of each subplot. Passed to `~proplot.ui.subplots`. Returns ------- `~proplot.figure.Figure` The figure. """ # Figure and plot if not args: raise ValueError('At least one positional argument required.') array = [[1, 1, 2, 2, 3, 3]] labels = ('Hue', 'Chroma', 'Luminance') if saturation: array += [[0, 4, 4, 5, 5, 0]] labels += ('HSL saturation', 'HPL saturation') if rgb: array += [np.array([4, 4, 5, 5, 6, 6]) + 2 * int(saturation)] labels += ('Red', 'Green', 'Blue') fig, axs = ui.subplots( array=array, span=False, share=1, axwidth=axwidth, axpad='1em', ) # Iterate through colormaps mc = ms = mp = 0 cmaps = [] for cmap in args: # Get colormap and avoid registering new names name = cmap if isinstance(cmap, str) else getattr(cmap, 'name', None) cmap = constructor.Colormap(cmap, N=N) # arbitrary cmap argument if name is not None: = name cmap._init() cmaps.append(cmap) # Get clipped RGB table x = np.linspace(0, 1, N) lut = cmap._lut[:-3, :3].copy() rgb_data = lut.T # 3 by N hcl_data = np.array([to_xyz(color, space='hcl') for color in lut]).T # 3 by N hsl_data = [to_xyz(color, space='hsl')[1] for color in lut] hpl_data = [to_xyz(color, space='hpl')[1] for color in lut] # Plot channels # If rgb is False, the zip will just truncate the other iterables data = tuple(hcl_data) if saturation: data += (hsl_data, hpl_data) if rgb: data += tuple(rgb_data) for ax, y, label in zip(axs, data, labels): ylim, ylocator = None, None if label in ('Red', 'Green', 'Blue'): ylim = (0, 1) ylocator = 0.2 elif label == 'Luminance': ylim = (0, 100) ylocator = 20 elif label == 'Hue': ylim = (minhue, minhue + 360) ylocator = 90 y = y - 720 for _ in range(3): # rotate up to 1080 degrees y[y < minhue] += 360 else: if label == 'Chroma': mc = max(min(max(mc, max(y)), maxsat), 100) m = mc elif 'HSL' in label: ms = max(min(max(ms, max(y)), maxsat), 100) m = ms else: mp = max(min(max(mp, max(y)), maxsat), 100) m = mp ylim = (0, m) ylocator = ('maxn', 5) ax.scatter(x, y, c=x, cmap=cmap, N=len(x), s=width, linewidths=0) ax.format(title=label, ylim=ylim, ylocator=ylocator) # Formatting suptitle = ( ', '.join(repr( for cmap in cmaps[:-1]) + (', and ' if len(cmaps) > 2 else ' and ' if len(cmaps) == 2 else ' ') + f'{repr(cmaps[-1].name)} colormap' + ('s' if len(cmaps) > 1 else '') ) axs.format( xlocator=0.25, xformatter='null', suptitle=f'{suptitle} by channel', ylim=None, ytickminor=False, ) # Colorbar on the bottom for cmap in cmaps: fig.colorbar( cmap, loc='b', span=(2, 5), locator='null',, labelweight='bold' ) return fig, axs
[docs]def show_colorspaces(*, luminance=None, saturation=None, hue=None, axwidth=2): """ Generate hue-saturation, hue-luminance, and luminance-saturation cross-sections for the HCL, HSL, and HPL colorspaces. Parameters ---------- luminance : float, optional If passed, saturation-hue cross-sections are drawn for this luminance. Must be between ``0`` and ``100``. Default is ``50``. saturation : float, optional If passed, luminance-hue cross-sections are drawn for this saturation. Must be between ``0`` and ``100``. hue : float, optional If passed, luminance-saturation cross-sections are drawn for this hue. Must be between ``0`` and ``360``. axwidth : str or float, optional Average width of each subplot. Units are interpreted by `~proplot.utils.units`. Returns ------- `~proplot.figure.Figure` The figure. """ # Get colorspace properties hues = np.linspace(0, 360, 361) sats = np.linspace(0, 120, 120) lums = np.linspace(0, 99.99, 101) if luminance is None and saturation is None and hue is None: luminance = 50 _not_none(luminance=luminance, saturation=saturation, hue=hue) # warning if luminance is not None: hsl = np.concatenate(( np.repeat(hues[:, None], len(sats), axis=1)[..., None], np.repeat(sats[None, :], len(hues), axis=0)[..., None], np.ones((len(hues), len(sats)))[..., None] * luminance, ), axis=2) suptitle = f'Hue-saturation cross-section for luminance {luminance}' xlabel, ylabel = 'hue', 'saturation' xloc, yloc = 60, 20 elif saturation is not None: hsl = np.concatenate(( np.repeat(hues[:, None], len(lums), axis=1)[..., None], np.ones((len(hues), len(lums)))[..., None] * saturation, np.repeat(lums[None, :], len(hues), axis=0)[..., None], ), axis=2) suptitle = f'Hue-luminance cross-section for saturation {saturation}' xlabel, ylabel = 'hue', 'luminance' xloc, yloc = 60, 20 elif hue is not None: hsl = np.concatenate(( np.ones((len(lums), len(sats)))[..., None] * hue, np.repeat(sats[None, :], len(lums), axis=0)[..., None], np.repeat(lums[:, None], len(sats), axis=1)[..., None], ), axis=2) suptitle = 'Luminance-saturation cross-section' xlabel, ylabel = 'luminance', 'saturation' xloc, yloc = 20, 20 # Make figure, with black indicating invalid values # Note we invert the x-y ordering for imshow fig, axs = ui.subplots( ncols=3, share=0, axwidth=axwidth, aspect=1, axpad=0.05 ) for ax, space in zip(axs, ('hcl', 'hsl', 'hpl')): rgba = np.ones((*hsl.shape[:2][::-1], 4)) # RGBA for j in range(hsl.shape[0]): for k in range(hsl.shape[1]): rgb_jk = to_rgb(hsl[j, k, :].flat, space) if not all(0 <= c <= 1 for c in rgb_jk): rgba[k, j, 3] = 0 # black cell else: rgba[k, j, :3] = rgb_jk ax.imshow(rgba, origin='lower', aspect='auto') ax.format( xlabel=xlabel, ylabel=ylabel, suptitle=suptitle, grid=False, xtickminor=False, ytickminor=False, xlocator=xloc, ylocator=yloc, facecolor='k', title=space.upper(), ) return fig, axs
def _color_filter(hcl, ihue, nhues, minsat): """ Filter colors into categories. Parameters ---------- hcl : (name, hcl) The data. ihue : int The hue column. nhues : int The total number of hues. minsat : float The minimum saturation used for the "grays" column. """ breakpoints = np.linspace(0, 360, nhues) gray = hcl[1] <= minsat if ihue == 0: return gray color = breakpoints[ihue - 1] <= hcl[0] < breakpoints[ihue] if ihue == nhues - 1: color = color or color == breakpoints[ihue] # endpoint inclusive return not gray and color
[docs]@docstring.add_snippets def show_colors(*, nhues=17, minsat=10, categories=None, unknown='User'): """ Generate tables of the registered color names. Adapted from `this example <>`__. Parameters ---------- nhues : int, optional The number of breaks between hues for grouping "like colors" in the color table. minsat : float, optional The threshold saturation, between ``0`` and ``100``, for designating "gray colors" in the color table. categories : list of str, optional Category names to be shown in the table. By default, every category is shown except for CSS colors. Valid categories are %(color.categories)s. unknown : str, optional Category name for color names that are unknown to ProPlot. The default is ``'User'``. Set this to ``False`` to hide unknown color names. Returns ------- fig : `~proplot.figure.Figure` The figure. """ # Tables of known colors to be plotted colordict = {} if isinstance(categories, str): categories = [categories] if categories is None: categories = ['base', 'opencolor', 'xkcd'] # preserve order for cat in categories: if cat not in COLORS_TABLE: raise ValueError( f'Invalid categories {categories!r}. Options are: ' + ', '.join(map(repr, COLORS_TABLE)) + '.' ) colordict[cat] = COLORS_TABLE[cat] # Add "unknown" colors if unknown: unknown_colors = [ color for color in map(repr, mcolors.colorConverter.colors) if 'xkcd:' not in color and 'tableau:' not in color and not any(color in list_ for list_ in COLORS_TABLE) ] if unknown_colors: colordict[unknown] = unknown_colors # Divide colors into columns and rows # For base and open colors, tables are already organized into like # colors, so just reshape them into grids. For other colors, we group # them by hue in descending order of luminance. namess = {} for cat in categories: if cat == 'base': names = np.asarray(colordict[cat]).reshape((2, 8)).T elif cat == 'opencolor': names = np.asarray(colordict[cat]) names.resize((7, 20)) else: hclpairs = [(name, to_xyz(name, 'hcl')) for name in colordict[cat]] hclpairs = [ sorted( [ pair for pair in hclpairs if _color_filter(pair[1], ihue, nhues, minsat) ], key=lambda x: x[1][2] # sort by luminance ) for ihue in range(nhues) ] names = np.array([ name for ipairs in hclpairs for name, _ in ipairs ]) ncols = 4 nrows = len(names) // ncols + 1 names.resize((ncols, nrows)) # fill empty slots with empty string namess[cat] = names # Draw figures for different groups of colors # NOTE: Aspect ratios should be number of columns divided by number # of rows, times the aspect ratio of the slot for each swatch-name # pair, which we set to 5. shape = tuple(namess.values())[0].shape # sample *first* group width = 6.5 margin = 0.1 ncols_max = max(names.shape[0] for names in namess.values()) aspect_axes = (width * 72) / (10 * shape[1]) # points height_ratios = tuple(names.shape[1] for names in namess.values()) fig, axs = ui.subplots( nrows=len(categories), width=width, aspect=aspect_axes, height_ratios=height_ratios, left=margin, right=margin, bottom=margin, ) title_dict = { 'css4': 'CSS4 colors', 'base': 'Base colors', 'opencolor': 'Open color', 'xkcd': 'XKCD color survey', } for ax, (cat, names) in zip(axs, namess.items()): # Format axes ax.format( title=title_dict.get(cat, cat), titleweight='bold', xlim=(0, ncols_max - 1), ylim=(0, names.shape[1]), grid=False, yloc='neither', xloc='neither', alpha=0, ) # Draw swatches as lines lw = 8 # best to just use trial and error swatch = 0.45 # percent of column reserved for swatch ncols, nrows = names.shape for col, inames in enumerate(names): for row, name in enumerate(inames): if not name: continue y = nrows - row - 1 # start at top x1 = col * (ncols_max - 1) / ncols # e.g. idx 3 --> idx 7 x2 = x1 + swatch # portion of column xtext = x1 + 1.1 * swatch ax.text( xtext, y, name, ha='left', va='center', transform='data', clip_on=False, ) ax.plot( [x1, x2], [y, y], color=name, lw=lw, solid_capstyle='butt', # do not stick out clip_on=False, ) return fig, axs
[docs]@docstring.add_snippets def show_cmaps(*args, **kwargs): """ Generate a table of the registered colormaps or the input colormaps categorized by source. Adapted from `this example \ <>`__. Parameters ---------- *args : colormap-spec, optional Colormap names or objects. N : int, optional The number of levels in each colorbar. Default is :rc:`image.lut`. unknown : str, optional Category name for colormaps that are unknown to ProPlot. The default is ``'User'``. Set this to ``False`` to hide unknown colormaps. categories : list of str, optional Category names to be shown in the table. By default, all categories are shown except for ``'MATLAB'``, ``'GNUplot'``, ``'GIST'``, and ``'Other'``. Use of these colormaps is discouraged, because they contain a variety of non-uniform colormaps (see :ref:`perceptually uniform colormaps <ug_perceptual>` for details). Valid categories are %(cmap.categories)s. %(show.colorbars)s Returns ------- `~proplot.figure.Figure` The figure. """ # Get the list of colormaps if args: cmaps = [constructor.Colormap(cmap) for cmap in args] else: cmaps = [ name for name in pcolors._cmap_database.keys() if isinstance(pcolors._cmap_database[name], pcolors.LinearSegmentedColormap) ] # Return figure of colorbars kwargs.setdefault('source', CMAPS_TABLE) return _draw_bars(cmaps, **kwargs)
[docs]@docstring.add_snippets def show_cycles(*args, **kwargs): """ Generate a table of registered color cycles or the input color cycles categorized by source. Adapted from `this example \ <>`__. Parameters ---------- *args : colormap-spec, optional Cycle names or objects. unknown : str, optional Category name for cycles that are unknown to ProPlot. The default is ``'User'``. Set this to ``False`` to hide unknown colormaps. categories : list of str, optional Category names to be shown in the table. By default, all categories are shown. Valid categories are %(cycle.categories)s. %(show.colorbars)s Returns ------- `~proplot.figure.Figure` The figure. """ # Get the list of cycles if args: cycles = [constructor.Cycle(cmap, to_listed=True) for cmap in args] else: cycles = [ name for name in pcolors._cmap_database.keys() if isinstance(pcolors._cmap_database[name], pcolors.ListedColormap) ] # Return figure of colorbars kwargs.setdefault('source', CYCLES_TABLE) return _draw_bars(cycles, **kwargs)
[docs]def show_fonts( *args, family=None, text=None, size=12, weight='normal', style='normal', stretch='normal', ): """ Generate a table of fonts. If a glyph for a particular font is unavailable, it is replaced with the "ยค" dummy character. Parameters ---------- *args The font name(s). If none are provided and the `family` keyword argument was not provided, the *available* :rcraw:`font.sans-serif` fonts and the fonts in your ``.proplot/fonts`` folder are shown. family : {'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', \ 'tex-gyre'}, optional If provided, the *available* fonts in the corresponding families are shown. The fonts belonging to these families are listed under the :rcraw:`font.serif`, :rcraw:`font.sans-serif`, :rcraw:`font.monospace`, :rcraw:`font.cursive`, and :rcraw:`font.fantasy` settings. The special family ``'tex-gyre'`` draws the `TeX Gyre \ <>`__ fonts. text : str, optional The sample text. The default sample text includes the Latin letters, Greek letters, Arabic numerals, and some simple mathematical symbols. size : float, optional The font size in points. weight : weight-spec, optional The font weight. style : style-spec, optional The font style. stretch : stretch-spec, optional The font stretch. """ if not args and family is None: # User fonts and sans-serif fonts. Note all proplot sans-serif fonts # are added to 'font.sans-serif' by default args = sorted({ for font in mfonts.fontManager.ttflist if in rc['font.sans-serif'] or _get_data_paths('fonts')[1] == os.path.dirname(font.fname) }) elif family is not None: options = ( 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'tex-gyre', ) if family not in options: raise ValueError( f'Invalid family {family!r}. Options are: ' + ', '.join(map(repr, options)) + '.' ) if family == 'tex-gyre': family_fonts = ( 'TeX Gyre Adventor', 'TeX Gyre Bonum', 'TeX Gyre Cursor', 'TeX Gyre Chorus', 'TeX Gyre Heros', 'TeX Gyre Pagella', 'TeX Gyre Schola', 'TeX Gyre Termes', ) else: family_fonts = rc['font.' + family] args = ( *args, *sorted({ for font in mfonts.fontManager.ttflist if in family_fonts }) ) # Text if text is None: text = ( 'the quick brown fox jumps over a lazy dog' '\n' 'THE QUICK BROWN FOX JUMPS OVER A LAZY DOG' '\n' '(0) + {1\N{DEGREE SIGN}} \N{MINUS SIGN} [2*] - <3> / 4,0 ' r'$\geq\gg$ 5.0 $\leq\ll$ ~6 $\times$ 7 ' r'$\equiv$ 8 $\approx$ 9 $\propto$' '\n' r'$\alpha\beta$ $\Gamma\gamma$ $\Delta\delta$ ' r'$\epsilon\zeta\eta$ $\Theta\theta$ $\kappa\mu\nu$ ' r'$\Lambda\lambda$ $\Pi\pi$ $\xi\rho\tau\chi$ $\Sigma\sigma$ ' r'$\Phi\phi$ $\Psi\psi$ $\Omega\omega$ !?&#%' ) # Create figure fig, axs = ui.subplots( ncols=1, nrows=len(args), space=0, axwidth=4.5, axheight=1.2 * (text.count('\n') + 2.5) * size / 72, fallback_to_cm=False ) axs.format( xloc='neither', yloc='neither', xlocator='null', ylocator='null', alpha=0 ) for i, ax in enumerate(axs): font = args[i] ax.text( 0, 0.5, f'{font}:\n{text}', fontfamily=font, fontsize=size, stretch=stretch, style=style, weight=weight, ha='left', va='center' ) return fig, axs