Source code for plotastro._extras

"""Markers, line styles, property cyclers, panel labels, reference charts."""

from __future__ import annotations

import functools
import operator
import string

import matplotlib.pyplot as plt
from cycler import cycler

from ._colors import COLORS, CYCLE
from ._core import figsize

#: A marker sequence that stays distinguishable at small sizes.
MARKERS = ["o", "s", "^", "D", "v", "p", "*", "X"]

#: Named dash patterns. The tuples are matplotlib (offset, (on, off, ...))
#: dash specs — they scale with the line width and print cleanly.
LINESTYLES = {
    "solid":            "-",
    "dashed":           (0, (5, 2)),
    "dotted":           (0, (1, 1.5)),
    "dashdot":          (0, (5, 2, 1, 2)),
    "long dash":        (0, (9, 3)),
    "dash dot dot":     (0, (5, 2, 1, 2, 1, 2)),
    "densely dotted":   (0, (1, 0.8)),
    "loosely dashed":   (0, (5, 6)),
}


[docs] def style_cycler(n=None, *, colors=True, markers=False, linestyles=False): """Build a property cycler that pairs colours with markers and/or dash patterns, so lines stay distinguishable in greyscale and for colour-blind readers (redundant encoding). Parameters ---------- n : int, optional Number of entries (default: length of the colour cycle, or 8 when markers/linestyles are included). colors, markers, linestyles : bool Which properties to cycle together. Returns ------- cycler.Cycler : for ``ax.set_prop_cycle`` or ``plt.rc("axes", prop_cycle=...)``. The colours are always the default cycle, :data:`COLORS`. Examples -------- >>> ax.set_prop_cycle(pa.style_cycler(markers=True)) >>> ax.set_prop_cycle(pa.style_cycler(linestyles=True)) >>> plt.rc("axes", prop_cycle=pa.style_cycler(markers=True)) # globally """ if n is None: n = len(CYCLE) if not (markers or linestyles) else min(len(CYCLE), 8) parts = [] if colors: parts.append(cycler(color=(CYCLE * 3)[:n])) if markers: parts.append(cycler(marker=(MARKERS * 3)[:n])) if linestyles: parts.append(cycler(linestyle=(list(LINESTYLES.values()) * 3)[:n])) if not parts: raise ValueError("Enable at least one of colors/markers/linestyles.") return functools.reduce(operator.add, parts)
# ---------------------------------------------------------------------- # Panel labels # ---------------------------------------------------------------------- _PANEL_POSITIONS = { # loc: (x, y, ha, va) in axes coordinates "upper left": (0.05, 0.95, "left", "top"), "upper right": (0.95, 0.95, "right", "top"), "lower left": (0.05, 0.05, "left", "bottom"), "lower right": (0.95, 0.05, "right", "bottom"), "outside": (0.0, 1.02, "left", "bottom"), }
[docs] def label_panels(axes, fmt="({})", loc="upper left", uppercase=False, labels=None, **text_kw): """Stamp (a), (b), (c), ... on a grid of subplots, the way most journals want multi-panel figures labelled. Parameters ---------- axes : Axes, sequence of Axes, or array from plt.subplots Labelled in the order given (arrays are flattened row-major). fmt : str, optional Applied to each letter; e.g. ``"({})"`` -> "(a)", ``"{}."`` -> "a.". loc : str, optional One of "upper left", "upper right", "lower left", "lower right", or "outside" (above the top-left corner — the Nature convention, usually combined with ``fmt="{}"`` and bold text). uppercase : bool, optional Use A, B, C instead of a, b, c. labels : sequence of str, optional Explicit labels, overriding the alphabet. **text_kw Forwarded to ``ax.text`` (e.g. ``fontweight="bold"``). Returns ------- list of the Text objects created. Examples -------- >>> fig, axes = pa.subplots(2, 2, width="full") >>> pa.label_panels(axes) >>> pa.label_panels(axes, loc="outside", fmt="{}", fontweight="bold") """ if hasattr(axes, "flat"): # numpy array from plt.subplots axes = list(axes.flat) elif not hasattr(axes, "__iter__"): axes = [axes] if loc not in _PANEL_POSITIONS: options = ", ".join(_PANEL_POSITIONS) raise ValueError(f"loc must be one of: {options}; got {loc!r}") x, y, ha, va = _PANEL_POSITIONS[loc] letters = string.ascii_uppercase if uppercase else string.ascii_lowercase if labels is None: labels = [fmt.format(letters[i]) for i in range(len(axes))] texts = [] for ax, label in zip(axes, labels): texts.append(ax.text(x, y, label, transform=ax.transAxes, ha=ha, va=va, **text_kw)) return texts
# ---------------------------------------------------------------------- # Reference charts (used in the tutorial and README) # ----------------------------------------------------------------------
[docs] def show_colors(palette=None, title="Default colour-blind-friendly cycle"): """Swatch chart of a palette: each colour with its cycle index (``C0``, ``C1``, ...), name and hex code. Parameters ---------- palette : dict, optional ``{name: colour}``, e.g. :data:`OKABE_ITO`. Default: the colour cycle, :data:`COLORS`. title : str, optional Figure title. Returns ------- Figure Examples -------- >>> pa.show_colors(pa.PETROFF10, title="Petroff (2021)") """ palette = palette if palette is not None else COLORS names = list(palette) fig, ax = plt.subplots( figsize=figsize("full", journal="mnras", fraction=0.9, height=0.32 * len(names) + 0.5)) ax.set_axis_off() ax.set_title(title) for i, name in enumerate(names): y = len(names) - 1 - i ax.add_patch(plt.Rectangle((0, y + 0.1), 1.6, 0.8, color=palette[name], ec="0.2", lw=0.4)) ax.text(1.75, y + 0.5, f"C{i} · {name} · {str(palette[name]).upper()}", va="center", ha="left", fontsize=8, family="monospace") ax.set_xlim(0, 6) ax.set_ylim(0, len(names)) return fig
[docs] def show_markers(): """Reference chart of :data:`MARKERS` (first row) and eight more matplotlib markers, each with the code that draws it. Returns ------- Figure """ extra = ["<", ">", "h", "8", "P", "d", "x", "+"] all_markers = MARKERS + extra fig, ax = plt.subplots( figsize=figsize("full", journal="mnras", fraction=0.9, height=1.6)) ax.set_axis_off() ax.set_title("Markers — first row is plotastro.MARKERS") for i, m in enumerate(all_markers): row, col = divmod(i, 8) y = 1.4 - row ax.plot(col, y, marker=m, ms=7, color=CYCLE[col % len(CYCLE)], ls="none", clip_on=False) ax.text(col, y - 0.42, repr(m), ha="center", va="top", fontsize=8, family="monospace") ax.set_xlim(-0.5, 7.5) ax.set_ylim(-0.7, 2.0) return fig
[docs] def show_linestyles(): """Reference chart of the named dash patterns in :data:`LINESTYLES`. Returns ------- Figure """ fig, ax = plt.subplots( figsize=figsize("full", journal="mnras", fraction=0.9, height=0.34 * len(LINESTYLES) + 0.5)) ax.set_axis_off() ax.set_title("Named line styles — plotastro.LINESTYLES") for i, (name, ls) in enumerate(LINESTYLES.items()): y = len(LINESTYLES) - 1 - i ax.plot([0.35, 1.0], [y, y], ls=ls, lw=1.4, color=CYCLE[i % len(CYCLE)]) ax.text(0.32, y, name, ha="right", va="center", fontsize=8) ax.set_xlim(0, 1.02) ax.set_ylim(-0.6, len(LINESTYLES) - 0.4) return fig