diff --git a/ultraplot/__init__.py b/ultraplot/__init__.py index 2a2db3bd..8d597545 100644 --- a/ultraplot/__init__.py +++ b/ultraplot/__init__.py @@ -16,8 +16,8 @@ from . import internals, externals, tests # noqa: F401 from .internals.benchmarks import _benchmark -with _benchmark("pyplot"): - from matplotlib import pyplot # noqa: F401 +# Defer pyplot import - it's the biggest import time bottleneck +# It will be imported lazily in ui.py when needed with _benchmark("cartopy"): try: import cartopy # noqa: F401 @@ -115,3 +115,13 @@ if rc["ultraplot.check_for_latest_version"]: check_for_update("ultraplot") + + +# Lazy pyplot access for backward compatibility +def __getattr__(name): + """Lazy load pyplot when accessed as ultraplot.pyplot.""" + if name == "pyplot": + from .ui import _get_pyplot + + return _get_pyplot() + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 9364fd0b..7099ef31 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -28,7 +28,6 @@ import matplotlib.lines as mlines import matplotlib.patches as mpatches import matplotlib.ticker as mticker -import matplotlib.pyplot as mplt import matplotlib as mpl from packaging import version import numpy as np @@ -387,25 +386,20 @@ docstring._snippet_manager["plot.cycle"] = _cycle_docstring docstring._snippet_manager["plot.cmap_norm"] = _cmap_norm_docstring + +# Log plot docstrings - built without importing pyplot _log_doc = """ Plot {kind} UltraPlot is optimized for visualizing logarithmic scales by default. For cases with large differences in magnitude, we recommend setting `rc["formatter.log"] = True` to enhance axis label formatting. -{matplotlib_doc} -""" - -docstring._snippet_manager["plot.loglog"] = _log_doc.format( - kind="loglog", matplotlib_doc=mplt.loglog.__doc__ -) -docstring._snippet_manager["plot.semilogy"] = _log_doc.format( - kind="semilogy", matplotlib_doc=mplt.semilogy.__doc__ -) +See matplotlib.pyplot.{kind} for more details. +""" -docstring._snippet_manager["plot.semilogx"] = _log_doc.format( - kind="semilogx", matplotlib_doc=mplt.semilogx.__doc__ -) +docstring._snippet_manager["plot.loglog"] = _log_doc.format(kind="loglog") +docstring._snippet_manager["plot.semilogy"] = _log_doc.format(kind="semilogy") +docstring._snippet_manager["plot.semilogx"] = _log_doc.format(kind="semilogx") # Levels docstrings # NOTE: In some functions we only need some components diff --git a/ultraplot/ui.py b/ultraplot/ui.py index 7fb66334..e1f8b089 100644 --- a/ultraplot/ui.py +++ b/ultraplot/ui.py @@ -2,8 +2,6 @@ """ The starting point for creating ultraplot figures. """ -import matplotlib.pyplot as plt - from . import axes as paxes from . import figure as pfigure from . import gridspec as pgridspec @@ -20,8 +18,31 @@ "ion", "ioff", "isinteractive", + # Note: pyplot is NOT in __all__ to prevent eager loading during star import + # It's accessible via __getattr__ for lazy loading: import ultraplot as uplt; uplt.pyplot ] +# Lazy pyplot import +_plt = None + + +def _get_pyplot(): + """Get matplotlib.pyplot, importing it lazily on first use.""" + global _plt + if _plt is None: + import matplotlib.pyplot as plt + + _plt = plt + return _plt + + +# Make pyplot accessible at module level +def __getattr__(name): + """Lazy load pyplot when accessed as module attribute.""" + if name == "pyplot": + return _get_pyplot() + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + # Docstrings _pyplot_docstring = """ @@ -58,6 +79,7 @@ def show(*args, **kwargs): *args, **kwargs Passed to `matplotlib.pyplot.show`. """ + plt = _get_pyplot() return plt.show(*args, **kwargs) @@ -72,6 +94,7 @@ def close(*args, **kwargs): *args, **kwargs Passed to `matplotlib.pyplot.close`. """ + plt = _get_pyplot() return plt.close(*args, **kwargs) @@ -86,6 +109,7 @@ def switch_backend(*args, **kwargs): *args, **kwargs Passed to `matplotlib.pyplot.switch_backend`. """ + plt = _get_pyplot() return plt.switch_backend(*args, **kwargs) @@ -95,6 +119,7 @@ def ion(): Call `matplotlib.pyplot.ion`. %(ui.pyplot)s """ + plt = _get_pyplot() return plt.ion() @@ -104,6 +129,7 @@ def ioff(): Call `matplotlib.pyplot.ioff`. %(ui.pyplot)s """ + plt = _get_pyplot() return plt.ioff() @@ -113,6 +139,7 @@ def isinteractive(): Call `matplotlib.pyplot.isinteractive`. %(ui.pyplot)s """ + plt = _get_pyplot() return plt.isinteractive() @@ -141,6 +168,7 @@ def figure(**kwargs): matplotlib.figure.Figure """ _parse_figsize(kwargs) + plt = _get_pyplot() return plt.figure(FigureClass=pfigure.Figure, **kwargs)