#!/usr/bin/env python3 """ Functions for displaying colors and fonts. """ import os import re import cycler import matplotlib.colors as mcolors import matplotlib.font_manager as mfonts import numpy as np from . import colors as pcolors from . import constructor, ui from .config import _get_data_folders, rc from .internals import ic # noqa: F401 from .internals import _not_none, _version_mpl, docstring, warnings from .utils import to_rgb, to_xyz __all__ = [ "show_cmaps", "show_channels", "show_colors", "show_colorspaces", "show_cycles", "show_fonts", ] # Tables and constants FAMILY_TEXGYRE = ( "TeX Gyre Heros", # sans-serif "TeX Gyre Schola", # serif "TeX Gyre Bonum", "TeX Gyre Termes", "TeX Gyre Pagella", "TeX Gyre Chorus", # cursive "TeX Gyre Adventor", # fantasy "TeX Gyre Cursor", # monospace ) COLOR_TABLE = { # NOTE: Just want the names but point to the dictionaries because # they don't get filled until after __init__ imports this module. "base": mcolors.BASE_COLORS, "css4": mcolors.CSS4_COLORS, "opencolor": pcolors.COLORS_OPEN, "xkcd": pcolors.COLORS_XKCD, } CYCLE_TABLE = { "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", ), } CMAP_TABLE = { # 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": ("twilight",), "Seaborn sequential": ( "Rocket", "Flare", "Mako", "Crest", ), "Seaborn diverging": ( "IceFire", "Vlag", ), "UltraPlot sequential": ( "Fire", "Stellar", "Glacial", "Dusk", "Marine", "Boreal", "Sunrise", "Sunset", ), "UltraPlot diverging": ( "Div", "NegPos", "DryWet", ), "Other sequential": ("cubehelix", "turbo"), "Other diverging": ( "BR", "ColdHot", "CoolWarm", ), "cmOcean sequential": ( "Oxy", "Thermal", "Dense", "Ice", "Haline", "Deep", "Algae", "Tempo", "Speed", "Turbid", "Solar", "Matter", "Amp", ), "cmOcean diverging": ( "Balance", "Delta", "Curl", ), "cmOcean cyclic": ("Phase",), "Scientific colour maps sequential": ( "batlow", "batlowK", "batlowW", "devon", "davos", "oslo", "lapaz", "acton", "lajolla", "bilbao", "tokyo", "turku", "bamako", "nuuk", "hawaii", "buda", "imola", "oleron", "bukavu", "fes", ), "Scientific colour maps diverging": ( "roma", "broc", "cork", "vik", "bam", "lisbon", "tofino", "berlin", "vanimo", ), "Scientific colour maps cyclic": ( "romaO", "brocO", "corkO", "vikO", "bamO", ), "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": ( "Blues1", "Blues2", "Blues3", "Blues4", "Blues5", "Blues6", "Blues7", "Blues8", "Blues9", "Blues10", "Blues11", ), "SciVisColor greens": ( "Greens1", "Greens2", "Greens3", "Greens4", "Greens5", "Greens6", "Greens7", "Greens8", ), "SciVisColor yellows": ( "Yellows1", "Yellows2", "Yellows3", "Yellows4", ), "SciVisColor oranges": ( "Oranges1", "Oranges2", "Oranges3", "Oranges4", ), "SciVisColor browns": ( "Browns1", "Browns2", "Browns3", "Browns4", "Browns5", "Browns6", "Browns7", "Browns8", "Browns9", ), "SciVisColor reds": ( "Reds1", "Reds2", "Reds3", "Reds4", "Reds5", ), "SciVisColor purples": ( "Purples1", "Purples2", "Purples3", ), # 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 "Wistia", "CMRmap", # individually released "seismic", "terrain", "nipy_spectral", # origin ambiguous "tab10", "tab20", "tab20b", "tab20c", # merged colormap cycles ), } # Docstring snippets _colorbar_docstring = """ length : unit-spec, optional The length of each colorbar. %(units.in)s width : float or str, optional The width of each colorbar. %(units.in)s rasterized : bool, default: :rc:`colorbar.rasterized` Whether to rasterize the colorbar solids. This increases rendering time and decreases file sizes for vector graphics. """ docstring._snippet_manager["demos.cmaps"] = ", ".join(f"``{s!r}``" for s in CMAP_TABLE) docstring._snippet_manager["demos.cycles"] = ", ".join( f"``{s!r}``" for s in CYCLE_TABLE ) # noqa: E501 docstring._snippet_manager["demos.colors"] = ", ".join( f"``{s!r}``" for s in COLOR_TABLE ) # noqa: E501 docstring._snippet_manager["demos.colorbar"] = _colorbar_docstring [docs] def show_channels( *args, N=100, rgb=False, saturation=True, minhue=0, maxsat=500, width=100, refwidth=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 \ <https://matplotlib.org/stable/tutorials/colors/colormaps.html#lightness-of-matplotlib-colormaps>`__. Parameters ---------- *args : colormap-spec, default: :rc:`image.cmap` Positional arguments are colormap names or objects. 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. 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. refwidth : int or str, optional The width of each subplot. Passed to `~ultraplot.ui.subplots`. Returns ------- ultraplot.figure.Figure The figure. ultraplot.gridspec.SubplotGrid The subplot grid. See also -------- show_cmaps show_colorspaces """ # 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, refwidth=refwidth, wratios=(1.5, 1, 1, 1, 1, 1.5), share="labels", span=False, innerpad=1, ) # 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: cmap.name = 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 "HSL" in label: m = ms = max(min(max(ms, max(y)), maxsat), 100) elif "HPL" in label: m = mp = max(min(max(mp, max(y)), maxsat), 100) else: m = mc = max(min(max(mc, max(y)), maxsat), 100) ylim = (0, m) ylocator = ("maxn", 5) ax.scatter(x, y, c=x, cmap=cmap, s=width, linewidths=0) ax.format(title=label, ylim=ylim, ylocator=ylocator) # Formatting suptitle = ( ", ".join(repr(cmap.name) 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", label=cmap.name, labelweight="bold", ) return fig, axs [docs] def show_colorspaces(*, luminance=None, saturation=None, hue=None, refwidth=2): """ Generate hue-saturation, hue-luminance, and luminance-saturation cross-sections for the HCL, HSL, and HPL colorspaces. Parameters ---------- luminance : float, default: 50 If passed, saturation-hue cross-sections are drawn for this luminance. Must be between ``0`` and ``100``. 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``. refwidth : str or float, optional Average width of each subplot. Units are interpreted by `~ultraplot.utils.units`. Returns ------- ultraplot.figure.Figure The figure. ultraplot.gridspec.SubplotGrid The subplot grid. See also -------- show_cmaps show_channels """ # 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(refwidth=refwidth, ncols=3, share=False, innerpad=0.5) 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, :], 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 @warnings._rename_kwargs("0.8.0", categories="include") @warnings._rename_kwargs("0.10.0", rasterize="rasterized") def _draw_bars( cmaps, *, source, unknown="User", include=None, ignore=None, length=4.0, width=0.2, N=None, rasterized=None, ): """ Draw colorbars for "colormaps" and "color cycles". This is called by `show_cycles` and `show_cmaps`. """ # Categorize the input names table = {unknown: []} if unknown else {} table.update({cat: [None] * len(names) for cat, names in source.items()}) for name, cmap in cmaps.items(): cat = None name = name or "_no_name" name = name.lower() for opt, names in source.items(): names = list(map(str.lower, names)) if name in names: i, cat = names.index(name), opt if cat: table[cat][i] = cmap elif unknown: table[unknown].append(cmap) # Filter out certain categories options = set(map(str.lower, source)) if ignore is None: ignore = ("matlab", "gnuplot", "gist", "other") if isinstance(include, str): include = (include,) if isinstance(ignore, str): ignore = (ignore,) if include is None: include = options - set(map(str.lower, ignore)) else: include = set(map(str.lower, include)) if any(cat not in options and cat != unknown for cat in include): raise ValueError( f"Invalid categories {include!r}. Options are: " + ", ".join(map(repr, source)) + "." ) for cat in tuple(table): table[cat][:] = [cmap for cmap in table[cat] if cmap is not None] if not table[cat] or cat.lower() not in include and cat != unknown: del table[cat] # Draw figure # Allocate two colorbar widths for each title of sections naxs = 2 * len(table) + sum(map(len, table.values())) fig, axs = ui.subplots( refwidth=length, refheight=width, nrows=naxs, share=False, hspace="2pt", top="-1em", ) i = -1 nheads = nbars = 0 # for deciding which axes to plot in for cat, cmaps in table.items(): nheads += 1 for j, cmap in enumerate(cmaps): i += 1 if j + nheads + nbars > naxs: break if j == 0: # allocate this axes for title i += 2 for ax in axs[i - 2 : i]: ax.set_visible(False) ax = axs[i] if N is not None: cmap = cmap.copy(N=N) label = cmap.name label = re.sub(r"\A_*", "", label) label = re.sub(r"(_copy)*\Z", "", label) ax.colorbar( cmap, loc="fill", orientation="horizontal", locator="null", linewidth=0, rasterized=rasterized, ) ax.text( 0 - (rc["axes.labelpad"] / 72) / length, 0.45, label, ha="right", va="center", transform="axes", ) if j == 0: ax.set_title(cat, weight="bold") nbars += len(cmaps) return fig, axs [docs] @docstring._snippet_manager def show_cmaps(*args, **kwargs): """ Generate a table of the registered colormaps or the input colormaps categorized by source. Adapted from `this example \ <http://matplotlib.org/stable/gallery/color/colormap_reference.html>`__. Parameters ---------- *args : colormap-spec, optional Colormap names or objects. N : int, default: :rc:`image.lut` The number of levels in each colorbar. unknown : str, default: 'User' Category name for colormaps that are unknown to ultraplot. Set this to ``False`` to hide unknown colormaps. include : str or sequence of str, default: None Category names to be shown in the table. Use this to limit the table to a subset of categories. Valid categories are %(demos.cmaps)s. ignore : str or sequence of str, default: 'MATLAB', 'GNUplot', 'GIST', 'Other' Used only if `include` was not passed. Category names to be removed from the table. Use of the default ignored colormaps is discouraged because they contain non-uniform color transitions (see the :ref:`user guide <ug_perceptual>`). %(demos.colorbar)s Returns ------- ultraplot.figure.Figure The figure. ultraplot.gridspec.SubplotGrid The subplot grid. See also -------- show_colorspaces show_channels show_cycles show_colors show_fonts """ # Get the list of colormaps if args: cmaps = {} for cmap in args: name = cmap.name cmap = constructor.Colormap(arg) if isinstance(cmap, mcolors.LinearSegmentedColormap): cmaps[name] = cmap else: cmaps[name] = pcolors._get_cmap_subtype(cmap, "continuous") ignore = () else: cmaps = {} for key, cmap in pcolors._cmap_database.items(): if isinstance(cmap, pcolors.ContinuousColormap): if key.startswith("_"): continue cmaps[key] = cmap ignore = None # Return figure of colorbars kwargs.setdefault("source", CMAP_TABLE) kwargs.setdefault("ignore", ignore) return _draw_bars(cmaps, **kwargs) [docs] @docstring._snippet_manager def show_cycles(*args, **kwargs): """ Generate a table of registered color cycles or the input color cycles categorized by source. Adapted from `this example \ <http://matplotlib.org/stable/gallery/color/colormap_reference.html>`__. Parameters ---------- *args : colormap-spec, optional Cycle names or objects. unknown : str, default: 'User' Category name for cycles that are unknown to ultraplot. Set this to ``False`` to hide unknown colormaps. include : str or sequence of str, default: None Category names to be shown in the table. Use this to limit the table to a subset of categories. Valid categories are %(demos.cycles)s. ignore : str or sequence of str, default: None Used only if `include` was not passed. Category names to be removed from the table. %(demos.colorbar)s Returns ------- ultraplot.figure.Figure The figure. ultraplot.gridspec.SubplotGrid The subplot grid. See also -------- show_cmaps show_colors show_fonts """ # Get the list of cycles if args: cycles = {} for cmap in args: match cmap: case mcolors.ListedColormap(): color = cmap.by_key().get("color", ["k"]) name = getattr(cmap, "name", None) cmap = pcolors.DiscreteColormap(color, name=name) case mcolors.LinearSegmentedColormap(): cmap = cmap case _: cmap = pcolors._get_cmap_subtype(cmap, "discrete") cycles[cmap.name] = cmap ignore = () else: cycles = {} for name, cmap in pcolors._cmap_database.items(): if isinstance(cmap, pcolors.DiscreteColormap): if name.startswith("_"): continue cycles[name] = cmap ignore = None # Return figure of colorbars kwargs.setdefault("source", CYCLE_TABLE) kwargs.setdefault("ignore", ignore) return _draw_bars(cycles, **kwargs) def _filter_colors(hcl, ihue, nhues, minsat): """ Filter colors into categories. Parameters ---------- hcl : tuple 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._snippet_manager def show_colors(*, nhues=17, minsat=10, unknown="User", include=None, ignore=None): """ Generate tables of the registered color names. Adapted from `this example <https://matplotlib.org/examples/color/named_colors.html>`__. 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. unknown : str, default: 'User' Category name for color names that are unknown to ultraplot. Set this to ``False`` to hide unknown color names. include : str or sequence of str, default: None Category names to be shown in the table. Use this to limit the table to a subset of categories. Valid categories are %(demos.colors)s. ignore : str or sequence of str, default: 'CSS4' Used only if `include` was not passed. Category names to be removed from the colormap table. Returns ------- ultraplot.figure.Figure The figure. ultraplot.gridspec.SubplotGrid The subplot grid. """ # Tables of known colors to be plotted colordict = {} if ignore is None: ignore = "css4" if isinstance(include, str): include = (include.lower(),) if isinstance(ignore, str): ignore = (ignore.lower(),) if include is None: include = COLOR_TABLE.keys() include -= set(map(str.lower, ignore)) for cat in sorted(include): if cat not in COLOR_TABLE: raise ValueError( f"Invalid categories {include!r}. Options are: " + ", ".join(map(repr, COLOR_TABLE)) + "." ) colordict[cat] = list(COLOR_TABLE[cat]) # copy the names # Add "unknown" colors if unknown: unknown_colors = [ color for color in map(repr, pcolors._color_database) if "xkcd:" not in color and "tableau:" not in color and not any(color in list_ for list_ in COLOR_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 sorted(include): if cat == "base": names = np.asarray(colordict[cat]) ncols, nrows = len(names), 1 elif cat == "opencolor": names = np.asarray(colordict[cat]) ncols, nrows = 7, 20 else: hclpairs = [(name, to_xyz(name, "hcl")) for name in colordict[cat]] hclpairs = [ sorted( [ pair for pair in hclpairs if _filter_colors(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, nrows = 4, len(names) // 4 + 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 figwidth = 6.5 refaspect = (figwidth * 72) / (10 * shape[1]) # points maxcols = max(names.shape[0] for names in namess.values()) hratios = tuple(names.shape[1] for names in namess.values()) fig, axs = ui.subplots( figwidth=figwidth, refaspect=refaspect, nrows=len(include), hratios=hratios, ) title_dict = { "css4": "CSS4 colors", "base": "Base colors", "opencolor": "Open color", "xkcd": "XKCD colors", } for ax, (cat, names) in zip(axs, namess.items()): # Format axes ax.format( title=title_dict.get(cat, cat), titleweight="bold", xlim=(0, maxcols - 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 * (maxcols - 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] def show_fonts( *args, family=None, user=None, text=None, math=False, fallback=False, **kwargs ): """ Generate a table of fonts. If a glyph for a particular font is unavailable, it is replaced with the "¤" dummy character. Parameters ---------- *args : str or `~matplotlib.font_manager.FontProperties` The font specs, font names, or `~matplotlib.font_manager.FontProperties`\\ s to show. If no positional arguments are passed and the `family` argument is not passed, then the fonts found in :func:`~ultraplot.config.Configurator.user_folder` and `~ultraplot.config.Configurator.local_folders` and the *available* :rcraw:`font.sans-serif` fonts are shown. family \ : {'tex-gyre', 'sans-serif', 'serif', 'monospace', 'cursive', 'fantasy'}, optional The family from which *available* fonts are shown. Default is ``'sans-serif'`` if no arguments were provided. Otherwise the default is to not show family fonts. The fonts belonging to each family are listed under :rcraw:`font.serif`, :rcraw:`font.sans-serif`, :rcraw:`font.monospace`, :rcraw:`font.cursive`, and :rcraw:`font.fantasy`. The special family ``'tex-gyre'`` includes the `TeX Gyre <http://www.gust.org.pl/projects/e-foundry/tex-gyre>`__ fonts. user : bool, optional Whether to include fonts in :func:`~ultraplot.config.Configurator.user_folder` and `~ultraplot.config.Configurator.local_folders` at the top of the table. Default is ``True`` if called without any arguments and ``False`` otherwise. text : str, optional The sample text shown for each font. If not passed then default math or non-math sample text is used. math : bool, default: False Whether the default sample text should show non-math Latin characters or or math equations and Greek letters. fallback : bool, default: False Whether to use the fallback font :rcraw:`mathtext.fallback` for unavailable characters. If ``False`` the dummy glyph "¤" is shown for missing characters. **kwargs Additional font properties passed to `~matplotlib.font_manager.FontProperties`. Default size is ``12`` and default weight, style, and strength are ``'normal'``. Other parameters ---------------- size : float, default: 12 The font size. weight : str, default: 'normal' The font weight. style : str, default: 'normal' The font style. stretch : str, default: 'normal' The font stretch. Returns ------- ultraplot.figure.Figure The figure. ultraplot.gridspec.SubplotGrid The subplot grid. See also -------- show_cmaps show_cycles show_colors """ # Parse user input fonts and translate into FontProperties. s = set() props = [] # should be string names all_fonts = sorted(mfonts.fontManager.ttflist, key=lambda font: font.name) all_fonts = [ font for font in all_fonts if font.name not in s and not s.add(font.name) ] # noqa: E501 all_names = [font.name for font in all_fonts] for arg in args: if isinstance(arg, str): arg = mfonts.FontProperties(arg, **kwargs) # possibly a fontspec elif not isinstance(arg, mfonts.FontProperties): raise TypeError(f"Expected string or FontProperties but got {type(arg)}.") opts = arg.get_family() # usually a singleton list if opts and opts[0] in all_names: props.append(arg) else: warnings._warn_ultraplot( f"Input font name {opts[:1]!r} not found. Skipping." ) # Add user and family FontProperties. user = _not_none(user, not args and family is None) family = _not_none(family, None if args else "sans-serif") if user: paths = _get_data_folders("fonts", default=False) for font in all_fonts: # fonts sorted by unique name if os.path.dirname(font.fname) in paths: props.append(mfonts.FontProperties(font.name, **kwargs)) if family is not None: options = ("serif", "sans-serif", "monospace", "cursive", "fantasy", "tex-gyre") if family not in options: raise ValueError( f"Invalid font family {family!r}. Options are: " + ", ".join(map(repr, options)) + "." ) names = FAMILY_TEXGYRE if family == "tex-gyre" else rc["font." + family] for name in names: if name in all_names: # valid font name props.append(mfonts.FontProperties(name, **kwargs)) # The default sample text linespacing = 0.8 if text is None and math else 1.2 if text is None: if not math: text = ( "the quick brown fox jumps over a lazy dog 01234 ; . , + - * ^ () ||" "\n" "THE QUICK BROWN FOX JUMPS OVER A LAZY DOG 56789 : ! ? & # % $ [] {}" ) else: text = ( "\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$ " r"$\{ \; \}^i$ $[ \; ]_j$ $( \; )^k$ $\left< \right>_n$" "\n" r"$0^a + 1_b - 2^c \times 3_d = " r"4.0^e \equiv 5.0_f \approx 6.0^g \sim 7_h \leq 8^i \geq 9_j" r"\ll \prod \, P \gg \sum \, Q \, " r"\int \, Y \mathrm{d}y \propto \oint \;\, Z \mathrm{d}z$" ) # Settings for rendering math text ctx = {"mathtext.fontset": "custom"} if not fallback: if _version_mpl < "3.4": ctx["mathtext.fallback_to_cm"] = False else: ctx["mathtext.fallback"] = None if "size" not in kwargs: for prop in props: if prop.get_size() == rc["font.size"]: prop.set_size(12) # only if fontspec did not change the size # Create figure refsize = props[0].get_size_in_points() if props else rc["font.size"] refheight = 1.2 * (text.count("\n") + 2.5) * refsize / 72 fig, axs = ui.subplots( refwidth=4.5, refheight=refheight, nrows=len(props), ncols=1, space=0, ) fig._render_context.update(ctx) fig.format( xloc="neither", yloc="neither", xlocator="null", ylocator="null", alpha=0 ) for ax, prop in zip(axs, props): name = prop.get_family()[0] ax.text( 0, 0.5, f"{name}:\n{text} ", ha="left", va="center", linespacing=linespacing, fontproperties=prop, ) return fig, axs
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4