Source code for dartwork_mpl.icon

"""Icon font utilities for Matplotlib figures.

This module enables convenient loading of external icon fonts such as
Material Design Icons and Font Awesome 6, rendering them as Unicode
glyphs directly in Matplotlib.

Examples
--------
>>> import dartwork_mpl as dm
>>> mdi = dm.icon_font('mdi')
>>> ax.text(0.5, 0.5, "\U000f050f",  # thermometer icon
...         fontproperties=mdi, fontsize=20)
"""

import difflib
import threading
from pathlib import Path

from matplotlib import font_manager as fm

_ICON_DIR: Path = Path(__file__).parent.parent / "asset/icon"

# NOTE: ``fa-solid`` and ``fa-regular`` share the family name
# "Font Awesome 6 Free" (weights 900/400), so addressing them by bare
# ``family=`` in matplotlib is ambiguous — the font manager picks one.
# To target a specific variant, pass the file explicitly:
# ``FontProperties(fname=icon_font_path("fa-solid"))``.
_REGISTRY: dict[str, str] = {
    "mdi": "materialdesignicons-webfont.ttf",
    "fa-solid": "Font Awesome 6 Free-Solid-900.otf",
    "fa-regular": "Font Awesome 6 Free-Regular-400.otf",
    "fa-brands": "Font Awesome 6 Brands-Regular-400.otf",
}

__all__ = ["ensure_loaded", "icon_font", "icon_font_path", "list_icon_fonts"]


def _did_you_mean(value: str, candidates: list[str]) -> str | None:
    """Return the single closest match to ``value`` from ``candidates``
    or ``None`` if no entry is close enough.

    Wraps :func:`difflib.get_close_matches` with the cutoff dartwork-mpl
    uses everywhere a "did you mean" hint is shown (0.5 — close enough
    to catch typos like ``"sceintific"`` / ``"reort"`` but far enough
    to avoid suggesting unrelated keys for completely wrong input).
    """
    matches = difflib.get_close_matches(
        value.strip().lower(), [c.lower() for c in candidates], n=1, cutoff=0.5
    )
    if not matches:
        return None
    # Return the original-case spelling so the user sees the canonical
    # form rather than the lowercased lookup key.
    lowered = matches[0]
    for c in candidates:
        if c.lower() == lowered:
            return c
    return lowered


[docs] def icon_font_path(name: str = "mdi") -> Path: """Return the absolute path to a bundled icon font file. Parameters ---------- name : str, optional Identifier of the icon font to access. Supported values: ``'mdi'``, ``'fa-solid'``, ``'fa-regular'``, ``'fa-brands'``. Default is ``'mdi'``. Returns ------- Path Absolute path to the font file (.ttf or .otf). Raises ------ ValueError If *name* is not a registered font identifier. FileNotFoundError If the font file does not exist on disk. Examples -------- >>> import dartwork_mpl as dm >>> path = dm.icon_font_path('mdi') >>> path.name 'materialdesignicons-webfont.ttf' """ ensure_loaded() if name not in _REGISTRY: available = sorted(_REGISTRY) hint = _did_you_mean(name, available) raise ValueError( f"Icon font {name!r} not found. " f"Available icon fonts: {available}." + (f" Did you mean {hint!r}?" if hint else "") ) path = _ICON_DIR / _REGISTRY[name] if not path.exists(): raise FileNotFoundError( f"Icon font file not found: {path}. Please reinstall dartwork-mpl." ) return path
[docs] def icon_font(name: str = "mdi") -> fm.FontProperties: """Create a :class:`~matplotlib.font_manager.FontProperties` object ready to pass directly to ``ax.text()`` and similar text functions. Parameters ---------- name : str, optional Identifier of the icon font to load. Supported values: ``'mdi'``, ``'fa-solid'``, ``'fa-regular'``, ``'fa-brands'``. Default is ``'mdi'``. Returns ------- FontProperties A configured Matplotlib FontProperties instance for the font. Examples -------- >>> import dartwork_mpl as dm >>> mdi = dm.icon_font('mdi') >>> # Render an icon glyph at the center >>> ax.text(0.5, 0.5, "\U000f050f", ... fontproperties=mdi, fontsize=20, ... ha='center', va='center') """ ensure_loaded() return fm.FontProperties(fname=str(icon_font_path(name)))
[docs] def list_icon_fonts() -> list[str]: """Return a list of available icon font identifiers. Returns ------- list[str] Sorted list of available identifiers (e.g., ``['fa-brands', 'fa-regular', 'fa-solid', 'mdi']``). """ return sorted(_REGISTRY.keys())
def _register_icon_fonts() -> None: """Register all bundled icon fonts with matplotlib's font manager. This function is called automatically on first import or when an internal load is required. """ for filename in _REGISTRY.values(): font_path = _ICON_DIR / filename if font_path.exists(): fm.fontManager.addfont(str(font_path)) _loaded: bool = False _lock: threading.Lock = threading.Lock()
[docs] def ensure_loaded() -> None: """Ensure icon fonts are loaded and registered if not already done. Thread-safe: uses double-checked locking to avoid duplicate icon-font registration when called concurrently from multiple threads (matching :func:`dartwork_mpl.font.ensure_loaded`). """ global _loaded # Fast path: skip the lock once already loaded. if _loaded: return with _lock: if _loaded: return _register_icon_fonts() _loaded = True