#!/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 = np.resize(names, (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