diff --git a/.github/workflows/proplot.yml b/.github/workflows/proplot.yml new file mode 100644 index 000000000..a22d6bfc0 --- /dev/null +++ b/.github/workflows/proplot.yml @@ -0,0 +1,36 @@ +name: Build and Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + run: | + pytest + + - name: Build documentation + run: | + pip install sphinx jupytext nbsphinx sphinx-copybutton sphinx-rtd-ligth-dark sphinx-automodapi + cd docs + make html diff --git a/.readthedocs.yml b/.readthedocs.yml index bbb41cdee..92e420fa7 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,17 +1,25 @@ -# .readthedocs.yml + # .readthedocs.yml # Read the Docs configuration file version: 2 -# Sphinx config -sphinx: - builder: html - configuration: docs/conf.py - # Python config build: - image: latest -python: - version: 3.6 - system_packages: true + os: ubuntu-22.04 + tools: + python: mambaforge-latest + jobs: + pre_build: + jobs: + pre_build: + - locale -a + - export LC_ALL=en_US.UTF-8 + - export LANG=en_US.UTF-8 + - pytest + + conda: - environment: docs/environment.yml + environment: ./docs/environment.yml + +sphinx: + configuration: ./docs/conf.py + builder: html diff --git a/baseline/test_colorbar.png b/baseline/test_colorbar.png new file mode 100644 index 000000000..bc3044515 Binary files /dev/null and b/baseline/test_colorbar.png differ diff --git a/baseline/test_inbounds_data.png b/baseline/test_inbounds_data.png new file mode 100644 index 000000000..f2c806653 Binary files /dev/null and b/baseline/test_inbounds_data.png differ diff --git a/baseline/test_standardized_input.png b/baseline/test_standardized_input.png new file mode 100644 index 000000000..731faed6f Binary files /dev/null and b/baseline/test_standardized_input.png differ diff --git a/baseline/test_standardized_inputs_1d.png b/baseline/test_standardized_inputs_1d.png new file mode 100644 index 000000000..fb6c75054 Binary files /dev/null and b/baseline/test_standardized_inputs_1d.png differ diff --git a/ci/environment.yml b/ci/environment.yml index bf8e0eb3b..a341a59a2 100644 --- a/ci/environment.yml +++ b/ci/environment.yml @@ -5,35 +5,34 @@ name: proplot-dev channels: - conda-forge dependencies: - - python==3.8 - - numpy + - python==3.11 + - numpy>=1.26.0 - pandas - xarray - - matplotlib==3.2.2 - - cartopy==0.20.2 + - matplotlib>=3.9.1 + - cartopy - ipykernel - pandoc - python-build - - setuptools + - setuptools==72.1.0 - setuptools_scm - setuptools_scm_git_archive - wheel - pip + - flake8 + - isort + - black + - doc8 + - pytest + - pytest-sugar + - pyqt5 + - docutils>=0.16 + - sphinx>=3.0 + - sphinx-copybutton + - sphinx-rtd-light-dark + - jinja2>=2.11.3 + - markupsafe>=2.0.1 + - nbsphinx>=0.8.1 + - jupytext - pip: - - .. - - flake8 - - isort - - black - - doc8 - - pytest - - pytest-sugar - - pyqt5 - - docutils==0.16 - - sphinx>=3.0 - - sphinx-copybutton - - sphinx-rtd-light-dark - - jinja2==2.11.3 - - markupsafe==2.0.1 - - nbsphinx==0.8.1 - - jupytext - git+https://github.com/proplot-dev/sphinx-automodapi@proplot-mods diff --git a/docs/1dplots.py b/docs/1dplots.py index 1bcb00443..be7fdaf41 100644 --- a/docs/1dplots.py +++ b/docs/1dplots.py @@ -68,35 +68,35 @@ N = 5 state = np.random.RandomState(51423) -with pplt.rc.context({'axes.prop_cycle': pplt.Cycle('Grays', N=N, left=0.3)}): +with pplt.rc.context({"axes.prop_cycle": pplt.Cycle("Grays", N=N, left=0.3)}): # Sample data x = np.linspace(-5, 5, N) y = state.rand(N, 5) - fig = pplt.figure(share=False, suptitle='Standardized input demonstration') + fig = pplt.figure(share=False, suptitle="Standardized input demonstration") # Plot by passing both x and y coordinates - ax = fig.subplot(121, title='Manual x coordinates') + ax = fig.subplot(121, title="Manual x coordinates") ax.area(x, -1 * y / N, stack=True) ax.bar(x, y, linewidth=0, alpha=1, width=0.8) ax.plot(x, y + 1, linewidth=2) - ax.scatter(x, y + 2, marker='s', markersize=5**2) + ax.scatter(x, y + 2, marker="s", markersize=5**2) # Plot by passing just y coordinates # Default x coordinates are inferred from DataFrame, # inferred from DataArray, or set to np.arange(0, y.shape[0]) - ax = fig.subplot(122, title='Auto x coordinates') + ax = fig.subplot(122, title="Auto x coordinates") ax.area(-1 * y / N, stack=True) ax.bar(y, linewidth=0, alpha=1) ax.plot(y + 1, linewidth=2) - ax.scatter(y + 2, marker='s', markersize=5**2) - fig.format(xlabel='xlabel', ylabel='ylabel') + ax.scatter(y + 2, marker="s", markersize=5**2) + fig.format(xlabel="xlabel", ylabel="ylabel") # %% import proplot as pplt import numpy as np # Sample data -cycle = pplt.Cycle('davos', right=0.8) +cycle = pplt.Cycle("davos", right=0.8) state = np.random.RandomState(51423) N, M = 400, 20 xmax = 20 @@ -107,24 +107,27 @@ fig = pplt.figure(refwidth=2.2, share=False) axs = fig.subplots([[0, 1, 1, 0], [2, 2, 3, 3]], wratios=(2, 1, 1, 2)) axs[0].axvspan( - 0, xmax, zorder=3, edgecolor='red', facecolor=pplt.set_alpha('red', 0.2), + 0, + xmax, + zorder=3, + edgecolor="red", + facecolor=pplt.set_alpha("red", 0.2), ) for i, ax in enumerate(axs): inbounds = i == 1 - title = f'Restricted xlim inbounds={inbounds}' - title += ' (default)' if inbounds else '' + title = f"Restricted xlim inbounds={inbounds}" + title += " (default)" if inbounds else "" ax.format( xmax=(None if i == 0 else xmax), - title=('Default xlim' if i == 0 else title), + title=("Default xlim" if i == 0 else title), ) ax.plot(x, y, cycle=cycle, inbounds=inbounds) fig.format( - xlabel='xlabel', - ylabel='ylabel', - suptitle='Default ylim restricted to in-bounds data' + xlabel="xlabel", + ylabel="ylabel", + suptitle="Default ylim restricted to in-bounds data", ) - # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_1dintegration: # @@ -164,49 +167,43 @@ # DataArray state = np.random.RandomState(51423) -data = ( - np.sin(np.linspace(0, 2 * np.pi, 20))[:, None] - + state.rand(20, 8).cumsum(axis=1) -) +data = np.sin(np.linspace(0, 2 * np.pi, 20))[:, None] + state.rand(20, 8).cumsum(axis=1) coords = { - 'x': xr.DataArray( + "x": xr.DataArray( np.linspace(0, 1, 20), - dims=('x',), - attrs={'long_name': 'distance', 'units': 'km'} + dims=("x",), + attrs={"long_name": "distance", "units": "km"}, + ), + "num": xr.DataArray( + np.arange(0, 80, 10), dims=("num",), attrs={"long_name": "parameter"} ), - 'num': xr.DataArray( - np.arange(0, 80, 10), - dims=('num',), - attrs={'long_name': 'parameter'} - ) } da = xr.DataArray( - data, dims=('x', 'num'), coords=coords, name='energy', attrs={'units': 'kJ'} + data, dims=("x", "num"), coords=coords, name="energy", attrs={"units": "kJ"} ) # DataFrame -data = ( - (np.cos(np.linspace(0, 2 * np.pi, 20))**4)[:, None] + state.rand(20, 5) ** 2 -) -ts = pd.date_range('1/1/2000', periods=20) -df = pd.DataFrame(data, index=ts, columns=['foo', 'bar', 'baz', 'zap', 'baf']) -df.name = 'data' -df.index.name = 'date' -df.columns.name = 'category' +data = (np.cos(np.linspace(0, 2 * np.pi, 20)) ** 4)[:, None] + state.rand(20, 5) ** 2 +ts = pd.date_range("1/1/2000", periods=20) +df = pd.DataFrame(data, index=ts, columns=["foo", "bar", "baz", "zap", "baf"]) +df.name = "data" +df.index.name = "date" +df.columns.name = "category" # %% import proplot as pplt -fig = pplt.figure(share=False, suptitle='Automatic subplot formatting') + +fig = pplt.figure(share=False, suptitle="Automatic subplot formatting") # Plot DataArray -cycle = pplt.Cycle('dark blue', space='hpl', N=da.shape[1]) +cycle = pplt.Cycle("dark blue", space="hpl", N=da.shape[1]) ax = fig.subplot(121) -ax.scatter(da, cycle=cycle, lw=3, colorbar='t', colorbar_kw={'locator': 20}) +ax.scatter(da, cycle=cycle, lw=3, colorbar="t", colorbar_kw={"locator": 20}) # Plot Dataframe -cycle = pplt.Cycle('dark green', space='hpl', N=df.shape[1]) +cycle = pplt.Cycle("dark green", space="hpl", N=df.shape[1]) ax = fig.subplot(122) -ax.plot(df, cycle=cycle, lw=3, legend='t', legend_kw={'frame': False}) +ax.plot(df, cycle=cycle, lw=3, legend="t", legend_kw={"frame": False}) # %% [raw] raw_mimetype="text/restructuredtext" @@ -244,25 +241,23 @@ data1 += state.rand(M, N) data2 += state.rand(M, N) -with pplt.rc.context({'lines.linewidth': 3}): +with pplt.rc.context({"lines.linewidth": 3}): # Use property cycle for columns of 2D input data fig = pplt.figure(share=False) - ax = fig.subplot(121, title='Single plot call') + ax = fig.subplot(121, title="Single plot call") ax.plot( 2 * data1 + data2, - cycle='black', # cycle from monochromatic colormap - cycle_kw={'ls': ('-', '--', '-.', ':')} + cycle="black", # cycle from monochromatic colormap + cycle_kw={"ls": ("-", "--", "-.", ":")}, ) # Use property cycle with successive plot() calls - ax = fig.subplot(122, title='Multiple plot calls') + ax = fig.subplot(122, title="Multiple plot calls") for i in range(data1.shape[1]): - ax.plot(data1[:, i], cycle='Reds', cycle_kw={'N': N, 'left': 0.3}) + ax.plot(data1[:, i], cycle="Reds", cycle_kw={"N": N, "left": 0.3}) for i in range(data1.shape[1]): - ax.plot(data2[:, i], cycle='Blues', cycle_kw={'N': N, 'left': 0.3}) - fig.format( - xlabel='xlabel', ylabel='ylabel', suptitle='On-the-fly property cycles' - ) + ax.plot(data2[:, i], cycle="Blues", cycle_kw={"N": N, "left": 0.3}) + fig.format(xlabel="xlabel", ylabel="ylabel", suptitle="On-the-fly property cycles") # %% [raw] raw_mimetype="text/restructuredtext" @@ -290,40 +285,41 @@ # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) gs = pplt.GridSpec(nrows=3, ncols=2) -fig = pplt.figure(refwidth=2.2, span=False, share='labels') +fig = pplt.figure(refwidth=2.2, span=False, share="labels") # Vertical vs. horizontal data = (state.rand(10, 5) - 0.5).cumsum(axis=0) -ax = fig.subplot(gs[0], title='Dependent x-axis') -ax.line(data, lw=2.5, cycle='seaborn') -ax = fig.subplot(gs[1], title='Dependent y-axis') -ax.linex(data, lw=2.5, cycle='seaborn') +ax = fig.subplot(gs[0], title="Dependent x-axis") +ax.line(data, lw=2.5, cycle="seaborn") +ax = fig.subplot(gs[1], title="Dependent y-axis") +ax.linex(data, lw=2.5, cycle="seaborn") # Vertical lines -gray = 'gray7' +gray = "gray7" data = state.rand(20) - 0.5 -ax = fig.subplot(gs[2], title='Vertical lines') +ax = fig.subplot(gs[2], title="Vertical lines") ax.area(data, color=gray, alpha=0.2) ax.vlines(data, negpos=True, lw=2) # Horizontal lines -ax = fig.subplot(gs[3], title='Horizontal lines') +ax = fig.subplot(gs[3], title="Horizontal lines") ax.areax(data, color=gray, alpha=0.2) ax.hlines(data, negpos=True, lw=2) # Step -ax = fig.subplot(gs[4], title='Step plot') +ax = fig.subplot(gs[4], title="Step plot") data = state.rand(20, 4).cumsum(axis=1).cumsum(axis=0) -cycle = ('gray6', 'blue7', 'red7', 'gray4') -ax.step(data, cycle=cycle, labels=list('ABCD'), legend='ul', legend_kw={'ncol': 2}) +cycle = ("gray6", "blue7", "red7", "gray4") +ax.step(data, cycle=cycle, labels=list("ABCD"), legend="ul", legend_kw={"ncol": 2}) # Stems -ax = fig.subplot(gs[5], title='Stem plot') +ax = fig.subplot(gs[5], title="Stem plot") data = state.rand(20) ax.stem(data) -fig.format(suptitle='Line plots demo', xlabel='xlabel', ylabel='ylabel') +fig.format(suptitle="Line plots demo", xlabel="xlabel", ylabel="ylabel") # %% [raw] raw_mimetype="text/restructuredtext" @@ -371,35 +367,44 @@ state = np.random.RandomState(51423) x = (state.rand(20) - 0).cumsum() data = (state.rand(20, 4) - 0.5).cumsum(axis=0) -data = pd.DataFrame(data, columns=pd.Index(['a', 'b', 'c', 'd'], name='label')) +data = pd.DataFrame(data, columns=pd.Index(["a", "b", "c", "d"], name="label")) # Figure gs = pplt.GridSpec(ncols=2, nrows=2) -fig = pplt.figure(refwidth=2.2, share='labels', span=False) +fig = pplt.figure(refwidth=2.2, share="labels", span=False) # Vertical vs. horizontal -ax = fig.subplot(gs[0], title='Dependent x-axis') -ax.scatter(data, cycle='538') -ax = fig.subplot(gs[1], title='Dependent y-axis') -ax.scatterx(data, cycle='538') +ax = fig.subplot(gs[0], title="Dependent x-axis") +ax.scatter(data, cycle="538") +ax = fig.subplot(gs[1], title="Dependent y-axis") +ax.scatterx(data, cycle="538") # Scatter plot with property cycler -ax = fig.subplot(gs[2], title='With property cycle') +ax = fig.subplot(gs[2], title="With property cycle") obj = ax.scatter( - x, data, legend='ul', legend_kw={'ncols': 2}, - cycle='Set2', cycle_kw={'m': ['x', 'o', 'x', 'o'], 'ms': [5, 10, 20, 30]} + x, + data, + legend="ul", + legend_kw={"ncols": 2}, + cycle="Set2", + cycle_kw={"m": ["x", "o", "x", "o"], "ms": [5, 10, 20, 30]}, ) # Scatter plot with colormap -ax = fig.subplot(gs[3], title='With colormap') +ax = fig.subplot(gs[3], title="With colormap") data = state.rand(2, 100) obj = ax.scatter( *data, - s=state.rand(100), smin=6, smax=60, marker='o', - c=data.sum(axis=0), cmap='maroon', - colorbar='lr', colorbar_kw={'label': 'label'}, + s=state.rand(100), + smin=6, + smax=60, + marker="o", + c=data.sum(axis=0), + cmap="maroon", + colorbar="lr", + colorbar_kw={"label": "label"}, ) -fig.format(suptitle='Scatter plot demo', xlabel='xlabel', ylabel='ylabel') +fig.format(suptitle="Scatter plot demo", xlabel="xlabel", ylabel="ylabel") # %% [raw] raw_mimetype="text/restructuredtext" @@ -421,10 +426,11 @@ import proplot as pplt import numpy as np import pandas as pd + gs = pplt.GridSpec(ncols=2, wratios=(2, 1)) -fig = pplt.figure(figwidth='16cm', refaspect=(2, 1), share=False) -fig.format(suptitle='Parametric plots demo') -cmap = 'IceFire' +fig = pplt.figure(figwidth="16cm", refaspect=(2, 1), share=False) +fig.format(suptitle="Parametric plots demo") +cmap = "IceFire" # Sample data state = np.random.RandomState(51423) @@ -432,15 +438,23 @@ x = (state.rand(N) - 0.52).cumsum() y = state.rand(N) c = np.linspace(-N / 2, N / 2, N) # color values -c = pd.Series(c, name='parametric coordinate') +c = pd.Series(c, name="parametric coordinate") # Parametric line with smooth gradations ax = fig.subplot(gs[0]) m = ax.parametric( - x, y, c, interp=10, capstyle='round', joinstyle='round', - lw=7, cmap=cmap, colorbar='b', colorbar_kw={'locator': 5} + x, + y, + c, + interp=10, + capstyle="round", + joinstyle="round", + lw=7, + cmap=cmap, + colorbar="b", + colorbar_kw={"locator": 5}, ) -ax.format(xlabel='xlabel', ylabel='ylabel', title='Line with smooth gradations') +ax.format(xlabel="xlabel", ylabel="ylabel", title="Line with smooth gradations") # Sample data N = 12 @@ -454,10 +468,13 @@ ax = fig.subplot(gs[1]) m = ax.parametric(x, y, c, cmap=cmap, lw=15) ax.format( - xlim=(-1, 1), ylim=(-1, 1), title='Step gradations', - xlabel='cosine angle', ylabel='sine angle' + xlim=(-1, 1), + ylim=(-1, 1), + title="Step gradations", + xlabel="cosine angle", + ylabel="sine angle", ) -ax.colorbar(m, loc='b', locator=2, label='parametric coordinate') +ax.colorbar(m, loc="b", locator=2, label="parametric coordinate") # %% [raw] raw_mimetype="text/restructuredtext" @@ -509,29 +526,34 @@ state = np.random.RandomState(51423) data = state.rand(5, 5).cumsum(axis=0).cumsum(axis=1)[:, ::-1] data = pd.DataFrame( - data, columns=pd.Index(np.arange(1, 6), name='column'), - index=pd.Index(['a', 'b', 'c', 'd', 'e'], name='row idx') + data, + columns=pd.Index(np.arange(1, 6), name="column"), + index=pd.Index(["a", "b", "c", "d", "e"], name="row idx"), ) # Figure -pplt.rc.abc = 'a.' -pplt.rc.titleloc = 'l' +pplt.rc.abc = "a." +pplt.rc.titleloc = "l" gs = pplt.GridSpec(nrows=2, hratios=(3, 2)) fig = pplt.figure(refaspect=2, refwidth=4.8, share=False) # Side-by-side bars -ax = fig.subplot(gs[0], title='Side-by-side') +ax = fig.subplot(gs[0], title="Side-by-side") obj = ax.bar( - data, cycle='Reds', edgecolor='red9', colorbar='ul', colorbar_kw={'frameon': False} + data, cycle="Reds", edgecolor="red9", colorbar="ul", colorbar_kw={"frameon": False} ) ax.format(xlocator=1, xminorlocator=0.5, ytickminor=False) # Stacked bars -ax = fig.subplot(gs[1], title='Stacked') +ax = fig.subplot(gs[1], title="Stacked") obj = ax.barh( - data.iloc[::-1, :], cycle='Blues', edgecolor='blue9', legend='ur', stack=True, + data.iloc[::-1, :], + cycle="Blues", + edgecolor="blue9", + legend="ur", + stack=True, ) -fig.format(grid=False, suptitle='Bar plot demo') +fig.format(grid=False, suptitle="Bar plot demo") pplt.rc.reset() # %% @@ -541,27 +563,37 @@ # Sample data state = np.random.RandomState(51423) data = state.rand(5, 3).cumsum(axis=0) -cycle = ('gray3', 'gray5', 'gray7') +cycle = ("gray3", "gray5", "gray7") # Figure -pplt.rc.abc = 'a.' -pplt.rc.titleloc = 'l' +pplt.rc.abc = "a." +pplt.rc.titleloc = "l" fig = pplt.figure(refwidth=2.3, share=False) # Overlaid area patches -ax = fig.subplot(121, title='Fill between columns') +ax = fig.subplot(121, title="Fill between columns") ax.area( - np.arange(5), data, data + state.rand(5)[:, None], cycle=cycle, alpha=0.7, - legend='uc', legend_kw={'center': True, 'ncols': 2, 'labels': ['z', 'y', 'qqqq']}, + np.arange(5), + data, + data + state.rand(5)[:, None], + cycle=cycle, + alpha=0.7, + legend="uc", + legend_kw={"center": True, "ncols": 2, "labels": ["z", "y", "qqqq"]}, ) # Stacked area patches -ax = fig.subplot(122, title='Stack between columns') +ax = fig.subplot(122, title="Stack between columns") ax.area( - np.arange(5), data, stack=True, cycle=cycle, alpha=0.8, - legend='ul', legend_kw={'center': True, 'ncols': 2, 'labels': ['z', 'y', 'qqqq']}, + np.arange(5), + data, + stack=True, + cycle=cycle, + alpha=0.8, + legend="ul", + legend_kw={"center": True, "ncols": 2, "labels": ["z", "y", "qqqq"]}, ) -fig.format(grid=False, xlabel='xlabel', ylabel='ylabel', suptitle='Area plot demo') +fig.format(grid=False, xlabel="xlabel", ylabel="ylabel", suptitle="Area plot demo") pplt.rc.reset() # %% [raw] raw_mimetype="text/restructuredtext" @@ -589,30 +621,33 @@ data = 4 * (state.rand(40) - 0.5) # Figure -pplt.rc.abc = 'a.' -pplt.rc.titleloc = 'l' +pplt.rc.abc = "a." +pplt.rc.titleloc = "l" fig, axs = pplt.subplots(nrows=3, refaspect=2, figwidth=5) axs.format( - xmargin=0, xlabel='xlabel', ylabel='ylabel', grid=True, - suptitle='Positive and negative colors demo', + xmargin=0, + xlabel="xlabel", + ylabel="ylabel", + grid=True, + suptitle="Positive and negative colors demo", ) for ax in axs: - ax.axhline(0, color='k', linewidth=1) # zero line + ax.axhline(0, color="k", linewidth=1) # zero line # Line plot ax = axs[0] ax.vlines(data, linewidth=3, negpos=True) -ax.format(title='Line plot') +ax.format(title="Line plot") # Bar plot ax = axs[1] -ax.bar(data, width=1, negpos=True, edgecolor='k') -ax.format(title='Bar plot') +ax.bar(data, width=1, negpos=True, edgecolor="k") +ax.format(title="Bar plot") # Area plot ax = axs[2] -ax.area(data, negpos=True, lw=0.5, edgecolor='k') -ax.format(title='Area plot') +ax.area(data, negpos=True, lw=0.5, edgecolor="k") +ax.format(title="Area plot") # Reset title styles changed above pplt.rc.reset() diff --git a/docs/2dplots.py b/docs/2dplots.py index 07ff162ce..707b63ef0 100644 --- a/docs/2dplots.py +++ b/docs/2dplots.py @@ -88,15 +88,19 @@ data = state.rand(y.size, x.size) # "center" coordinates lim = (np.min(xedges), np.max(xedges)) -with pplt.rc.context({'cmap': 'Grays', 'cmap.levels': 21}): +with pplt.rc.context({"cmap": "Grays", "cmap.levels": 21}): # Figure fig = pplt.figure(refwidth=2.3, share=False) axs = fig.subplots(ncols=2, nrows=2) axs.format( - xlabel='xlabel', ylabel='ylabel', - xlim=lim, ylim=lim, xlocator=5, ylocator=5, - suptitle='Standardized input demonstration', - toplabels=('Coordinate centers', 'Coordinate edges'), + xlabel="xlabel", + ylabel="ylabel", + xlim=lim, + ylim=lim, + xlocator=5, + ylocator=5, + suptitle="Standardized input demonstration", + toplabels=("Coordinate centers", "Coordinate edges"), ) # Plot using both centers and edges as coordinates @@ -110,7 +114,7 @@ import numpy as np # Sample data -cmap = 'turku_r' +cmap = "turku_r" state = np.random.RandomState(51423) N = 80 x = y = np.arange(N + 1) @@ -119,25 +123,32 @@ # Plot the data fig, axs = pplt.subplots( - [[0, 1, 1, 0], [2, 2, 3, 3]], wratios=(1.3, 1, 1, 1.3), span=False, refwidth=2.2, + [[0, 1, 1, 0], [2, 2, 3, 3]], + wratios=(1.3, 1, 1, 1.3), + span=False, + refwidth=2.2, ) axs[0].fill_between( - xlim, *ylim, zorder=3, edgecolor='red', facecolor=pplt.set_alpha('red', 0.2), + xlim, + *ylim, + zorder=3, + edgecolor="red", + facecolor=pplt.set_alpha("red", 0.2), ) for i, ax in enumerate(axs): inbounds = i == 1 - title = f'Restricted lims inbounds={inbounds}' - title += ' (default)' if inbounds else '' + title = f"Restricted lims inbounds={inbounds}" + title += " (default)" if inbounds else "" ax.format( xlim=(None if i == 0 else xlim), ylim=(None if i == 0 else ylim), - title=('Default axis limits' if i == 0 else title), + title=("Default axis limits" if i == 0 else title), ) ax.pcolor(x, y, data, cmap=cmap, inbounds=inbounds) fig.format( - xlabel='xlabel', - ylabel='ylabel', - suptitle='Default vmin/vmax restricted to in-bounds data' + xlabel="xlabel", + ylabel="ylabel", + suptitle="Default vmin/vmax restricted to in-bounds data", ) # %% [raw] raw_mimetype="text/restructuredtext" @@ -180,51 +191,51 @@ # DataArray state = np.random.RandomState(51423) linspace = np.linspace(0, np.pi, 20) -data = 50 * state.normal(1, 0.2, size=(20, 20)) * ( - np.sin(linspace * 2) ** 2 - * np.cos(linspace + np.pi / 2)[:, None] ** 2 +data = ( + 50 + * state.normal(1, 0.2, size=(20, 20)) + * (np.sin(linspace * 2) ** 2 * np.cos(linspace + np.pi / 2)[:, None] ** 2) ) lat = xr.DataArray( - np.linspace(-90, 90, 20), - dims=('lat',), - attrs={'units': '\N{DEGREE SIGN}N'} + np.linspace(-90, 90, 20), dims=("lat",), attrs={"units": "\N{DEGREE SIGN}N"} ) plev = xr.DataArray( np.linspace(1000, 0, 20), - dims=('plev',), - attrs={'long_name': 'pressure', 'units': 'hPa'} + dims=("plev",), + attrs={"long_name": "pressure", "units": "hPa"}, ) da = xr.DataArray( data, - name='u', - dims=('plev', 'lat'), - coords={'plev': plev, 'lat': lat}, - attrs={'long_name': 'zonal wind', 'units': 'm/s'} + name="u", + dims=("plev", "lat"), + coords={"plev": plev, "lat": lat}, + attrs={"long_name": "zonal wind", "units": "m/s"}, ) # DataFrame data = state.rand(12, 20) df = pd.DataFrame( (data - 0.4).cumsum(axis=0).cumsum(axis=1)[::1, ::-1], - index=pd.date_range('2000-01', '2000-12', freq='MS') + index=pd.date_range("2000-01", "2000-12", freq="MS"), ) -df.name = 'temperature (\N{DEGREE SIGN}C)' -df.index.name = 'date' -df.columns.name = 'variable (units)' +df.name = "temperature (\N{DEGREE SIGN}C)" +df.index.name = "date" +df.columns.name = "variable (units)" # %% import proplot as pplt -fig = pplt.figure(refwidth=2.5, share=False, suptitle='Automatic subplot formatting') + +fig = pplt.figure(refwidth=2.5, share=False, suptitle="Automatic subplot formatting") # Plot DataArray -cmap = pplt.Colormap('PuBu', left=0.05) +cmap = pplt.Colormap("PuBu", left=0.05) ax = fig.subplot(121, yreverse=True) -ax.contourf(da, cmap=cmap, colorbar='t', lw=0.7, ec='k') +ax.contourf(da, cmap=cmap, colorbar="t", lw=0.7, ec="k") # Plot DataFrame ax = fig.subplot(122, yreverse=True) -ax.contourf(df, cmap='YlOrRd', colorbar='t', lw=0.7, ec='k') -ax.format(xtickminor=False, yformatter='%b', ytickminor=False) +ax.contourf(df, cmap="YlOrRd", colorbar="t", lw=0.7, ec="k") +ax.format(xtickminor=False, yformatter="%b", ytickminor=False) # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_apply_cmap: @@ -263,23 +274,23 @@ data = np.cumsum(state.rand(N, N), axis=0) # Custom defaults of each type -pplt.rc['cmap.sequential'] = 'PuBuGn' -pplt.rc['cmap.diverging'] = 'PiYG' -pplt.rc['cmap.cyclic'] = 'bamO' -pplt.rc['cmap.qualitative'] = 'flatui' +pplt.rc["cmap.sequential"] = "PuBuGn" +pplt.rc["cmap.diverging"] = "PiYG" +pplt.rc["cmap.cyclic"] = "bamO" +pplt.rc["cmap.qualitative"] = "flatui" # Make plots. Note the default behavior is sequential=True or diverging=True # depending on whether data contains negative values (see below). -fig = pplt.figure(refwidth=2.2, span=False, suptitle='Colormap types') +fig = pplt.figure(refwidth=2.2, span=False, suptitle="Colormap types") axs = fig.subplots(ncols=2, nrows=2) -axs.format(xformatter='none', yformatter='none') -axs[0].pcolor(data, sequential=True, colorbar='l', extend='max') -axs[1].pcolor(data - 5, diverging=True, colorbar='r', extend='both') -axs[2].pcolor(data % 8, cyclic=True, colorbar='l') -axs[3].pcolor(data, levels=pplt.arange(0, 12, 2), qualitative=True, colorbar='r') -types = ('sequential', 'diverging', 'cyclic', 'qualitative') +axs.format(xformatter="none", yformatter="none") +axs[0].pcolor(data, sequential=True, colorbar="l", extend="max") +axs[1].pcolor(data - 5, diverging=True, colorbar="r", extend="both") +axs[2].pcolor(data % 8, cyclic=True, colorbar="l") +axs[3].pcolor(data, levels=pplt.arange(0, 12, 2), qualitative=True, colorbar="r") +types = ("sequential", "diverging", "cyclic", "qualitative") for ax, typ in zip(axs, types): - ax.format(title=typ.title() + ' colormap') + ax.format(title=typ.title() + " colormap") pplt.rc.reset() # %% @@ -293,13 +304,13 @@ # Continuous "diverging" colormap fig = pplt.figure(refwidth=2.3, spanx=False) -ax = fig.subplot(121, title="Diverging colormap with 'cmap'", xlabel='xlabel') +ax = fig.subplot(121, title="Diverging colormap with 'cmap'", xlabel="xlabel") ax.contourf( data, - norm='div', - cmap=('cobalt', 'white', 'violet red'), - cmap_kw={'space': 'hsl', 'cut': 0.15}, - colorbar='b', + norm="div", + cmap=("cobalt", "white", "violet red"), + cmap_kw={"space": "hsl", "cut": 0.15}, + colorbar="b", ) # Discrete "qualitative" colormap @@ -307,31 +318,10 @@ ax.contourf( data, levels=pplt.arange(-6, 9, 3), - colors=['red5', 'blue5', 'yellow5', 'gray5', 'violet5'], - colorbar='b', + colors=["red5", "blue5", "yellow5", "gray5", "violet5"], + colorbar="b", ) -fig.format(xlabel='xlabel', ylabel='ylabel', suptitle='On-the-fly colormaps') - -# %% [raw] raw_mimetype="text/restructuredtext" -# .. _ug_apply_norm: -# -# Changing the normalizer -# ----------------------- -# -# Matplotlib `colormap "normalizers" -# `__ -# translate raw data values into normalized colormap indices. In proplot, -# you can select the normalizer from its "registered" name using the -# `~proplot.constructor.Norm` :ref:`constructor function `. You -# can also build a normalizer on-the-fly using the `norm` and `norm_kw` keywords, -# available with most 2D `~proplot.axes.PlotAxes` commands. -# If you want to work with the normalizer classes directly, they are available in -# the top-level namespace (e.g., ``norm=pplt.LogNorm(...)`` is allowed). To -# explicitly set the normalization range, you can pass the usual `vmin` and `vmax` -# keywords to the plotting command. See :ref:`below ` for more -# details on colormap normalization in proplot. -# %% import proplot as pplt import numpy as np @@ -342,13 +332,13 @@ # Create figure gs = pplt.GridSpec(ncols=2) -fig = pplt.figure(refwidth=2.3, span=False, suptitle='Normalizer types') +fig = pplt.figure(refwidth=2.3, span=False, suptitle="Normalizer types") # Different normalizers -ax = fig.subplot(gs[0], title='Default linear normalizer') -ax.pcolormesh(data, cmap='magma', colorbar='b') +ax = fig.subplot(gs[0], title="Default linear normalizer") +ax.pcolormesh(data, cmap="magma", colorbar="b") ax = fig.subplot(gs[1], title="Logarithmic normalizer with norm='log'") -ax.pcolormesh(data, cmap='magma', norm='log', colorbar='b') +ax.pcolormesh(data, cmap="magma", norm="log", colorbar="b") # %% [raw] raw_mimetype="text/restructuredtext" @@ -391,16 +381,19 @@ # Linear segmented norm fig, axs = pplt.subplots(ncols=2, refwidth=2.4) -fig.format(suptitle='Segmented normalizer demo') +fig.format(suptitle="Segmented normalizer demo") ticks = [5, 10, 20, 50, 100, 200, 500, 1000] -for ax, norm in zip(axs, ('linear', 'segmented')): +for ax, norm in zip(axs, ("linear", "segmented")): m = ax.contourf( - data, levels=ticks, extend='both', - cmap='Mako', norm=norm, - colorbar='b', colorbar_kw={'ticks': ticks}, + data, + levels=ticks, + extend="both", + cmap="Mako", + norm=norm, + colorbar="b", + colorbar_kw={"ticks": ticks}, ) - ax.format(title=norm.title() + ' normalizer') - + ax.format(title=norm.title() + " normalizer") # %% import proplot as pplt import numpy as np @@ -411,21 +404,23 @@ data2 = (state.rand(20, 20) - 0.515).cumsum(axis=0).cumsum(axis=1) # Figure -fig, axs = pplt.subplots(nrows=2, ncols=2, refwidth=2.2, order='F') -axs.format(suptitle='Diverging normalizer demo') -cmap = pplt.Colormap('DryWet', cut=0.1) +fig, axs = pplt.subplots(nrows=2, ncols=2, refwidth=2.2, order="F") +axs.format(suptitle="Diverging normalizer demo") +cmap = pplt.Colormap("DryWet", cut=0.1) # Diverging norms i = 0 for data, mode, fair in zip( - (data1, data2), ('positive', 'negative'), ('fair', 'unfair'), + (data1, data2), + ("positive", "negative"), + ("fair", "unfair"), ): - for fair in ('fair', 'unfair'): - norm = pplt.Norm('diverging', fair=(fair == 'fair')) + for fair in ("fair", "unfair"): + norm = pplt.Norm("diverging", fair=(fair == "fair")) ax = axs[i] m = ax.contourf(data, cmap=cmap, norm=norm) - ax.colorbar(m, loc='b') - ax.format(title=f'{mode.title()}-skewed + {fair} scaling') + ax.colorbar(m, loc="b") + ax.format(title=f"{mode.title()}-skewed + {fair} scaling") i += 1 # %% [raw] raw_mimetype="text/restructuredtext" @@ -477,17 +472,17 @@ # Figure fig, axs = pplt.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], ref=3, refwidth=2.3) -axs.format(yformatter='none', suptitle='Discrete vs. smooth colormap levels') +axs.format(yformatter="none", suptitle="Discrete vs. smooth colormap levels") # Pcolor -axs[0].pcolor(data, cmap='viridis', colorbar='l') -axs[0].set_title('Pcolor plot\ndiscrete=True (default)') -axs[1].pcolor(data, discrete=False, cmap='viridis', colorbar='r') -axs[1].set_title('Pcolor plot\ndiscrete=False') +axs[0].pcolor(data, cmap="viridis", colorbar="l") +axs[0].set_title("Pcolor plot\ndiscrete=True (default)") +axs[1].pcolor(data, discrete=False, cmap="viridis", colorbar="r") +axs[1].set_title("Pcolor plot\ndiscrete=False") # Imshow -m = axs[2].imshow(data, cmap='oslo', colorbar='b') -axs[2].format(title='Imshow plot\ndiscrete=False (default)', yformatter='auto') +m = axs[2].imshow(data, cmap="oslo", colorbar="b") +axs[2].format(title="Imshow plot\ndiscrete=False (default)", yformatter="auto") # %% import proplot as pplt @@ -501,22 +496,29 @@ # Figure gs = pplt.GridSpec(nrows=2, ncols=4, hratios=(1.5, 1)) fig = pplt.figure(refwidth=2.4, right=2) -fig.format(suptitle='DiscreteNorm end-color standardization') +fig.format(suptitle="DiscreteNorm end-color standardization") # Cyclic colorbar with distinct end colors -cmap = pplt.Colormap('twilight', shift=-90) +cmap = pplt.Colormap("twilight", shift=-90) ax = fig.subplot(gs[0, 1:3], title='distinct "cyclic" end colors') ax.pcolormesh( - data, cmap=cmap, levels=levels, - colorbar='b', colorbar_kw={'locator': 90}, + data, + cmap=cmap, + levels=levels, + colorbar="b", + colorbar_kw={"locator": 90}, ) # Colorbars with different extend values -for i, extend in enumerate(('min', 'max', 'neither', 'both')): - ax = fig.subplot(gs[1, i], title=f'extend={extend!r}') +for i, extend in enumerate(("min", "max", "neither", "both")): + ax = fig.subplot(gs[1, i], title=f"extend={extend!r}") ax.pcolormesh( - data[:, :10], levels=levels, cmap='oxy', - extend=extend, colorbar='b', colorbar_kw={'locator': 180} + data[:, :10], + levels=levels, + cmap="oxy", + extend=extend, + colorbar="b", + colorbar_kw={"locator": 180}, ) # %% [raw] raw_mimetype="text/restructuredtext" tags=[] @@ -560,28 +562,28 @@ # %% import proplot as pplt import numpy as np + N = 20 state = np.random.RandomState(51423) data = N * 2 + (state.rand(N, N) - 0.45).cumsum(axis=0).cumsum(axis=1) * 10 fig, axs = pplt.subplots( - nrows=2, ncols=2, refwidth=2, - suptitle='Auto normalization demo' + nrows=2, ncols=2, refwidth=2, suptitle="Auto normalization demo" ) # Auto diverging -pplt.rc['cmap.sequential'] = 'lapaz_r' -pplt.rc['cmap.diverging'] = 'vik' +pplt.rc["cmap.sequential"] = "lapaz_r" +pplt.rc["cmap.diverging"] = "vik" for i, ax in enumerate(axs[:2]): - ax.pcolor(data - i * N * 6, colorbar='b') - ax.format(title='Diverging ' + ('on' if i else 'off')) + ax.pcolor(data - i * N * 6, colorbar="b") + ax.format(title="Diverging " + ("on" if i else "off")) # Auto range -pplt.rc['cmap.sequential'] = 'lajolla' +pplt.rc["cmap.sequential"] = "lajolla" data = data[::-1, :] data[-1, 0] = 2e3 for i, ax in enumerate(axs[2:]): - ax.pcolor(data, robust=bool(i), colorbar='b') - ax.format(title='Robust ' + ('on' if i else 'off')) + ax.pcolor(data, robust=bool(i), colorbar="b") + ax.format(title="Robust " + ("on" if i else "off")) pplt.rc.reset() # %% [raw] raw_mimetype="text/restructuredtext" @@ -610,39 +612,40 @@ # Sample data state = np.random.RandomState(51423) data = state.rand(6, 6) -data = pd.DataFrame(data, index=pd.Index(['a', 'b', 'c', 'd', 'e', 'f'])) +data = pd.DataFrame(data, index=pd.Index(["a", "b", "c", "d", "e", "f"])) # Figure fig, axs = pplt.subplots( [[1, 1, 2, 2], [0, 3, 3, 0]], - refwidth=2.3, share='labels', span=False, + refwidth=2.3, + share="labels", + span=False, ) -axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Labels demo') +axs.format(xlabel="xlabel", ylabel="ylabel", suptitle="Labels demo") # Heatmap with labeled boxes ax = axs[0] m = ax.heatmap( - data, cmap='rocket', - labels=True, precision=2, labels_kw={'weight': 'bold'} + data, + cmap="rocket", + labels=True, + precision=2, + labels_kw={"weight": "bold"}, ) -ax.format(title='Heatmap with labels') +ax.format(title="Heatmap with labels") # Filled contours with labels ax = axs[1] m = ax.contourf( - data.cumsum(axis=0), cmap='rocket', - labels=True, labels_kw={'weight': 'bold'} + data.cumsum(axis=0), cmap="rocket", labels=True, labels_kw={"weight": "bold"} ) -ax.format(title='Filled contours with labels') +ax.format(title="Filled contours with labels") # Line contours with labels and no zero level data = 5 * (data - 0.45).cumsum(axis=0) - 2 ax = axs[2] -ax.contour( - data, nozero=True, color='gray8', - labels=True, labels_kw={'weight': 'bold'} -) -ax.format(title='Line contours with labels') +ax.contour(data, nozero=True, color="gray8", labels=True, labels_kw={"weight": "bold"}) +ax.format(title="Line contours with labels") # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_heatmap: @@ -670,17 +673,31 @@ data = (data - data.mean(axis=0)) / data.std(axis=0) data = (data.T @ data) / data.shape[0] data[np.tril_indices(data.shape[0], -1)] = np.nan # fill half with empty boxes -data = pd.DataFrame(data, columns=list('abcdefghij'), index=list('abcdefghij')) +data = pd.DataFrame(data, columns=list("abcdefghij"), index=list("abcdefghij")) # Covariance matrix plot fig, ax = pplt.subplots(refwidth=4.5) m = ax.heatmap( - data, cmap='ColdHot', vmin=-1, vmax=1, N=100, lw=0.5, ec='k', - labels=True, precision=2, labels_kw={'weight': 'bold'}, + data, + cmap="ColdHot", + vmin=-1, + vmax=1, + N=100, + lw=0.5, + ec="k", + labels=True, + precision=2, + labels_kw={"weight": "bold"}, clip_on=False, # turn off clipping so box edges are not cut in half ) ax.format( - suptitle='Heatmap demo', title='Table of correlation coefficients', - xloc='top', yloc='right', yreverse=True, ticklabelweight='bold', - alpha=0, linewidth=0, tickpad=4, + suptitle="Heatmap demo", + title="Table of correlation coefficients", + xloc="top", + yloc="right", + yreverse=True, + ticklabelweight="bold", + alpha=0, + linewidth=0, + tickpad=4, ) diff --git a/docs/basics.py b/docs/basics.py index 57b7f3b63..bc6404f5e 100644 --- a/docs/basics.py +++ b/docs/basics.py @@ -78,9 +78,10 @@ # Simple subplot import numpy as np import proplot as pplt + state = np.random.RandomState(51423) data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) -fig, ax = pplt.subplot(suptitle='Single subplot', xlabel='x axis', ylabel='y axis') +fig, ax = pplt.subplot(suptitle="Single subplot", xlabel="x axis", ylabel="y axis") # fig = pplt.figure(suptitle='Single subplot') # equivalent to above # ax = fig.subplot(xlabel='x axis', ylabel='y axis') ax.plot(data, lw=2) @@ -145,6 +146,7 @@ # Simple subplot grid import numpy as np import proplot as pplt + state = np.random.RandomState(51423) data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) fig = pplt.figure() @@ -152,8 +154,7 @@ ax.plot(data, lw=2) ax = fig.subplot(122) fig.format( - suptitle='Simple subplot grid', title='Title', - xlabel='x axis', ylabel='y axis' + suptitle="Simple subplot grid", title="Title", xlabel="x axis", ylabel="y axis" ) # fig.save('~/example1.png') # save the figure # fig.savefig('~/example1.png') # alternative @@ -163,6 +164,7 @@ # Complex grid import numpy as np import proplot as pplt + state = np.random.RandomState(51423) data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) array = [ # the "picture" (0 == nothing, 1 == subplot A, 2 == subplot B, etc.) @@ -172,8 +174,11 @@ fig = pplt.figure(refwidth=1.8) axs = fig.subplots(array) axs.format( - abc=True, abcloc='ul', suptitle='Complex subplot grid', - xlabel='xlabel', ylabel='ylabel' + abc=True, + abcloc="ul", + suptitle="Complex subplot grid", + xlabel="xlabel", + ylabel="ylabel", ) axs[2].plot(data, lw=2) # fig.save('~/example2.png') # save the figure @@ -184,6 +189,7 @@ # Really complex grid import numpy as np import proplot as pplt + state = np.random.RandomState(51423) data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) array = [ # the "picture" (1 == subplot A, 2 == subplot B, etc.) @@ -194,8 +200,7 @@ ] fig, axs = pplt.subplots(array, figwidth=5, span=False) axs.format( - suptitle='Really complex subplot grid', - xlabel='xlabel', ylabel='ylabel', abc=True + suptitle="Really complex subplot grid", xlabel="xlabel", ylabel="ylabel", abc=True ) axs[0].plot(data, lw=2) # fig.save('~/example3.png') # save the figure @@ -205,6 +210,7 @@ # Using a GridSpec import numpy as np import proplot as pplt + state = np.random.RandomState(51423) data = 2 * (state.rand(100, 5) - 0.5).cumsum(axis=0) gs = pplt.GridSpec(nrows=2, ncols=2, pad=1) @@ -214,8 +220,7 @@ ax = fig.subplot(gs[0, 1]) ax = fig.subplot(gs[1, 1]) fig.format( - suptitle='Subplot grid with a GridSpec', - xlabel='xlabel', ylabel='ylabel', abc=True + suptitle="Subplot grid with a GridSpec", xlabel="xlabel", ylabel="ylabel", abc=True ) # fig.save('~/example4.png') # save the figure # fig.savefig('~/example4.png') # alternative @@ -265,29 +270,30 @@ # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) # Selected subplots in a simple grid fig, axs = pplt.subplots(ncols=4, nrows=4, refwidth=1.2, span=True) -axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Simple SubplotGrid') +axs.format(xlabel="xlabel", ylabel="ylabel", suptitle="Simple SubplotGrid") axs.format(grid=False, xlim=(0, 50), ylim=(-4, 4)) -axs[:, 0].format(facecolor='blush', edgecolor='gray7', linewidth=1) # eauivalent -axs[:, 0].format(fc='blush', ec='gray7', lw=1) -axs[0, :].format(fc='sky blue', ec='gray7', lw=1) -axs[0].format(ec='black', fc='gray5', lw=1.4) -axs[1:, 1:].format(fc='gray1') +axs[:, 0].format(facecolor="blush", edgecolor="gray7", linewidth=1) # eauivalent +axs[:, 0].format(fc="blush", ec="gray7", lw=1) +axs[0, :].format(fc="sky blue", ec="gray7", lw=1) +axs[0].format(ec="black", fc="gray5", lw=1.4) +axs[1:, 1:].format(fc="gray1") for ax in axs[1:, 1:]: - ax.plot((state.rand(50, 5) - 0.5).cumsum(axis=0), cycle='Grays', lw=2) + ax.plot((state.rand(50, 5) - 0.5).cumsum(axis=0), cycle="Grays", lw=2) # Selected subplots in a complex grid fig = pplt.figure(refwidth=1, refnum=5, span=False) axs = fig.subplots([[1, 1, 2], [3, 4, 2], [3, 4, 5]], hratios=[2.2, 1, 1]) -axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Complex SubplotGrid') -axs[0].format(ec='black', fc='gray1', lw=1.4) -axs[1, 1:].format(fc='blush') -axs[1, :1].format(fc='sky blue') -axs[-1, -1].format(fc='gray4', grid=False) -axs[0].plot((state.rand(50, 10) - 0.5).cumsum(axis=0), cycle='Grays_r', lw=2) +axs.format(xlabel="xlabel", ylabel="ylabel", suptitle="Complex SubplotGrid") +axs[0].format(ec="black", fc="gray1", lw=1.4) +axs[1, 1:].format(fc="blush") +axs[1, :1].format(fc="sky blue") +axs[-1, -1].format(fc="gray4", grid=False) +axs[0].plot((state.rand(50, 10) - 0.5).cumsum(axis=0), cycle="Grays_r", lw=2) # %% [raw] raw_mimetype="text/restructuredtext" @@ -332,17 +338,21 @@ data = N + (state.rand(N, N) - 0.55).cumsum(axis=0).cumsum(axis=1) # Example plots -cycle = pplt.Cycle('greys', left=0.2, N=5) +cycle = pplt.Cycle("greys", left=0.2, N=5) fig, axs = pplt.subplots(ncols=2, nrows=2, figwidth=5, share=False) -axs[0].plot(data[:, :5], linewidth=2, linestyle='--', cycle=cycle) -axs[1].scatter(data[:, :5], marker='x', cycle=cycle) -axs[2].pcolormesh(data, cmap='greys') -m = axs[3].contourf(data, cmap='greys') +axs[0].plot(data[:, :5], linewidth=2, linestyle="--", cycle=cycle) +axs[1].scatter(data[:, :5], marker="x", cycle=cycle) +axs[2].pcolormesh(data, cmap="greys") +m = axs[3].contourf(data, cmap="greys") axs.format( - abc='a.', titleloc='l', title='Title', - xlabel='xlabel', ylabel='ylabel', suptitle='Quick plotting demo' + abc="a.", + titleloc="l", + title="Title", + xlabel="xlabel", + ylabel="ylabel", + suptitle="Quick plotting demo", ) -fig.colorbar(m, loc='b', label='label') +fig.colorbar(m, loc="b", label="label") # %% [raw] raw_mimetype="text/restructuredtext" @@ -419,6 +429,7 @@ # %% import proplot as pplt import numpy as np + fig, axs = pplt.subplots(ncols=2, nrows=2, refwidth=2, share=False) state = np.random.RandomState(51423) N = 60 @@ -426,19 +437,31 @@ y = (state.rand(N, 5) - 0.5).cumsum(axis=0) axs[0].plot(x, y, linewidth=1.5) axs.format( - suptitle='Format command demo', - abc='A.', abcloc='ul', - title='Main', ltitle='Left', rtitle='Right', # different titles - ultitle='Title 1', urtitle='Title 2', lltitle='Title 3', lrtitle='Title 4', - toplabels=('Column 1', 'Column 2'), - leftlabels=('Row 1', 'Row 2'), - xlabel='xaxis', ylabel='yaxis', - xscale='log', - xlim=(1, 10), xticks=1, - ylim=(-3, 3), yticks=pplt.arange(-3, 3), - yticklabels=('a', 'bb', 'c', 'dd', 'e', 'ff', 'g'), - ytickloc='both', yticklabelloc='both', - xtickdir='inout', xtickminor=False, ygridminor=True, + suptitle="Format command demo", + abc="A.", + abcloc="ul", + title="Main", + ltitle="Left", + rtitle="Right", # different titles + ultitle="Title 1", + urtitle="Title 2", + lltitle="Title 3", + lrtitle="Title 4", + toplabels=("Column 1", "Column 2"), + leftlabels=("Row 1", "Row 2"), + xlabel="xaxis", + ylabel="yaxis", + xscale="log", + xlim=(1, 10), + xticks=1, + ylim=(-3, 3), + yticks=pplt.arange(-3, 3), + yticklabels=("a", "bb", "c", "dd", "e", "ff", "g"), + ytickloc="both", + yticklabelloc="both", + xtickdir="inout", + xtickminor=False, + ygridminor=True, ) # %% [raw] raw_mimetype="text/restructuredtext" @@ -473,34 +496,39 @@ import numpy as np # Update global settings in several different ways -pplt.rc.metacolor = 'gray6' -pplt.rc.update({'fontname': 'Source Sans Pro', 'fontsize': 11}) -pplt.rc['figure.facecolor'] = 'gray3' -pplt.rc.axesfacecolor = 'gray4' +pplt.rc.metacolor = "gray6" +pplt.rc.update({"fontname": "Source Sans Pro", "fontsize": 11}) +pplt.rc["figure.facecolor"] = "gray3" +pplt.rc.axesfacecolor = "gray4" # pplt.rc.save() # save the current settings to ~/.proplotrc # Apply settings to figure with context() -with pplt.rc.context({'suptitle.size': 13}, toplabelcolor='gray6', metawidth=1.5): - fig = pplt.figure(figwidth=6, sharey='limits', span=False) +with pplt.rc.context({"suptitle.size": 13}, toplabelcolor="gray6", metawidth=1.5): + fig = pplt.figure(figwidth=6, sharey="limits", span=False) axs = fig.subplots(ncols=2) # Plot lines with a custom cycler N, M = 100, 7 state = np.random.RandomState(51423) values = np.arange(1, M + 1) -cycle = pplt.get_colors('grays', M - 1) + ['red'] +cycle = pplt.get_colors("grays", M - 1) + ["red"] for i, ax in enumerate(axs): data = np.cumsum(state.rand(N, M) - 0.5, axis=0) lines = ax.plot(data, linewidth=3, cycle=cycle) # Apply settings to axes with format() axs.format( - grid=False, xlabel='xlabel', ylabel='ylabel', - toplabels=('Column 1', 'Column 2'), - suptitle='Rc settings demo', - suptitlecolor='gray7', - abc='[A]', abcloc='l', - title='Title', titleloc='r', titlecolor='gray7' + grid=False, + xlabel="xlabel", + ylabel="ylabel", + toplabels=("Column 1", "Column 2"), + suptitle="Rc settings demo", + suptitlecolor="gray7", + abc="[A]", + abcloc="l", + title="Title", + titleloc="r", + titlecolor="gray7", ) # Reset persistent modifications from head of cell @@ -510,6 +538,7 @@ # %% import proplot as pplt import numpy as np + # pplt.rc.style = 'style' # set the style everywhere # Sample data @@ -518,10 +547,10 @@ # Set up figure fig, axs = pplt.subplots(ncols=2, nrows=2, span=False, share=False) -axs.format(suptitle='Stylesheets demo') -styles = ('ggplot', 'seaborn', '538', 'bmh') +axs.format(suptitle="Stylesheets demo") +styles = ("ggplot", "seaborn", "538", "bmh") # Apply different styles to different axes with format() for ax, style in zip(axs, styles): - ax.format(style=style, xlabel='xlabel', ylabel='ylabel', title=style) + ax.format(style=style, xlabel="xlabel", ylabel="ylabel", title=style) ax.plot(data, linewidth=3) diff --git a/docs/cartesian.py b/docs/cartesian.py index eb6640397..b4b6b8785 100644 --- a/docs/cartesian.py +++ b/docs/cartesian.py @@ -54,43 +54,48 @@ # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) pplt.rc.update( - metawidth=1, fontsize=10, - metacolor='dark blue', suptitlecolor='dark blue', - titleloc='upper center', titlecolor='dark blue', titleborder=False, - axesfacecolor=pplt.scale_luminance('powderblue', 1.15), + metawidth=1, + fontsize=10, + metacolor="dark blue", + suptitlecolor="dark blue", + titleloc="upper center", + titlecolor="dark blue", + titleborder=False, + axesfacecolor=pplt.scale_luminance("powderblue", 1.15), ) fig = pplt.figure(share=False, refwidth=5, refaspect=(8, 1)) -fig.format(suptitle='Tick locators demo') +fig.format(suptitle="Tick locators demo") # Step size for tick locations -ax = fig.subplot(711, title='MultipleLocator') +ax = fig.subplot(711, title="MultipleLocator") ax.format(xlim=(0, 200), xminorlocator=10, xlocator=30) # Specific list of locations -ax = fig.subplot(712, title='FixedLocator') +ax = fig.subplot(712, title="FixedLocator") ax.format(xlim=(0, 10), xminorlocator=0.1, xlocator=[0, 0.3, 0.8, 1.6, 4.4, 8, 8.8]) # Ticks at numpy.linspace(xmin, xmax, N) -ax = fig.subplot(713, title='LinearLocator') -ax.format(xlim=(0, 10), xlocator=('linear', 21)) +ax = fig.subplot(713, title="LinearLocator") +ax.format(xlim=(0, 10), xlocator=("linear", 21)) # Logarithmic locator, used automatically for log scale plots -ax = fig.subplot(714, title='LogLocator') -ax.format(xlim=(1, 100), xlocator='log', xminorlocator='logminor') +ax = fig.subplot(714, title="LogLocator") +ax.format(xlim=(1, 100), xlocator="log", xminorlocator="logminor") # Maximum number of ticks, but at "nice" locations -ax = fig.subplot(715, title='MaxNLocator') -ax.format(xlim=(1, 7), xlocator=('maxn', 11)) +ax = fig.subplot(715, title="MaxNLocator") +ax.format(xlim=(1, 7), xlocator=("maxn", 11)) # Hide all ticks -ax = fig.subplot(716, title='NullLocator') -ax.format(xlim=(-10, 10), xlocator='null') +ax = fig.subplot(716, title="NullLocator") +ax.format(xlim=(-10, 10), xlocator="null") # Tick locations that cleanly divide 60 minute/60 second intervals -ax = fig.subplot(717, title='Degree-Minute-Second Locator (requires cartopy)') -ax.format(xlim=(0, 2), xlocator='dms', xformatter='dms') +ax = fig.subplot(717, title="Degree-Minute-Second Locator (requires cartopy)") +ax.format(xlim=(0, 2), xlocator="dms", xformatter="dms") pplt.rc.reset() @@ -130,6 +135,7 @@ # %% import proplot as pplt + pplt.rc.fontsize = 11 pplt.rc.metawidth = 1.5 pplt.rc.gridwidth = 1 @@ -137,26 +143,40 @@ # Create the figure fig, axs = pplt.subplots(ncols=2, nrows=2, refwidth=1.5, share=False) axs.format( - ytickloc='both', yticklabelloc='both', - titlepad='0.5em', suptitle='Default formatters demo' + ytickloc="both", + yticklabelloc="both", + titlepad="0.5em", + suptitle="Default formatters demo", ) # Formatter comparison locator = [0, 0.25, 0.5, 0.75, 1] -axs[0].format(xformatter='scalar', yformatter='scalar', title='Matplotlib formatter') -axs[1].format(title='Proplot formatter') +axs[0].format(xformatter="scalar", yformatter="scalar", title="Matplotlib formatter") +axs[1].format(title="Proplot formatter") axs[:2].format(xlocator=locator, ylocator=locator) # Limiting the tick range axs[2].format( - title='Omitting tick labels', ticklen=5, xlim=(0, 5), ylim=(0, 5), - xtickrange=(0, 2), ytickrange=(0, 2), xlocator=1, ylocator=1 + title="Omitting tick labels", + ticklen=5, + xlim=(0, 5), + ylim=(0, 5), + xtickrange=(0, 2), + ytickrange=(0, 2), + xlocator=1, + ylocator=1, ) # Setting the wrap range axs[3].format( - title='Wrapping the tick range', ticklen=5, xlim=(0, 7), ylim=(0, 6), - xwraprange=(0, 5), ywraprange=(0, 3), xlocator=1, ylocator=1 + title="Wrapping the tick range", + ticklen=5, + xlim=(0, 7), + ylim=(0, 6), + xwraprange=(0, 5), + ywraprange=(0, 3), + xlocator=1, + ylocator=1, ) pplt.rc.reset() @@ -164,49 +184,59 @@ # %% import proplot as pplt import numpy as np + pplt.rc.update( - metawidth=1.2, fontsize=10, axesfacecolor='gray0', figurefacecolor='gray2', - metacolor='gray8', gridcolor='gray8', titlecolor='gray8', suptitlecolor='gray8', - titleloc='upper center', titleborder=False, + metawidth=1.2, + fontsize=10, + axesfacecolor="gray0", + figurefacecolor="gray2", + metacolor="gray8", + gridcolor="gray8", + titlecolor="gray8", + suptitlecolor="gray8", + titleloc="upper center", + titleborder=False, ) fig = pplt.figure(refwidth=5, refaspect=(8, 1), share=False) # Scientific notation -ax = fig.subplot(911, title='SciFormatter') -ax.format(xlim=(0, 1e20), xformatter='sci') +ax = fig.subplot(911, title="SciFormatter") +ax.format(xlim=(0, 1e20), xformatter="sci") # N significant figures for ticks at specific values -ax = fig.subplot(912, title='SigFigFormatter') +ax = fig.subplot(912, title="SigFigFormatter") ax.format( - xlim=(0, 20), xlocator=(0.0034, 3.233, 9.2, 15.2344, 7.2343, 19.58), - xformatter=('sigfig', 2), # 2 significant digits + xlim=(0, 20), + xlocator=(0.0034, 3.233, 9.2, 15.2344, 7.2343, 19.58), + xformatter=("sigfig", 2), # 2 significant digits ) # Fraction formatters -ax = fig.subplot(913, title='FracFormatter') -ax.format(xlim=(0, 3 * np.pi), xlocator=np.pi / 4, xformatter='pi') -ax = fig.subplot(914, title='FracFormatter') -ax.format(xlim=(0, 2 * np.e), xlocator=np.e / 2, xticklabels='e') +ax = fig.subplot(913, title="FracFormatter") +ax.format(xlim=(0, 3 * np.pi), xlocator=np.pi / 4, xformatter="pi") +ax = fig.subplot(914, title="FracFormatter") +ax.format(xlim=(0, 2 * np.e), xlocator=np.e / 2, xticklabels="e") # Geographic formatters -ax = fig.subplot(915, title='Latitude Formatter') -ax.format(xlim=(-90, 90), xlocator=30, xformatter='deglat') -ax = fig.subplot(916, title='Longitude Formatter') -ax.format(xlim=(0, 360), xlocator=60, xformatter='deglon') +ax = fig.subplot(915, title="Latitude Formatter") +ax.format(xlim=(-90, 90), xlocator=30, xformatter="deglat") +ax = fig.subplot(916, title="Longitude Formatter") +ax.format(xlim=(0, 360), xlocator=60, xformatter="deglon") # User input labels -ax = fig.subplot(917, title='FixedFormatter') +ax = fig.subplot(917, title="FixedFormatter") ax.format( - xlim=(0, 5), xlocator=np.arange(5), - xticklabels=['a', 'b', 'c', 'd', 'e'], + xlim=(0, 5), + xlocator=np.arange(5), + xticklabels=["a", "b", "c", "d", "e"], ) # Custom style labels -ax = fig.subplot(918, title='FormatStrFormatter') -ax.format(xlim=(0, 0.001), xlocator=0.0001, xformatter='%.E') -ax = fig.subplot(919, title='StrMethodFormatter') -ax.format(xlim=(0, 100), xtickminor=False, xlocator=20, xformatter='{x:.1f}') -fig.format(ylocator='null', suptitle='Tick formatters demo') +ax = fig.subplot(918, title="FormatStrFormatter") +ax.format(xlim=(0, 0.001), xlocator=0.0001, xformatter="%.E") +ax = fig.subplot(919, title="StrMethodFormatter") +ax.format(xlim=(0, 100), xtickminor=False, xlocator=20, xformatter="{x:.1f}") +fig.format(ylocator="null", suptitle="Tick formatters demo") pplt.rc.reset() # %% [raw] raw_mimetype="text/restructuredtext" @@ -233,10 +263,15 @@ # %% import proplot as pplt import numpy as np + pplt.rc.update( - metawidth=1.2, fontsize=10, ticklenratio=0.7, - figurefacecolor='w', axesfacecolor='pastel blue', - titleloc='upper center', titleborder=False, + metawidth=1.2, + fontsize=10, + ticklenratio=0.7, + figurefacecolor="w", + axesfacecolor="pastel blue", + titleloc="upper center", + titleborder=False, ) fig, axs = pplt.subplots(nrows=5, refwidth=6, refaspect=(8, 1), share=False) @@ -244,41 +279,48 @@ # This is enabled if you plot datetime data or set datetime limits ax = axs[0] ax.format( - xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-02')), - title='Auto date locator and formatter' + xlim=(np.datetime64("2000-01-01"), np.datetime64("2001-01-02")), + title="Auto date locator and formatter", ) # Concise date formatter introduced in matplotlib 3.1 ax = axs[1] ax.format( - xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-01')), - xformatter='concise', title='Concise date formatter', + xlim=(np.datetime64("2000-01-01"), np.datetime64("2001-01-01")), + xformatter="concise", + title="Concise date formatter", ) # Minor ticks every year, major every 10 years ax = axs[2] ax.format( - xlim=(np.datetime64('2000-01-01'), np.datetime64('2050-01-01')), - xlocator=('year', 10), xformatter='\'%y', title='Ticks every N units', + xlim=(np.datetime64("2000-01-01"), np.datetime64("2050-01-01")), + xlocator=("year", 10), + xformatter="'%y", + title="Ticks every N units", ) # Minor ticks every 10 minutes, major every 2 minutes ax = axs[3] ax.format( - xlim=(np.datetime64('2000-01-01T00:00:00'), np.datetime64('2000-01-01T12:00:00')), - xlocator=('hour', range(0, 24, 2)), xminorlocator=('minute', range(0, 60, 10)), - xformatter='T%H:%M:%S', title='Ticks at specific intervals', + xlim=(np.datetime64("2000-01-01T00:00:00"), np.datetime64("2000-01-01T12:00:00")), + xlocator=("hour", range(0, 24, 2)), + xminorlocator=("minute", range(0, 60, 10)), + xformatter="T%H:%M:%S", + title="Ticks at specific intervals", ) # Month and year labels, with default tick label rotation ax = axs[4] ax.format( - xlim=(np.datetime64('2000-01-01'), np.datetime64('2008-01-01')), - xlocator='year', xminorlocator='month', # minor ticks every month - xformatter='%b %Y', title='Ticks with default rotation', + xlim=(np.datetime64("2000-01-01"), np.datetime64("2008-01-01")), + xlocator="year", + xminorlocator="month", # minor ticks every month + xformatter="%b %Y", + title="Ticks with default rotation", ) axs[:4].format(xrotation=0) # no rotation for the first four examples -fig.format(ylocator='null', suptitle='Datetime locators and formatters demo') +fig.format(ylocator="null", suptitle="Datetime locators and formatters demo") pplt.rc.reset() @@ -306,25 +348,29 @@ # %% import proplot as pplt + pplt.rc.update( - metawidth=1.2, fontsize=10, gridcolor='coral', - axesedgecolor='deep orange', figurefacecolor='white', + metawidth=1.2, + fontsize=10, + gridcolor="coral", + axesedgecolor="deep orange", + figurefacecolor="white", ) -fig = pplt.figure(share=False, refwidth=2, suptitle='Axis locations demo') +fig = pplt.figure(share=False, refwidth=2, suptitle="Axis locations demo") # Spine location demonstration -ax = fig.subplot(121, title='Various locations') -ax.format(xloc='top', xlabel='original axis') -ax.twiny(xloc='bottom', xcolor='black', xlabel='locked twin') -ax.twiny(xloc=('axes', 1.25), xcolor='black', xlabel='offset twin') -ax.twiny(xloc=('axes', -0.25), xcolor='black', xlabel='offset twin') -ax.format(ytickloc='both', yticklabelloc='both') -ax.format(ylabel='labels on both sides') +ax = fig.subplot(121, title="Various locations") +ax.format(xloc="top", xlabel="original axis") +ax.twiny(xloc="bottom", xcolor="black", xlabel="locked twin") +ax.twiny(xloc=("axes", 1.25), xcolor="black", xlabel="offset twin") +ax.twiny(xloc=("axes", -0.25), xcolor="black", xlabel="offset twin") +ax.format(ytickloc="both", yticklabelloc="both") +ax.format(ylabel="labels on both sides") # Other locations locations -ax = fig.subplot(122, title='Zero-centered spines', titlepad='1em') +ax = fig.subplot(122, title="Zero-centered spines", titlepad="1em") ax.format(xlim=(-10, 10), ylim=(-3, 3), yticks=1) -ax.format(xloc='zero', yloc='zero') +ax.format(xloc="zero", yloc="zero") pplt.rc.reset() @@ -373,30 +419,31 @@ # %% import proplot as pplt import numpy as np + N = 200 lw = 3 -pplt.rc.update({'meta.width': 1, 'label.weight': 'bold', 'tick.labelweight': 'bold'}) +pplt.rc.update({"meta.width": 1, "label.weight": "bold", "tick.labelweight": "bold"}) fig = pplt.figure(refwidth=1.8, share=False) # Linear and log scales ax1 = fig.subplot(221) -ax1.format(yscale='linear', ylabel='linear scale') +ax1.format(yscale="linear", ylabel="linear scale") ax2 = fig.subplot(222) -ax2.format(ylim=(1e-3, 1e3), yscale='log', ylabel='log scale') +ax2.format(ylim=(1e-3, 1e3), yscale="log", ylabel="log scale") for ax in (ax1, ax2): ax.plot(np.linspace(0, 1, N), np.linspace(0, 1000, N), lw=lw) # Symlog scale ax = fig.subplot(223) -ax.format(yscale='symlog', ylabel='symlog scale') +ax.format(yscale="symlog", ylabel="symlog scale") ax.plot(np.linspace(0, 1, N), np.linspace(-1000, 1000, N), lw=lw) # Logit scale ax = fig.subplot(224) -ax.format(yscale='logit', ylabel='logit scale') +ax.format(yscale="logit", ylabel="logit scale") ax.plot(np.linspace(0, 1, N), np.linspace(0.01, 0.99, N), lw=lw) -fig.format(suptitle='Axis scales demo', ytickminor=True) +fig.format(suptitle="Axis scales demo", ytickminor=True) pplt.rc.reset() @@ -410,17 +457,17 @@ ys = (np.sin(x), np.cos(x)) state = np.random.RandomState(51423) data = state.rand(len(dy) - 1, len(x) - 1) -colors = ('coral', 'sky blue') -cmap = pplt.Colormap('grays', right=0.8) +colors = ("coral", "sky blue") +cmap = pplt.Colormap("grays", right=0.8) fig, axs = pplt.subplots(nrows=4, refaspect=(5, 1), figwidth=5.5, sharex=False) # Loop through various cutoff scale options -titles = ('Zoom out of left', 'Zoom into left', 'Discrete jump', 'Fast jump') +titles = ("Zoom out of left", "Zoom into left", "Discrete jump", "Fast jump") args = ( (np.pi, 3), # speed up (3 * np.pi, 1 / 3), # slow down (np.pi, np.inf, 3 * np.pi), # discrete jump - (np.pi, 5, 3 * np.pi) # fast jump + (np.pi, 5, 3 * np.pi), # fast jump ) locators = ( np.pi / 3, @@ -433,10 +480,15 @@ for y, color in zip(ys, colors): ax.plot(x, y, lw=4, color=color) ax.format( - xscale=('cutoff', *iargs), xlim=(0, 4 * np.pi), - xlocator=locator, xformatter='pi', xtickminor=False, - ygrid=False, ylabel='wave amplitude', - title=title, suptitle='Cutoff axis scales demo' + # xscale=("cutoff", *iargs), + xlim=(0, 4 * np.pi), + xlocator=locator, + xformatter="pi", + xtickminor=False, + ygrid=False, + ylabel="wave amplitude", + title=title, + suptitle="Cutoff axis scales demo", ) # %% @@ -447,22 +499,25 @@ n = 30 state = np.random.RandomState(51423) data = state.rand(n - 1, n - 1) -colors = ('coral', 'sky blue') -cmap = pplt.Colormap('grays', right=0.8) +colors = ("coral", "sky blue") +cmap = pplt.Colormap("grays", right=0.8) gs = pplt.GridSpec(nrows=2, ncols=2) fig = pplt.figure(refwidth=2.3, share=False) -fig.format(grid=False, suptitle='Other axis scales demo') +fig.format(grid=False, suptitle="Other axis scales demo") # Geographic scales x = np.linspace(-180, 180, n) y = np.linspace(-85, 85, n) -for i, scale in enumerate(('sine', 'mercator')): +for i, scale in enumerate(("sine", "mercator")): ax = fig.subplot(gs[i, 0]) - ax.plot(x, y, '-', color=colors[i], lw=4) - ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8}) + ax.plot(x, y, "-", color=colors[i], lw=4) + ax.pcolormesh(x, y, data, cmap="grays", cmap_kw={"right": 0.8}) ax.format( - yscale=scale, title=scale.title() + ' scale', - ylim=(-85, 85), ylocator=20, yformatter='deg', + yscale=scale, + title=scale.title() + " scale", + ylim=(-85, 85), + ylocator=20, + yformatter="deg", ) # Exponential scale @@ -471,17 +526,17 @@ y = 3 * np.linspace(0, 1, n) data = state.rand(len(y) - 1, len(x) - 1) ax = fig.subplot(gs[0, 1]) -title = 'Exponential $e^x$ scale' -ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8}) +title = "Exponential $e^x$ scale" +ax.pcolormesh(x, y, data, cmap="grays", cmap_kw={"right": 0.8}) ax.plot(x, y, lw=4, color=colors[0]) -ax.format(ymin=0.05, yscale=('exp', np.e), title=title) +ax.format(ymin=0.05, yscale=("exp", np.e), title=title) # Power scale ax = fig.subplot(gs[1, 1]) -title = 'Power $x^{0.5}$ scale' -ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8}) +title = "Power $x^{0.5}$ scale" +ax.pcolormesh(x, y, data, cmap="grays", cmap_kw={"right": 0.8}) ax.plot(x, y, lw=4, color=colors[1]) -ax.format(ymin=0.05, yscale=('power', 0.5), title=title) +ax.format(ymin=0.05, yscale=("power", 0.5), title=title) # %% [raw] raw_mimetype="text/restructuredtext" @@ -518,29 +573,30 @@ # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) -c0 = 'gray5' -c1 = 'red8' -c2 = 'blue8' +c0 = "gray5" +c1 = "red8" +c2 = "blue8" N, M = 50, 10 # Alternate y axis data = state.rand(M) + (state.rand(N, M) - 0.48).cumsum(axis=0) altdata = 5 * (state.rand(N) - 0.45).cumsum(axis=0) fig = pplt.figure(share=False) -ax = fig.subplot(121, title='Alternate y twin x') -ax.line(data, color=c0, ls='--') -ox = ax.alty(color=c2, label='alternate ylabel', linewidth=1) +ax = fig.subplot(121, title="Alternate y twin x") +ax.line(data, color=c0, ls="--") +ox = ax.alty(color=c2, label="alternate ylabel", linewidth=1) ox.line(altdata, color=c2) # Alternate x axis data = state.rand(M) + (state.rand(N, M) - 0.48).cumsum(axis=0) altdata = 5 * (state.rand(N) - 0.45).cumsum(axis=0) -ax = fig.subplot(122, title='Alternate x twin y') -ax.linex(data, color=c0, ls='--') -ox = ax.altx(color=c1, label='alternate xlabel', linewidth=1) +ax = fig.subplot(122, title="Alternate x twin y") +ax.linex(data, color=c0, ls="--") +ox = ax.altx(color=c1, label="alternate xlabel", linewidth=1) ox.linex(altdata, color=c1) -fig.format(xlabel='xlabel', ylabel='ylabel', suptitle='Alternate axes demo') +fig.format(xlabel="xlabel", ylabel="ylabel", suptitle="Alternate axes demo") # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_dual: @@ -571,95 +627,116 @@ # %% import proplot as pplt -pplt.rc.update({'grid.alpha': 0.4, 'meta.width': 1, 'grid.linewidth': 1}) -c1 = pplt.scale_luminance('cerulean', 0.5) -c2 = pplt.scale_luminance('red', 0.5) + +pplt.rc.update({"grid.alpha": 0.4, "meta.width": 1, "grid.linewidth": 1}) +c1 = pplt.scale_luminance("cerulean", 0.5) +c2 = pplt.scale_luminance("red", 0.5) fig = pplt.figure(refaspect=2.2, refwidth=3, share=False) axs = fig.subplots( [[1, 1, 2, 2], [0, 3, 3, 0]], - suptitle='Duplicate axes with simple transformations', - ylocator=[], yformatter=[], xcolor=c1, gridcolor=c1, + suptitle="Duplicate axes with simple transformations", + ylocator=[], + yformatter=[], + xcolor=c1, + gridcolor=c1, ) # Meters and kilometers ax = axs[0] -ax.format(xlim=(0, 5000), xlabel='meters') -ax.dualx( - lambda x: x * 1e-3, - label='kilometers', grid=True, color=c2, gridcolor=c2 -) +ax.format(xlim=(0, 5000), xlabel="meters") +ax.dualx(lambda x: x * 1e-3, label="kilometers", grid=True, color=c2, gridcolor=c2) # Kelvin and Celsius ax = axs[1] -ax.format(xlim=(200, 300), xlabel='temperature (K)') +ax.format(xlim=(200, 300), xlabel="temperature (K)") ax.dualx( lambda x: x - 273.15, - label='temperature (\N{DEGREE SIGN}C)', grid=True, color=c2, gridcolor=c2 + label="temperature (\N{DEGREE SIGN}C)", + grid=True, + color=c2, + gridcolor=c2, ) # With symlog parent ax = axs[2] -ax.format(xlim=(-100, 100), xscale='symlog', xlabel='MegaJoules') +ax.format(xlim=(-100, 100), xscale="symlog", xlabel="MegaJoules") ax.dualx( lambda x: x * 1e6, - label='Joules', formatter='log', grid=True, color=c2, gridcolor=c2 + label="Joules", + formatter="log", + grid=True, + color=c2, + gridcolor=c2, ) pplt.rc.reset() # %% import proplot as pplt -pplt.rc.update({'grid.alpha': 0.4, 'meta.width': 1, 'grid.linewidth': 1}) -c1 = pplt.scale_luminance('cerulean', 0.5) -c2 = pplt.scale_luminance('red', 0.5) + +pplt.rc.update({"grid.alpha": 0.4, "meta.width": 1, "grid.linewidth": 1}) +c1 = pplt.scale_luminance("cerulean", 0.5) +c2 = pplt.scale_luminance("red", 0.5) fig = pplt.figure( - share=False, refaspect=0.4, refwidth=1.8, - suptitle='Duplicate axes with pressure and height' + share=False, + refaspect=0.4, + refwidth=1.8, + suptitle="Duplicate axes with pressure and height", ) # Pressure as the linear scale, height on opposite axis (scale height 7km) ax = fig.subplot(121) ax.format( - xformatter='null', ylabel='pressure (hPa)', - ylim=(1000, 10), xlocator=[], ycolor=c1, gridcolor=c1 -) -ax.dualy( - 'height', label='height (km)', ticks=2.5, color=c2, gridcolor=c2, grid=True + xformatter="null", + ylabel="pressure (hPa)", + ylim=(1000, 10), + xlocator=[], + ycolor=c1, + gridcolor=c1, ) +ax.dualy("height", label="height (km)", ticks=2.5, color=c2, gridcolor=c2, grid=True) # Height as the linear scale, pressure on opposite axis (scale height 7km) ax = fig.subplot(122) ax.format( - xformatter='null', ylabel='height (km)', ylim=(0, 20), xlocator='null', - grid=True, gridcolor=c2, ycolor=c2 + xformatter="null", + ylabel="height (km)", + ylim=(0, 20), + xlocator="null", + grid=True, + gridcolor=c2, + ycolor=c2, ) ax.dualy( - 'pressure', label='pressure (hPa)', locator=100, color=c1, gridcolor=c1, grid=True + "pressure", label="pressure (hPa)", locator=100, color=c1, gridcolor=c1, grid=True ) pplt.rc.reset() # %% import proplot as pplt import numpy as np + pplt.rc.margin = 0 -c1 = pplt.scale_luminance('cerulean', 0.5) -c2 = pplt.scale_luminance('red', 0.5) +c1 = pplt.scale_luminance("cerulean", 0.5) +c2 = pplt.scale_luminance("red", 0.5) fig, ax = pplt.subplots(refaspect=(3, 1), figwidth=6) # Sample data cutoff = 1 / 5 x = np.linspace(0.01, 0.5, 1000) # in wavenumber days response = (np.tanh(-((x - cutoff) / 0.03)) + 1) / 2 # response func -ax.axvline(cutoff, lw=2, ls='-', color=c2) +ax.axvline(cutoff, lw=2, ls="-", color=c2) ax.fill_between([cutoff - 0.03, cutoff + 0.03], 0, 1, color=c2, alpha=0.3) ax.plot(x, response, color=c1, lw=2) # Add inverse scale to top ax.format( - title='Imaginary response function', - suptitle='Duplicate axes with wavenumber and period', - xlabel='wavenumber (days$^{-1}$)', ylabel='response', grid=False, + title="Imaginary response function", + suptitle="Duplicate axes with wavenumber and period", + xlabel="wavenumber (days$^{-1}$)", + ylabel="response", + grid=False, ) ax = ax.dualx( - 'inverse', locator='log', locator_kw={'subs': (1, 2, 5)}, label='period (days)' + "inverse", locator="log", locator_kw={"subs": (1, 2, 5)}, label="period (days)" ) pplt.rc.reset() diff --git a/docs/colorbars_legends.py b/docs/colorbars_legends.py index a29be7040..2d955e1af 100644 --- a/docs/colorbars_legends.py +++ b/docs/colorbars_legends.py @@ -80,27 +80,30 @@ # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) fig = pplt.figure(share=False, refwidth=2.3) # Colorbars -ax = fig.subplot(121, title='Axes colorbars') +ax = fig.subplot(121, title="Axes colorbars") data = state.rand(10, 10) -m = ax.heatmap(data, cmap='dusk') -ax.colorbar(m, loc='r') -ax.colorbar(m, loc='t') # title is automatically adjusted -ax.colorbar(m, loc='ll', label='colorbar label') # inset colorbar demonstration +m = ax.heatmap(data, cmap="dusk") +ax.colorbar(m, loc="r") +ax.colorbar(m, loc="t") # title is automatically adjusted +ax.colorbar(m, loc="ll", label="colorbar label") # inset colorbar demonstration # Legends -ax = fig.subplot(122, title='Axes legends', titlepad='0em') +ax = fig.subplot(122, title="Axes legends", titlepad="0em") data = (state.rand(10, 5) - 0.5).cumsum(axis=0) -hs = ax.plot(data, lw=3, cycle='ggplot', labels=list('abcde')) -ax.legend(loc='ll', label='legend label') # automatically infer handles and labels -ax.legend(hs, loc='t', ncols=5, frame=False) # automatically infer labels from handles -ax.legend(hs, list('jklmn'), loc='r', ncols=1, frame=False) # manually override labels +hs = ax.plot(data, lw=3, cycle="ggplot", labels=list("abcde")) +ax.legend(loc="ll", label="legend label") # automatically infer handles and labels +ax.legend(hs, loc="t", ncols=5, frame=False) # automatically infer labels from handles +ax.legend(hs, list("jklmn"), loc="r", ncols=1, frame=False) # manually override labels fig.format( - abc=True, xlabel='xlabel', ylabel='ylabel', - suptitle='Colorbar and legend location demo' + abc=True, + xlabel="xlabel", + ylabel="ylabel", + suptitle="Colorbar and legend location demo", ) # %% [raw] raw_mimetype="text/restructuredtext" @@ -132,60 +135,80 @@ # %% import proplot as pplt -labels = list('xyzpq') + +labels = list("xyzpq") state = np.random.RandomState(51423) -fig = pplt.figure(share=0, refwidth=2.3, suptitle='On-the-fly colorbar and legend demo') +fig = pplt.figure(share=0, refwidth=2.3, suptitle="On-the-fly colorbar and legend demo") # Legends data = (state.rand(30, 10) - 0.5).cumsum(axis=0) -ax = fig.subplot(121, title='On-the-fly legend') +ax = fig.subplot(121, title="On-the-fly legend") ax.plot( # add all at once - data[:, :5], lw=2, cycle='Reds1', cycle_kw={'ls': ('-', '--'), 'left': 0.1}, - labels=labels, legend='b', legend_kw={'title': 'legend title'} + data[:, :5], + lw=2, + cycle="Reds1", + cycle_kw={"ls": ("-", "--"), "left": 0.1}, + labels=labels, + legend="b", + legend_kw={"title": "legend title"}, ) for i in range(5): ax.plot( # add one-by-one - data[:, 5 + i], label=labels[i], linewidth=2, - cycle='Blues1', cycle_kw={'N': 5, 'ls': ('-', '--'), 'left': 0.1}, - colorbar='ul', colorbar_kw={'label': 'colorbar from lines'} + data[:, 5 + i], + label=labels[i], + linewidth=2, + cycle="Blues1", + cycle_kw={"N": 5, "ls": ("-", "--"), "left": 0.1}, + colorbar="ul", + colorbar_kw={"label": "colorbar from lines"}, ) # Colorbars -ax = fig.subplot(122, title='On-the-fly colorbar') +ax = fig.subplot(122, title="On-the-fly colorbar") data = state.rand(8, 8) ax.contourf( - data, cmap='Reds1', extend='both', colorbar='b', - colorbar_kw={'length': 0.8, 'label': 'colorbar label'}, + data, + cmap="Reds1", + extend="both", + colorbar="b", + colorbar_kw={"length": 0.8, "label": "colorbar label"}, ) ax.contour( - data, color='gray7', lw=1.5, - label='contour', legend='ul', legend_kw={'label': 'legend from contours'}, + data, + color="gray7", + lw=1.5, + label="contour", + legend="ul", + legend_kw={"label": "legend from contours"}, ) # %% import proplot as pplt import numpy as np + N = 10 state = np.random.RandomState(51423) fig, axs = pplt.subplots( - nrows=2, share=False, - refwidth='55mm', panelpad='1em', - suptitle='Stacked colorbars demo' + nrows=2, + share=False, + refwidth="55mm", + panelpad="1em", + suptitle="Stacked colorbars demo", ) # Repeat for both axes -args1 = (0, 0.5, 1, 1, 'grays', 0.5) -args2 = (0, 0, 0.5, 0.5, 'reds', 1) -args3 = (0.5, 0, 1, 0.5, 'blues', 2) +args1 = (0, 0.5, 1, 1, "grays", 0.5) +args2 = (0, 0, 0.5, 0.5, "reds", 1) +args3 = (0.5, 0, 1, 0.5, "blues", 2) for j, ax in enumerate(axs): - ax.format(xlabel='data', xlocator=np.linspace(0, 0.8, 5), title=f'Subplot #{j+1}') + ax.format(xlabel="data", xlocator=np.linspace(0, 0.8, 5), title=f"Subplot #{j+1}") for i, (x0, y0, x1, y1, cmap, scale) in enumerate((args1, args2, args3)): if j == 1 and i == 0: continue data = state.rand(N, N) * scale x, y = np.linspace(x0, x1, N + 1), np.linspace(y0, y1, N + 1) m = ax.pcolormesh(x, y, data, cmap=cmap, levels=np.linspace(0, scale, 11)) - ax.colorbar(m, loc='l', label=f'dataset #{i + 1}') + ax.colorbar(m, loc="l", label=f"dataset #{i + 1}") # %% [raw] raw_mimetype="text/restructuredtext" @@ -212,50 +235,54 @@ # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) fig, axs = pplt.subplots(ncols=3, nrows=3, refwidth=1.4) for ax in axs: m = ax.pcolormesh( - state.rand(20, 20), cmap='grays', - levels=np.linspace(0, 1, 11), extend='both' + state.rand(20, 20), cmap="grays", levels=np.linspace(0, 1, 11), extend="both" ) fig.format( - suptitle='Figure colorbars and legends demo', - abc='a.', abcloc='l', xlabel='xlabel', ylabel='ylabel' + suptitle="Figure colorbars and legends demo", + abc="a.", + abcloc="l", + xlabel="xlabel", + ylabel="ylabel", ) -fig.colorbar(m, label='column 1', ticks=0.5, loc='b', col=1) -fig.colorbar(m, label='columns 2 and 3', ticks=0.2, loc='b', cols=(2, 3)) -fig.colorbar(m, label='stacked colorbar', ticks=0.1, loc='b', minorticks=0.05) -fig.colorbar(m, label='colorbar with length <1', ticks=0.1, loc='r', length=0.7) +fig.colorbar(m, label="column 1", ticks=0.5, loc="b", col=1) +fig.colorbar(m, label="columns 2 and 3", ticks=0.2, loc="b", cols=(2, 3)) +fig.colorbar(m, label="stacked colorbar", ticks=0.1, loc="b", minorticks=0.05) +fig.colorbar(m, label="colorbar with length <1", ticks=0.1, loc="r", length=0.7) # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) fig, axs = pplt.subplots( - ncols=2, nrows=2, order='F', refwidth=1.7, wspace=2.5, share=False + ncols=2, nrows=2, order="F", refwidth=1.7, wspace=2.5, share=False ) # Plot data data = (state.rand(50, 50) - 0.1).cumsum(axis=0) for ax in axs[:2]: - m = ax.contourf(data, cmap='grays', extend='both') + m = ax.contourf(data, cmap="grays", extend="both") hs = [] -colors = pplt.get_colors('grays', 5) -for abc, color in zip('ABCDEF', colors): +colors = pplt.get_colors("grays", 5) +for abc, color in zip("ABCDEF", colors): data = state.rand(10) for ax in axs[2:]: - h, = ax.plot(data, color=color, lw=3, label=f'line {abc}') + (h,) = ax.plot(data, color=color, lw=3, label=f"line {abc}") hs.append(h) # Add colorbars and legends -fig.colorbar(m, length=0.8, label='colorbar label', loc='b', col=1, locator=5) -fig.colorbar(m, label='colorbar label', loc='l') -fig.legend(hs, ncols=2, center=True, frame=False, loc='b', col=2) -fig.legend(hs, ncols=1, label='legend label', frame=False, loc='r') -fig.format(abc='A', abcloc='ul', suptitle='Figure colorbars and legends demo') -for ax, title in zip(axs, ('2D {} #1', '2D {} #2', 'Line {} #1', 'Line {} #2')): - ax.format(xlabel='xlabel', title=title.format('dataset')) +fig.colorbar(m, length=0.8, label="colorbar label", loc="b", col=1, locator=5) +fig.colorbar(m, label="colorbar label", loc="l") +fig.legend(hs, ncols=2, center=True, frame=False, loc="b", col=2) +fig.legend(hs, ncols=1, label="legend label", frame=False, loc="r") +fig.format(abc="A", abcloc="ul", suptitle="Figure colorbars and legends demo") +for ax, title in zip(axs, ("2D {} #1", "2D {} #2", "Line {} #1", "Line {} #2")): + ax.format(xlabel="xlabel", title=title.format("dataset")) # %% [raw] raw_mimetype="text/restructuredtext" @@ -308,39 +335,42 @@ # %% import proplot as pplt import numpy as np + fig = pplt.figure(share=False, refwidth=2) # Colorbars from lines ax = fig.subplot(121) state = np.random.RandomState(51423) data = 1 + (state.rand(12, 10) - 0.45).cumsum(axis=0) -cycle = pplt.Cycle('algae') +cycle = pplt.Cycle("algae") hs = ax.line( - data, lw=4, cycle=cycle, colorbar='lr', - colorbar_kw={'length': '8em', 'label': 'line colorbar'} -) -ax.colorbar( - hs, loc='t', values=np.arange(0, 10), - label='line colorbar', ticks=2 + data, + lw=4, + cycle=cycle, + colorbar="lr", + colorbar_kw={"length": "8em", "label": "line colorbar"}, ) +ax.colorbar(hs, loc="t", values=np.arange(0, 10), label="line colorbar", ticks=2) # Colorbars from a mappable ax = fig.subplot(122) -m = ax.contourf( - data.T, extend='both', cmap='algae', - levels=pplt.arange(0, 3, 0.5) -) +m = ax.contourf(data.T, extend="both", cmap="algae", levels=pplt.arange(0, 3, 0.5)) fig.colorbar( - m, loc='r', length=1, # length is relative - label='interior ticks', tickloc='left' + m, loc="r", length=1, label="interior ticks", tickloc="left" # length is relative ) ax.colorbar( - m, loc='ul', length=6, # length is em widths - label='inset colorbar', tickminor=True, alpha=0.5, + m, + loc="ul", + length=6, # length is em widths + label="inset colorbar", + tickminor=True, + alpha=0.5, ) fig.format( - suptitle='Colorbar formatting demo', - xlabel='xlabel', ylabel='ylabel', titleabove=False + suptitle="Colorbar formatting demo", + xlabel="xlabel", + ylabel="ylabel", + titleabove=False, ) @@ -400,9 +430,10 @@ # %% import proplot as pplt import numpy as np -pplt.rc.cycle = '538' -fig, axs = pplt.subplots(ncols=2, span=False, share='labels', refwidth=2.3) -labels = ['a', 'bb', 'ccc', 'dddd', 'eeeee'] + +pplt.rc.cycle = "538" +fig, axs = pplt.subplots(ncols=2, span=False, share="labels", refwidth=2.3) +labels = ["a", "bb", "ccc", "dddd", "eeeee"] hs1, hs2 = [], [] # On-the-fly legends @@ -410,19 +441,26 @@ for i, label in enumerate(labels): data = (state.rand(20) - 0.45).cumsum(axis=0) h1 = axs[0].plot( - data, lw=4, label=label, legend='ul', - legend_kw={'order': 'F', 'title': 'column major'} + data, + lw=4, + label=label, + legend="ul", + legend_kw={"order": "F", "title": "column major"}, ) hs1.extend(h1) h2 = axs[1].plot( - data, lw=4, cycle='Set3', label=label, legend='r', - legend_kw={'lw': 8, 'ncols': 1, 'frame': False, 'title': 'modified\n handles'} + data, + lw=4, + cycle="Set3", + label=label, + legend="r", + legend_kw={"lw": 8, "ncols": 1, "frame": False, "title": "modified\n handles"}, ) hs2.extend(h2) # Outer legends ax = axs[0] -ax.legend(hs1, loc='b', ncols=3, title='row major', order='C', facecolor='gray2') +ax.legend(hs1, loc="b", ncols=3, title="row major", order="C", facecolor="gray2") ax = axs[1] -ax.legend(hs2, loc='b', ncols=3, center=True, title='centered rows') -axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Legend formatting demo') +ax.legend(hs2, loc="b", ncols=3, center=True, title="centered rows") +axs.format(xlabel="xlabel", ylabel="ylabel", suptitle="Legend formatting demo") diff --git a/docs/colormaps.py b/docs/colormaps.py index 5ae2ecb03..e2d240a27 100644 --- a/docs/colormaps.py +++ b/docs/colormaps.py @@ -87,6 +87,7 @@ # %% import proplot as pplt + fig, axs = pplt.show_cmaps(rasterized=True) @@ -134,6 +135,7 @@ # %% # Colorspace demo import proplot as pplt + fig, axs = pplt.show_colorspaces(refwidth=1.6, luminance=50) fig, axs = pplt.show_colorspaces(refwidth=1.6, saturation=60) fig, axs = pplt.show_colorspaces(refwidth=1.6, hue=0) @@ -141,7 +143,8 @@ # %% # Compare colormaps import proplot as pplt -for cmaps in (('magma', 'rocket'), ('fire', 'dusk')): + +for cmaps in (("magma", "rocket"), ("fire", "dusk")): fig, axs = pplt.show_channels( *cmaps, refwidth=1.5, minhue=-180, maxsat=400, rgb=False ) @@ -197,6 +200,7 @@ # Sample data import proplot as pplt import numpy as np + state = np.random.RandomState(51423) data = state.rand(30, 30).cumsum(axis=1) @@ -204,20 +208,18 @@ # Colormap from a color # The trailing '_r' makes the colormap go dark-to-light instead of light-to-dark fig = pplt.figure(refwidth=2, span=False) -ax = fig.subplot(121, title='From single named color') -cmap1 = pplt.Colormap('prussian blue_r', l=100, name='Pacific', space='hpl') +ax = fig.subplot(121, title="From single named color") +cmap1 = pplt.Colormap("prussian blue_r", l=100, name="Pacific", space="hpl") m = ax.contourf(data, cmap=cmap1) -ax.colorbar(m, loc='b', ticks='none', label=cmap1.name) +ax.colorbar(m, loc="b", ticks="none", label=cmap1.name) # Colormap from lists -ax = fig.subplot(122, title='From list of colors') -cmap2 = pplt.Colormap(('maroon', 'light tan'), name='Heatwave') +ax = fig.subplot(122, title="From list of colors") +cmap2 = pplt.Colormap(("maroon", "light tan"), name="Heatwave") m = ax.contourf(data, cmap=cmap2) -ax.colorbar(m, loc='b', ticks='none', label=cmap2.name) +ax.colorbar(m, loc="b", ticks="none", label=cmap2.name) fig.format( - xticklabels='none', - yticklabels='none', - suptitle='Making PerceptualColormaps' + xticklabels="none", yticklabels="none", suptitle="Making PerceptualColormaps" ) # Display the channels @@ -226,24 +228,20 @@ # %% # Sequential colormap from channel values cmap3 = pplt.Colormap( - h=('red', 'red-720'), s=(80, 20), l=(20, 100), space='hpl', name='CubeHelix' + h=("red", "red-720"), s=(80, 20), l=(20, 100), space="hpl", name="CubeHelix" ) fig = pplt.figure(refwidth=2, span=False) -ax = fig.subplot(121, title='Sequential from channel values') +ax = fig.subplot(121, title="Sequential from channel values") m = ax.contourf(data, cmap=cmap3) -ax.colorbar(m, loc='b', ticks='none', label=cmap3.name) +ax.colorbar(m, loc="b", ticks="none", label=cmap3.name) # Cyclic colormap from channel values -ax = fig.subplot(122, title='Cyclic from channel values') -cmap4 = pplt.Colormap( - h=(0, 360), c=50, l=70, space='hcl', cyclic=True, name='Spectrum' -) +ax = fig.subplot(122, title="Cyclic from channel values") +cmap4 = pplt.Colormap(h=(0, 360), c=50, l=70, space="hcl", cyclic=True, name="Spectrum") m = ax.contourf(data, cmap=cmap4) -ax.colorbar(m, loc='b', ticks='none', label=cmap4.name) +ax.colorbar(m, loc="b", ticks="none", label=cmap4.name) fig.format( - xticklabels='none', - yticklabels='none', - suptitle='Making PerceptualColormaps' + xticklabels="none", yticklabels="none", suptitle="Making PerceptualColormaps" ) # Display the channels @@ -275,36 +273,38 @@ # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) data = state.rand(30, 30).cumsum(axis=1) # Generate figure fig, axs = pplt.subplots([[0, 1, 1, 0], [2, 2, 3, 3]], refwidth=2.4, span=False) -axs.format( - xlabel='xlabel', ylabel='ylabel', - suptitle='Merging colormaps' -) +axs.format(xlabel="xlabel", ylabel="ylabel", suptitle="Merging colormaps") # Diverging colormap example -title1 = 'Diverging from sequential maps' -cmap1 = pplt.Colormap('Blues4_r', 'Reds3', name='Diverging', save=True) +title1 = "Diverging from sequential maps" +cmap1 = pplt.Colormap("Blues4_r", "Reds3", name="Diverging", save=True) # SciVisColor examples -title2 = 'SciVisColor example' +title2 = "SciVisColor example" cmap2 = pplt.Colormap( - 'Greens1_r', 'Oranges1', 'Blues1_r', 'Blues6', - ratios=(1, 3, 5, 10), name='SciVisColorUneven', save=True + "Greens1_r", + "Oranges1", + "Blues1_r", + "Blues6", + ratios=(1, 3, 5, 10), + name="SciVisColorUneven", + save=True, ) -title3 = 'SciVisColor with equal ratios' +title3 = "SciVisColor with equal ratios" cmap3 = pplt.Colormap( - 'Greens1_r', 'Oranges1', 'Blues1_r', 'Blues6', - name='SciVisColorEven', save=True + "Greens1_r", "Oranges1", "Blues1_r", "Blues6", name="SciVisColorEven", save=True ) # Plot examples for ax, cmap, title in zip(axs, (cmap1, cmap2, cmap3), (title1, title2, title3)): m = ax.contourf(data, cmap=cmap, levels=500) - ax.colorbar(m, loc='b', locator='null', label=cmap.name) + ax.colorbar(m, loc="b", locator="null", label=cmap.name) ax.format(title=title) @@ -352,79 +352,93 @@ # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) data = state.rand(40, 40).cumsum(axis=0) # Generate figure fig, axs = pplt.subplots([[0, 1, 1, 0], [2, 2, 3, 3]], refwidth=1.9, span=False) -axs.format(xlabel='y axis', ylabel='x axis', suptitle='Truncating sequential colormaps') +axs.format(xlabel="y axis", ylabel="x axis", suptitle="Truncating sequential colormaps") # Cutting left and right -cmap = 'Ice' +cmap = "Ice" for ax, coord in zip(axs, (None, 0.3, 0.7)): if coord is None: - title, cmap_kw = 'Original', {} + title, cmap_kw = "Original", {} elif coord < 0.5: - title, cmap_kw = f'left={coord}', {'left': coord} + title, cmap_kw = f"left={coord}", {"left": coord} else: - title, cmap_kw = f'right={coord}', {'right': coord} + title, cmap_kw = f"right={coord}", {"right": coord} ax.format(title=title) ax.contourf( - data, cmap=cmap, cmap_kw=cmap_kw, colorbar='b', colorbar_kw={'locator': 'null'} + data, cmap=cmap, cmap_kw=cmap_kw, colorbar="b", colorbar_kw={"locator": "null"} ) # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) data = (state.rand(40, 40) - 0.5).cumsum(axis=0).cumsum(axis=1) # Create figure fig, axs = pplt.subplots(ncols=2, nrows=2, refwidth=1.7, span=False) axs.format( - xlabel='x axis', ylabel='y axis', xticklabels='none', - suptitle='Modifying diverging colormaps', + xlabel="x axis", + ylabel="y axis", + xticklabels="none", + suptitle="Modifying diverging colormaps", ) # Cutting out central colors titles = ( - 'Negative-positive cutoff', 'Neutral-valued center', - 'Sharper cutoff', 'Expanded center' + "Negative-positive cutoff", + "Neutral-valued center", + "Sharper cutoff", + "Expanded center", ) for i, (ax, title, cut) in enumerate(zip(axs, titles, (None, None, 0.2, -0.1))): if i % 2 == 0: - kw = {'levels': pplt.arange(-10, 10, 2)} # negative-positive cutoff + kw = {"levels": pplt.arange(-10, 10, 2)} # negative-positive cutoff else: - kw = {'values': pplt.arange(-10, 10, 2)} # dedicated center + kw = {"values": pplt.arange(-10, 10, 2)} # dedicated center if cut is not None: fmt = pplt.SimpleFormatter() # a proper minus sign - title = f'{title}\ncut = {fmt(cut)}' + title = f"{title}\ncut = {fmt(cut)}" ax.format(title=title) m = ax.contourf( - data, cmap='Div', cmap_kw={'cut': cut}, extend='both', - colorbar='b', colorbar_kw={'locator': 'null'}, - **kw # level edges or centers + data, + cmap="Div", + cmap_kw={"cut": cut}, + extend="both", + colorbar="b", + colorbar_kw={"locator": "null"}, + **kw, # level edges or centers ) # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) data = (state.rand(50, 50) - 0.48).cumsum(axis=0).cumsum(axis=1) % 30 # Rotating cyclic colormaps fig, axs = pplt.subplots(ncols=3, refwidth=1.7, span=False) for ax, shift in zip(axs, (0, 90, 180)): - m = ax.pcolormesh(data, cmap='romaO', cmap_kw={'shift': shift}, levels=12) + m = ax.pcolormesh(data, cmap="romaO", cmap_kw={"shift": shift}, levels=12) ax.format( - xlabel='x axis', ylabel='y axis', title=f'shift = {shift}', - suptitle='Rotating cyclic colormaps' + xlabel="x axis", + ylabel="y axis", + title=f"shift = {shift}", + suptitle="Rotating cyclic colormaps", ) - ax.colorbar(m, loc='b', locator='null') + ax.colorbar(m, loc="b", locator="null") # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) data = state.rand(20, 20).cumsum(axis=1) @@ -432,29 +446,34 @@ fig, axs = pplt.subplots(ncols=3, refwidth=1.7, span=False) for ax, alpha in zip(axs, (1.0, 0.5, 0.0)): alpha = (alpha, 1.0) - cmap = pplt.Colormap('batlow_r', alpha=alpha) - m = ax.imshow(data, cmap=cmap, levels=10, extend='both') - ax.colorbar(m, loc='b', locator='none') + cmap = pplt.Colormap("batlow_r", alpha=alpha) + m = ax.imshow(data, cmap=cmap, levels=10, extend="both") + ax.colorbar(m, loc="b", locator="none") ax.format( - title=f'alpha = {alpha}', xlabel='x axis', ylabel='y axis', - suptitle='Adding opacity gradations' + title=f"alpha = {alpha}", + xlabel="x axis", + ylabel="y axis", + suptitle="Adding opacity gradations", ) # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) data = state.rand(20, 20).cumsum(axis=1) # Changing the colormap gamma fig, axs = pplt.subplots(ncols=3, refwidth=1.7, span=False) for ax, gamma in zip(axs, (0.7, 1.0, 1.4)): - cmap = pplt.Colormap('boreal', gamma=gamma) - m = ax.pcolormesh(data, cmap=cmap, levels=10, extend='both') - ax.colorbar(m, loc='b', locator='none') + cmap = pplt.Colormap("boreal", gamma=gamma) + m = ax.pcolormesh(data, cmap=cmap, levels=10, extend="both") + ax.colorbar(m, loc="b", locator="none") ax.format( - title=f'gamma = {gamma}', xlabel='x axis', ylabel='y axis', - suptitle='Changing the PerceptualColormap gamma' + title=f"gamma = {gamma}", + xlabel="x axis", + ylabel="y axis", + suptitle="Changing the PerceptualColormap gamma", ) # %% [raw] raw_mimetype="text/restructuredtext" diff --git a/docs/colors.py b/docs/colors.py index 46bf7c7b1..f96ef0266 100644 --- a/docs/colors.py +++ b/docs/colors.py @@ -55,6 +55,7 @@ # %% import proplot as pplt + fig, axs = pplt.show_colors() @@ -87,32 +88,33 @@ state = np.random.RandomState(51423) fig, axs = pplt.subplots(ncols=3, axwidth=2) axs.format( - suptitle='Modifying colors', - toplabels=('Shifted hue', 'Scaled luminance', 'Scaled saturation'), - toplabelweight='normal', - xformatter='none', yformatter='none', + suptitle="Modifying colors", + toplabels=("Shifted hue", "Scaled luminance", "Scaled saturation"), + toplabelweight="normal", + xformatter="none", + yformatter="none", ) # Shifted hue N = 50 fmt = pplt.SimpleFormatter() -marker = 'o' +marker = "o" for shift in (0, -60, 60): x, y = state.rand(2, N) - color = pplt.shift_hue('grass', shift) - axs[0].scatter(x, y, marker=marker, c=color, legend='b', label=fmt(shift)) + color = pplt.shift_hue("grass", shift) + axs[0].scatter(x, y, marker=marker, c=color, legend="b", label=fmt(shift)) # Scaled luminance for scale in (0.2, 1, 2): x, y = state.rand(2, N) - color = pplt.scale_luminance('bright red', scale) - axs[1].scatter(x, y, marker=marker, c=color, legend='b', label=fmt(scale)) + color = pplt.scale_luminance("bright red", scale) + axs[1].scatter(x, y, marker=marker, c=color, legend="b", label=fmt(scale)) # Scaled saturation for scale in (0, 1, 3): x, y = state.rand(2, N) - color = pplt.scale_saturation('ocean blue', scale) - axs[2].scatter(x, y, marker=marker, c=color, legend='b', label=fmt(scale)) + color = pplt.scale_saturation("ocean blue", scale) + axs[2].scatter(x, y, marker=marker, c=color, legend="b", label=fmt(scale)) # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_colors_cmaps: @@ -140,34 +142,44 @@ fig = pplt.figure(refwidth=2.2, share=False) # Drawing from colormaps -name = 'Deep' +name = "Deep" idxs = pplt.arange(0, 1, 0.2) state.shuffle(idxs) -ax = fig.subplot(121, grid=True, title=f'Drawing from colormap {name!r}') +ax = fig.subplot(121, grid=True, title=f"Drawing from colormap {name!r}") for idx in idxs: data = (state.rand(20) - 0.4).cumsum() h = ax.plot( - data, lw=5, color=(name, idx), - label=f'idx {idx:.1f}', legend='l', legend_kw={'ncols': 1} + data, + lw=5, + color=(name, idx), + label=f"idx {idx:.1f}", + legend="l", + legend_kw={"ncols": 1}, ) -ax.colorbar(pplt.Colormap(name), loc='l', locator='none') +ax.colorbar(pplt.Colormap(name), loc="l", locator="none") # Drawing from color cycles -name = 'Qual1' +name = "Qual1" idxs = np.arange(6) state.shuffle(idxs) -ax = fig.subplot(122, title=f'Drawing from color cycle {name!r}') +ax = fig.subplot(122, title=f"Drawing from color cycle {name!r}") for idx in idxs: data = (state.rand(20) - 0.4).cumsum() h = ax.plot( - data, lw=5, color=(name, idx), - label=f'idx {idx:.0f}', legend='r', legend_kw={'ncols': 1} + data, + lw=5, + color=(name, idx), + label=f"idx {idx:.0f}", + legend="r", + legend_kw={"ncols": 1}, ) -ax.colorbar(pplt.Colormap(name), loc='r', locator='none') +ax.colorbar(pplt.Colormap(name), loc="r", locator="none") fig.format( - abc='A.', titleloc='l', - suptitle='On-the-fly color selections', - xformatter='null', yformatter='null', + abc="A.", + titleloc="l", + suptitle="On-the-fly color selections", + xformatter="null", + yformatter="null", ) diff --git a/docs/conf.py b/docs/conf.py index a9b48620e..5924858d5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,50 +16,56 @@ import sys import datetime import subprocess +from pathlib import Path # Update path for sphinx-automodapi and sphinxext extension -sys.path.append(os.path.abspath('.')) -sys.path.insert(0, os.path.abspath('..')) +sys.path.append(os.path.abspath(".")) +sys.path.insert(0, os.path.abspath("..")) # Print available system fonts from matplotlib.font_manager import fontManager -print('Font files:', end=' ') -print(', '.join(os.path.basename(font.fname) for font in fontManager.ttflist)) + +print("Font files:", end=" ") +print(", ".join(os.path.basename(font.fname) for font in fontManager.ttflist)) # -- Project information ------------------------------------------------------- # The basic info -project = 'ProPlot' -copyright = f'{datetime.datetime.today().year}, Luke L. B. Davis' -author = 'Luke L. B. Davis' +project = "ProPlot" +copyright = f"{datetime.datetime.today().year}, Luke L. B. Davis" +author = "Luke L. B. Davis" # The short X.Y version -version = '' +version = "" # The full version, including alpha/beta/rc tags -release = '' +release = "" # -- Create files -------------------------------------------------------------- # Create RST table and sample proplotrc file from proplot.config import rc -folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), '_static') -if not os.path.isdir(folder): - os.mkdir(folder) -rc._save_rst(os.path.join(folder, 'rctable.rst')) -rc._save_yaml(os.path.join(folder, 'proplotrc')) + +folder = (Path(__file__).parent / "_static").absolute() +if not folder.is_dir(): + folder.mkdir() + +rc._save_rst(str(folder / "rctable.rst")) +rc._save_yaml(str(folder / "proplotrc")) # -- Setup basemap -------------------------------------------------------------- # Hack to get basemap to work # See: https://github.com/readthedocs/readthedocs.org/issues/5339 -if os.environ.get('READTHEDOCS', None) == 'True': - conda = os.path.join(os.environ['CONDA_ENVS_PATH'], os.environ['CONDA_DEFAULT_ENV']) +if os.environ.get("READTHEDOCS", None) == "True": + conda = ( + Path(os.environ["CONDA_ENVS_PATH"]) / os.environ["CONDA_DEFAULT_ENV"] + ).absolute() else: - conda = os.environ['CONDA_PREFIX'] -os.environ['GEOS_DIR'] = conda -os.environ['PROJ_LIB'] = os.path.join(conda, 'share', 'proj') + conda = Path(os.environ["CONDA_PREFIX"]).absolute() +os.environ["GEOS_DIR"] = str(conda) +os.environ["PROJ_LIB"] = str((conda / "share" / "proj")) # Install basemap if does not exist # Extremely ugly but impossible to install in environment.yml. Must set @@ -69,7 +75,8 @@ import mpl_toolkits.basemap # noqa: F401 except ImportError: subprocess.check_call( - ['pip', 'install', 'git+https://github.com/matplotlib/basemap@v1.2.2rel'] + ["pip", "install", "basemap"] + # ["pip", "install", "git+https://github.com/matplotlib/basemap@v1.2.2rel"] ) @@ -81,37 +88,47 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - # 'matplotlib.sphinxext.plot_directive', # see: https://matplotlib.org/sampledoc/extensions.html # noqa: E501 - 'sphinx.ext.autodoc', # include documentation from docstrings - 'sphinx.ext.doctest', # >>> examples - 'sphinx.ext.extlinks', # for :pr:, :issue:, :commit: - 'sphinx.ext.autosectionlabel', # use :ref:`Heading` for any heading - 'sphinx.ext.todo', # Todo headers and todo:: directives - 'sphinx.ext.mathjax', # LaTeX style math - 'sphinx.ext.viewcode', # view code links - 'sphinx.ext.napoleon', # for NumPy style docstrings - 'sphinx.ext.intersphinx', # external links - 'sphinx.ext.autosummary', # autosummary directive - 'sphinxext.custom_roles', # local extension - 'sphinx_automodapi.automodapi', # fork of automodapi - 'sphinx_rtd_light_dark', # use custom theme - 'sphinx_copybutton', # add copy button to code - 'nbsphinx', # parse rst books + "matplotlib.sphinxext.plot_directive", # see: https://matplotlib.org/sampledoc/extensions.html # noqa: E501 + # "sphinx.ext.autodoc", # include documentation from docstrings + "autoapi.extension", + "sphinx.ext.doctest", # >>> examples + "sphinx.ext.extlinks", # for :pr:, :issue:, :commit: + "sphinx.ext.autosectionlabel", # use :ref:`Heading` for any heading + "sphinx.ext.todo", # Todo headers and todo:: directives + "sphinx.ext.mathjax", # LaTeX style math + "sphinx.ext.viewcode", # view code links + "sphinx.ext.napoleon", # for NumPy style docstrings + "sphinx.ext.intersphinx", # external links + "sphinx.ext.autosummary", # autosummary directive + "sphinxext.custom_roles", # local extension + "sphinx_automodapi.automodapi", # fork of automodapi + "sphinx_rtd_light_dark", # use custom theme + "sphinx_copybutton", # add copy button to code + "nbsphinx", # parse rst books ] +autoapi_dirs = ["../proplot/"] + # The master toctree document. -master_doc = 'index' +master_doc = "index" # The suffix(es) of source filenames, either a string or list. -source_suffix = ['.rst', '.html'] +source_suffix = [".rst", ".html"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of file patterns relative to source dir that should be ignored exclude_patterns = [ - 'conf.py', 'sphinxext', '_build', '_templates', '_themes', - '*.ipynb', '**.ipynb_checkpoints' '.DS_Store', 'trash', 'tmp', + "conf.py", + "sphinxext", + "_build", + "_templates", + "_themes", + "*.ipynb", + "**.ipynb_checkpoints" ".DS_Store", + "trash", + "tmp", ] # The language for content autogenerated by Sphinx. Refer to documentation @@ -120,7 +137,7 @@ # Role. Default family is py but can also set default role so don't need # :func:`name`, :module:`name`, etc. -default_role = 'py:obj' +default_role = "py:obj" # If true, the current module name will be prepended to all description # unit titles (such as .. function::). @@ -128,7 +145,7 @@ # Autodoc configuration. Here we concatenate class and __init__ docstrings # See: http://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html -autoclass_content = 'both' # options are 'class', 'both', 'init' +autoclass_content = "both" # options are 'class', 'both', 'init' # Generate stub pages whenever ::autosummary directive encountered # This way don't have to call sphinx-autogen manually @@ -137,40 +154,40 @@ # Automodapi tool: https://sphinx-automodapi.readthedocs.io/en/latest/automodapi.html # Normally have to *enumerate* function names manually. This will document them # automatically. Just be careful to exclude public names from automodapi:: directive. -automodapi_toctreedirnm = 'api' +automodapi_toctreedirnm = "api" automodsumm_inherited_members = False # Doctest configuration. For now do not run tests, they are just to show syntax # and expected output may be graphical -doctest_test_doctest_blocks = '' +doctest_test_doctest_blocks = "" # Cupybutton configuration # See: https://sphinx-copybutton.readthedocs.io/en/latest/ -copybutton_prompt_text = r'>>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: ' +copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " copybutton_prompt_is_regexp = True copybutton_only_copy_prompt_lines = True copybutton_remove_prompts = True # Links for What's New github commits, issues, and pull requests extlinks = { - 'issue': ('https://github.com/proplot-dev/proplot/issues/%s', 'GH#'), - 'commit': ('https://github.com/proplot-dev/proplot/commit/%s', '@'), - 'pr': ('https://github.com/proplot-dev/proplot/pull/%s', 'GH#'), + "issue": ("https://github.com/proplot-dev/proplot/issues/%s", "GH#%s"), + "commit": ("https://github.com/proplot-dev/proplot/commit/%s", "@%s"), + "pr": ("https://github.com/proplot-dev/proplot/pull/%s", "GH#%s"), } # Set up mapping for other projects' docs intersphinx_mapping = { - 'cycler': ('https://matplotlib.org/cycler/', None), - 'matplotlib': ('https://matplotlib.org/stable', None), - 'sphinx': ('http://www.sphinx-doc.org/en/stable', None), - 'python': ('https://docs.python.org/3', None), - 'numpy': ('https://docs.scipy.org/doc/numpy', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/reference', None), - 'xarray': ('http://xarray.pydata.org/en/stable', None), - 'cartopy': ('https://scitools.org.uk/cartopy/docs/latest', None), - 'basemap': ('https://matplotlib.org/basemap', None), - 'pandas': ('https://pandas.pydata.org/pandas-docs/stable', None), - 'pint': ('https://pint.readthedocs.io/en/stable/', None), + "cycler": ("https://matplotlib.org/cycler/", None), + "matplotlib": ("https://matplotlib.org/stable", None), + "sphinx": ("http://www.sphinx-doc.org/en/stable", None), + "python": ("https://docs.python.org/3", None), + "numpy": ("https://docs.scipy.org/doc/numpy", None), + "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), + "xarray": ("http://xarray.pydata.org/en/stable", None), + "cartopy": ("https://scitools.org.uk/cartopy/docs/latest", None), + "basemap": ("https://matplotlib.org/basemap", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), + "pint": ("https://pint.readthedocs.io/en/stable/", None), } # Fix duplicate class member documentation from autosummary + numpydoc @@ -198,21 +215,21 @@ napoleon_type_aliases = { # Python or inherited terms # NOTE: built-in types are automatically included - 'callable': ':py:func:`callable`', - 'sequence': ':term:`sequence`', - 'dict-like': ':term:`dict-like `', - 'path-like': ':term:`path-like `', - 'array-like': ':term:`array-like `', + "callable": ":py:func:`callable`", + "sequence": ":term:`sequence`", + "dict-like": ":term:`dict-like `", + "path-like": ":term:`path-like `", + "array-like": ":term:`array-like `", # Proplot defined terms - 'unit-spec': ':py:func:`unit-spec `', - 'locator-spec': ':py:func:`locator-spec `', - 'formatter-spec': ':py:func:`formatter-spec `', - 'scale-spec': ':py:func:`scale-spec `', - 'colormap-spec': ':py:func:`colormap-spec `', - 'cycle-spec': ':py:func:`cycle-spec `', - 'norm-spec': ':py:func:`norm-spec `', - 'color-spec': ':py:func:`color-spec `', - 'artist': ':py:func:`artist `', + "unit-spec": ":py:func:`unit-spec `", + "locator-spec": ":py:func:`locator-spec `", + "formatter-spec": ":py:func:`formatter-spec `", + "scale-spec": ":py:func:`scale-spec `", + "colormap-spec": ":py:func:`colormap-spec `", + "cycle-spec": ":py:func:`cycle-spec `", + "norm-spec": ":py:func:`norm-spec `", + "color-spec": ":py:func:`color-spec `", + "artist": ":py:func:`artist `", } # Fail on error. Note nbsphinx compiles all notebooks in docs unless excluded @@ -222,34 +239,36 @@ nbsphinx_timeout = 300 # Add jupytext support to nbsphinx -nbsphinx_custom_formats = {'.py': ['jupytext.reads', {'fmt': 'py:percent'}]} +nbsphinx_custom_formats = {".py": ["jupytext.reads", {"fmt": "py:percent"}]} # The name of the Pygments (syntax highlighting) style to use. # The light-dark theme toggler overloads this, but set default anyway -pygments_style = 'none' +pygments_style = "none" # -- Options for HTML output ------------------------------------------------- # Logo -html_logo = os.path.join('_static', 'logo_square.png') +html_logo = str(Path("_static") / "logo_square.png") # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # Use modified RTD theme with overrides in custom.css and custom.js -html_theme = 'sphinx_rtd_light_dark' +html_theme = "sphinx_rtd_light_dark" +# html_theme = "alabaster" +# html_theme = "sphinx_rtd_theme" html_theme_options = { - 'logo_only': True, - 'display_version': False, - 'collapse_navigation': True, - 'navigation_depth': 4, - 'prev_next_buttons_location': 'bottom', # top and bottom + "logo_only": True, + "display_version": False, + "collapse_navigation": True, + "navigation_depth": 4, + "prev_next_buttons_location": "bottom", # top and bottom } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -263,12 +282,12 @@ # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. Static folder is for CSS and image files. Use ImageMagick to # convert png to ico on command line with 'convert image.png image.ico' -html_favicon = os.path.join('_static', 'logo_blank.ico') +html_favicon = str(Path("_static") / "logo_blank.ico") # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'proplotdoc' +htmlhelp_basename = "proplotdoc" # -- Options for LaTeX output ------------------------------------------------ @@ -276,13 +295,10 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # 'preamble': '', - # Latex figure (float) alignment # 'figure_align': 'htbp', } @@ -291,8 +307,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'proplot.tex', 'ProPlot Documentation', - 'Luke L. B. Davis', 'manual'), + (master_doc, "proplot.tex", "ProPlot Documentation", "Luke L. B. Davis", "manual"), ] @@ -300,15 +315,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ( - master_doc, - 'proplot', - 'ProPlot Documentation', - [author], - 1 - ) -] +man_pages = [(master_doc, "proplot", "ProPlot Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -319,13 +326,13 @@ texinfo_documents = [ ( master_doc, - 'proplot', - 'ProPlot Documentation', + "proplot", + "ProPlot Documentation", author, - 'proplot', - 'A succinct matplotlib wrapper for making beautiful, ' - 'publication-quality graphics.', - 'Miscellaneous' + "proplot", + "A succinct matplotlib wrapper for making beautiful, " + "publication-quality graphics.", + "Miscellaneous", ) ] diff --git a/docs/cycles.py b/docs/cycles.py index 3cf06d172..8c03f45aa 100644 --- a/docs/cycles.py +++ b/docs/cycles.py @@ -51,6 +51,7 @@ # %% import proplot as pplt + fig, axs = pplt.show_cycles(rasterized=True) @@ -77,28 +78,28 @@ # Sample data state = np.random.RandomState(51423) data = (state.rand(12, 6) - 0.45).cumsum(axis=0) -kwargs = {'legend': 'b', 'labels': list('abcdef')} +kwargs = {"legend": "b", "labels": list("abcdef")} # Figure lw = 5 -pplt.rc.cycle = '538' -fig = pplt.figure(refwidth=1.9, suptitle='Changing the color cycle') +pplt.rc.cycle = "538" +fig = pplt.figure(refwidth=1.9, suptitle="Changing the color cycle") # Modify the default color cycle -ax = fig.subplot(131, title='Global color cycle') +ax = fig.subplot(131, title="Global color cycle") ax.plot(data, lw=lw, **kwargs) # Pass the cycle to a plotting command -ax = fig.subplot(132, title='Local color cycle') -ax.plot(data, cycle='qual1', lw=lw, **kwargs) +ax = fig.subplot(132, title="Local color cycle") +ax.plot(data, cycle="qual1", lw=lw, **kwargs) # As above but draw each line individually # Note that passing cycle=name to successive plot calls does # not reset the cycle position if the cycle is unchanged -ax = fig.subplot(133, title='Multiple plot calls') -labels = kwargs['labels'] +ax = fig.subplot(133, title="Multiple plot calls") +labels = kwargs["labels"] for i in range(data.shape[1]): - ax.plot(data[:, i], cycle='qual1', legend='b', label=labels[i], lw=lw) + ax.plot(data[:, i], cycle="qual1", legend="b", label=labels[i], lw=lw) # %% [raw] raw_mimetype="text/restructuredtext" @@ -137,24 +138,25 @@ # %% import proplot as pplt import numpy as np + fig = pplt.figure(refwidth=2, share=False) state = np.random.RandomState(51423) data = (20 * state.rand(10, 21) - 10).cumsum(axis=0) # Cycle from on-the-fly monochromatic colormap ax = fig.subplot(121) -lines = ax.plot(data[:, :5], cycle='plum', lw=5) -fig.colorbar(lines, loc='b', col=1, values=np.arange(0, len(lines))) -fig.legend(lines, loc='b', col=1, labels=np.arange(0, len(lines))) -ax.format(title='Cycle from a single color') +lines = ax.plot(data[:, :5], cycle="plum", lw=5) +fig.colorbar(lines, loc="b", col=1, values=np.arange(0, len(lines))) +fig.legend(lines, loc="b", col=1, labels=np.arange(0, len(lines))) +ax.format(title="Cycle from a single color") # Cycle from registered colormaps ax = fig.subplot(122) -cycle = pplt.Cycle('blues', 'reds', 'oranges', 15, left=0.1) +cycle = pplt.Cycle("blues", "reds", "oranges", 15, left=0.1) lines = ax.plot(data[:, :15], cycle=cycle, lw=5) -fig.colorbar(lines, loc='b', col=2, values=np.arange(0, len(lines)), locator=2) -fig.legend(lines, loc='b', col=2, labels=np.arange(0, len(lines)), ncols=4) -ax.format(title='Cycle from merged colormaps', suptitle='Color cycles from colormaps') +fig.colorbar(lines, loc="b", col=2, values=np.arange(0, len(lines)), locator=2) +fig.legend(lines, loc="b", col=2, labels=np.arange(0, len(lines)), ncols=4) +ax.format(title="Cycle from merged colormaps", suptitle="Color cycles from colormaps") # %% [raw] raw_mimetype="text/restructuredtext" @@ -182,13 +184,12 @@ # Sample data state = np.random.RandomState(51423) data = (state.rand(20, 4) - 0.5).cumsum(axis=0) -data = pd.DataFrame(data, columns=pd.Index(['a', 'b', 'c', 'd'], name='label')) +data = pd.DataFrame(data, columns=pd.Index(["a", "b", "c", "d"], name="label")) # Plot data -fig, ax = pplt.subplots(refwidth=2.5, suptitle='Plot without color cycle') +fig, ax = pplt.subplots(refwidth=2.5, suptitle="Plot without color cycle") obj = ax.plot( - data, cycle=cycle, legend='ll', - legend_kw={'ncols': 2, 'handlelength': 2.5} + data, cycle=cycle, legend="ll", legend_kw={"ncols": 2, "handlelength": 2.5} ) diff --git a/docs/environment.yml b/docs/environment.yml index 2a7bdcf35..edb4ca923 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -15,25 +15,35 @@ name: proplot-dev channels: - conda-forge + - defaults dependencies: - - python==3.8 - - matplotlib==3.2.2 - - cartopy==0.20.2 - - numpy - - pandas - - xarray + - cartopy + - docutils - ipykernel + - jinja2 + - jupytext + - markupsafe + - matplotlib>=3.9.1 + - numpy>=1.26.0,<2 + - pandas - pandoc + - pint - pip + - pyqt + - pytest + - python + - seaborn + - setuptools==72.1.0 + - xarray - pip: - .. - - pyqt5 - - docutils==0.16 - - sphinx>=3.0 + - basemap + - sphinx-autoapi + - basemap-data-hires + # - git+https://github.com/proplot-dev/sphinx-automodapi@proplot-mods + - sphinx-automodapi + - nbsphinx + - sphinx>=7,<8 - sphinx-copybutton - - sphinx-rtd-light-dark - - jinja2==2.11.3 - - markupsafe==2.0.1 - - nbsphinx==0.8.1 - - jupytext - - git+https://github.com/proplot-dev/sphinx-automodapi@proplot-mods + - sphinx-rtd-theme==2.1.0rc2 + - git+https://github.com/cvanelteren/sphinx-rtd-light-dark.git@mplv3.9.1 diff --git a/docs/fonts.py b/docs/fonts.py index 6cc1264c3..939b8d77d 100644 --- a/docs/fonts.py +++ b/docs/fonts.py @@ -71,11 +71,13 @@ # %% import proplot as pplt -fig, axs = pplt.show_fonts(family='sans-serif') + +fig, axs = pplt.show_fonts(family="sans-serif") # %% import proplot as pplt -fig, axs = pplt.show_fonts(family='tex-gyre') + +fig, axs = pplt.show_fonts(family="tex-gyre") # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_fonts_math: @@ -119,7 +121,8 @@ # %% import proplot as pplt -fig, axs = pplt.show_fonts(family='sans-serif', math=True) + +fig, axs = pplt.show_fonts(family="sans-serif", math=True) # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_fonts_user: diff --git a/docs/insets_panels.py b/docs/insets_panels.py index c9909c839..1e6cf0e87 100644 --- a/docs/insets_panels.py +++ b/docs/insets_panels.py @@ -68,69 +68,81 @@ # spacing, aspect ratios, and axis sharing gs = pplt.GridSpec(nrows=2, ncols=2) fig = pplt.figure(refwidth=1.5, share=False) -for ss, side in zip(gs, 'tlbr'): +for ss, side in zip(gs, "tlbr"): ax = fig.add_subplot(ss) - px = ax.panel_axes(side, width='3em') + px = ax.panel_axes(side, width="3em") fig.format( - xlim=(0, 1), ylim=(0, 1), - xlabel='xlabel', ylabel='ylabel', - xticks=0.2, yticks=0.2, - title='Title', suptitle='Complex arrangement of panels', - toplabels=('Column 1', 'Column 2'), - abc=True, abcloc='ul', titleloc='uc', titleabove=False, + xlim=(0, 1), + ylim=(0, 1), + xlabel="xlabel", + ylabel="ylabel", + xticks=0.2, + yticks=0.2, + title="Title", + suptitle="Complex arrangement of panels", + toplabels=("Column 1", "Column 2"), + abc=True, + abcloc="ul", + titleloc="uc", + titleabove=False, ) # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) data = (state.rand(20, 20) - 0.48).cumsum(axis=1).cumsum(axis=0) data = 10 * (data - data.min()) / (data.max() - data.min()) # Stacked panels with outer colorbars -for cbarloc, ploc in ('rb', 'br'): +for cbarloc, ploc in ("rb", "br"): # Create figure fig, axs = pplt.subplots( - nrows=1, ncols=2, refwidth=1.8, panelpad=0.8, - share=False, includepanels=True + nrows=1, ncols=2, refwidth=1.8, panelpad=0.8, share=False, includepanels=True ) axs.format( - xlabel='xlabel', ylabel='ylabel', title='Title', - suptitle='Using panels for summary statistics', + xlabel="xlabel", + ylabel="ylabel", + title="Title", + suptitle="Using panels for summary statistics", ) # Plot 2D dataset for ax in axs: ax.contourf( - data, cmap='glacial', extend='both', - colorbar=cbarloc, colorbar_kw={'label': 'colorbar'}, + data, + cmap="glacial", + extend="both", + colorbar=cbarloc, + colorbar_kw={"label": "colorbar"}, ) # Get summary statistics and settings - axis = int(ploc == 'r') # dimension along which stats are taken + axis = int(ploc == "r") # dimension along which stats are taken x1 = x2 = np.arange(20) y1 = data.mean(axis=axis) y2 = data.std(axis=axis) - titleloc = 'upper center' - if ploc == 'r': - titleloc = 'center' + titleloc = "upper center" + if ploc == "r": + titleloc = "center" x1, x2, y1, y2 = y1, y2, x1, x2 # Panels for plotting the mean. Note SubplotGrid.panel() returns a SubplotGrid # of panel axes. We use this to call format() for all the panels at once. space = 0 - width = '4em' - kwargs = {'titleloc': titleloc, 'xreverse': False, 'yreverse': False} + width = "4em" + kwargs = {"titleloc": titleloc, "xreverse": False, "yreverse": False} pxs = axs.panel(ploc, space=space, width=width) - pxs.format(title='Mean', **kwargs) + pxs.format(title="Mean", **kwargs) for px in pxs: - px.plot(x1, y1, color='gray7') + px.plot(x1, y1, color="gray7") # Panels for plotting the standard deviation pxs = axs.panel(ploc, space=space, width=width) - pxs.format(title='Stdev', **kwargs) + pxs.format(title="Stdev", **kwargs) for px in pxs: - px.plot(x2, y2, color='gray7', ls='--') + px.plot(x2, y2, color="gray7", ls="--") # %% [raw] raw_mimetype="text/restructuredtext" @@ -166,21 +178,17 @@ # Plot data in the main axes fig, ax = pplt.subplots(refwidth=3) -m = ax.pcolormesh(data, cmap='Grays', levels=N) -ax.colorbar(m, loc='b', label='label') -ax.format( - xlabel='xlabel', ylabel='ylabel', - suptitle='"Zooming in" with an inset axes' -) +m = ax.pcolormesh(data, cmap="Grays", levels=N) +ax.colorbar(m, loc="b", label="label") +ax.format(xlabel="xlabel", ylabel="ylabel", suptitle='"Zooming in" with an inset axes') # Create an inset axes representing a "zoom-in" # See the 1D plotting section for more on the "inbounds" keyword ix = ax.inset( - [5, 5, 4, 4], transform='data', zoom=True, - zoom_kw={'ec': 'blush', 'ls': '--', 'lw': 2} -) -ix.format( - xlim=(2, 4), ylim=(2, 4), color='red8', - linewidth=1.5, ticklabelweight='bold' + [5, 5, 4, 4], + transform="data", + zoom=True, + zoom_kw={"ec": "blush", "ls": "--", "lw": 2}, ) -ix.pcolormesh(data, cmap='Grays', levels=N, inbounds=False) +ix.format(xlim=(2, 4), ylim=(2, 4), color="red8", linewidth=1.5, ticklabelweight="bold") +ix.pcolormesh(data, cmap="Grays", levels=N, inbounds=False) diff --git a/docs/projections.py b/docs/projections.py index 9be3ffc69..88f31a4b8 100644 --- a/docs/projections.py +++ b/docs/projections.py @@ -57,35 +57,54 @@ # %% import proplot as pplt import numpy as np + N = 200 state = np.random.RandomState(51423) x = np.linspace(0, 2 * np.pi, N)[:, None] + np.arange(5) * 2 * np.pi / 5 y = 100 * (state.rand(N, 5) - 0.3).cumsum(axis=0) / N -fig, axs = pplt.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], proj='polar') +fig, axs = pplt.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], proj="polar") axs.format( - suptitle='Polar axes demo', linewidth=1, titlepad='1em', - ticklabelsize=9, rlines=0.5, rlim=(0, 19), + suptitle="Polar axes demo", + linewidth=1, + titlepad="1em", + ticklabelsize=9, + rlines=0.5, + rlim=(0, 19), ) for ax in axs: - ax.plot(x, y, cycle='default', zorder=0, lw=3) + ax.plot(x, y, cycle="default", zorder=0, lw=3) # Standard polar plot axs[0].format( - title='Normal plot', thetaformatter='tau', - rlabelpos=225, rlines=pplt.arange(5, 30, 5), - edgecolor='red8', tickpad='1em', + title="Normal plot", + thetaformatter="tau", + rlabelpos=225, + rlines=pplt.arange(5, 30, 5), + edgecolor="red8", + tickpad="1em", ) # Sector plot axs[1].format( - title='Sector plot', thetadir=-1, thetalines=90, thetalim=(0, 270), theta0='N', - rlim=(0, 22), rlines=pplt.arange(5, 30, 5), + title="Sector plot", + thetadir=-1, + thetalines=90, + thetalim=(0, 270), + theta0="N", + rlim=(0, 22), + rlines=pplt.arange(5, 30, 5), ) # Annular plot axs[2].format( - title='Annular plot', thetadir=-1, thetalines=20, gridcolor='red', - r0=-20, rlim=(0, 22), rformatter='null', rlocator=2 + title="Annular plot", + thetadir=-1, + thetalines=20, + gridcolor="red", + r0=-20, + rlim=(0, 22), + rformatter="null", + rlocator=2, ) # %% [raw] raw_mimetype="text/restructuredtext" @@ -114,13 +133,16 @@ # %% # Use an on-the-fly projection import proplot as pplt + fig = pplt.figure(refwidth=3) -axs = fig.subplots(nrows=2, proj='robin', proj_kw={'lon0': 150}) +axs = fig.subplots(nrows=2, proj="robin", proj_kw={"lon0": 150}) # proj = pplt.Proj('robin', lon0=180) # axs = pplt.subplots(nrows=2, proj=proj) # equivalent to above axs.format( - suptitle='Figure with single projection', - land=True, latlines=30, lonlines=60, + suptitle="Figure with single projection", + land=True, + latlines=30, + lonlines=60, ) # %% [raw] raw_mimetype="text/restructuredtext" @@ -204,26 +226,28 @@ # %% import proplot as pplt + fig = pplt.figure() # Add projections gs = pplt.GridSpec(ncols=2, nrows=3, hratios=(1, 1, 1.4)) -for i, proj in enumerate(('cyl', 'hammer', 'npstere')): +for i, proj in enumerate(("cyl", "hammer", "npstere")): ax1 = fig.subplot(gs[i, 0], proj=proj) # default cartopy backend - ax2 = fig.subplot(gs[i, 1], proj=proj, backend='basemap') # basemap backend + ax2 = fig.subplot(gs[i, 1], proj=proj, backend="basemap") # basemap backend # Format projections axs = fig.subplotgrid axs.format( land=True, - suptitle='Figure with several projections', - toplabels=('Cartopy examples', 'Basemap examples'), - toplabelweight='normal', - latlines=30, lonlines=60, + suptitle="Figure with several projections", + toplabels=("Cartopy examples", "Basemap examples"), + toplabelweight="normal", + latlines=30, + lonlines=60, ) -axs[:2].format(lonlabels='b', latlabels='r') # or lonlabels=True, lonlabels='bottom', -axs[2:4].format(lonlabels=False, latlabels='both') -axs[4:].format(lonlabels='all', lonlines=30) +axs[:2].format(lonlabels="b", latlabels="r") # or lonlabels=True, lonlabels='bottom', +axs[2:4].format(lonlabels=False, latlabels="both") +axs[4:].format(lonlabels="all", lonlines=30) pplt.rc.reset() @@ -268,25 +292,29 @@ # Plot data both without and with globe=True for globe in (False, True): - string = 'with' if globe else 'without' + string = "with" if globe else "without" gs = pplt.GridSpec(nrows=2, ncols=2) fig = pplt.figure(refwidth=2.5) for i, ss in enumerate(gs): - cmap = ('sunset', 'sunrise')[i % 2] - backend = ('cartopy', 'basemap')[i % 2] - ax = fig.subplot(ss, proj='kav7', backend=backend) + cmap = ("sunset", "sunrise")[i % 2] + backend = ("cartopy", "basemap")[i % 2] + ax = fig.subplot(ss, proj="kav7", backend=backend) if i > 1: - ax.pcolor(lon, lat, data, cmap=cmap, globe=globe, extend='both') + ax.pcolor(lon, lat, data, cmap=cmap, globe=globe, extend="both") else: - m = ax.contourf(lon, lat, data, cmap=cmap, globe=globe, extend='both') - fig.colorbar(m, loc='b', span=i + 1, label='values', extendsize='1.7em') + m = ax.contourf(lon, lat, data, cmap=cmap, globe=globe, extend="both") + fig.colorbar(m, loc="b", span=i + 1, label="values", extendsize="1.7em") fig.format( - suptitle=f'Geophysical data {string} global coverage', - toplabels=('Cartopy example', 'Basemap example'), - leftlabels=('Filled contours', 'Grid boxes'), - toplabelweight='normal', leftlabelweight='normal', - coast=True, lonlines=90, - abc='A.', abcloc='ul', abcborder=False, + suptitle=f"Geophysical data {string} global coverage", + toplabels=("Cartopy example", "Basemap example"), + leftlabels=("Filled contours", "Grid boxes"), + toplabelweight="normal", + leftlabelweight="normal", + coast=True, + lonlines=90, + abc="A.", + abcloc="ul", + abcborder=False, ) @@ -322,36 +350,68 @@ # %% import proplot as pplt + gs = pplt.GridSpec(ncols=3, nrows=2, wratios=(1, 1, 1.2), hratios=(1, 1.2)) fig = pplt.figure(refwidth=4) # Styling projections in different ways -ax = fig.subplot(gs[0, :2], proj='eqearth') +ax = fig.subplot(gs[0, :2], proj="eqearth") ax.format( - title='Equal earth', land=True, landcolor='navy', facecolor='pale blue', - coastcolor='gray5', borderscolor='gray5', innerborderscolor='gray5', - gridlinewidth=1.5, gridcolor='gray5', gridalpha=0.5, - gridminor=True, gridminorlinewidth=0.5, - coast=True, borders=True, borderslinewidth=0.8, + title="Equal earth", + land=True, + landcolor="navy", + facecolor="pale blue", + coastcolor="gray5", + borderscolor="gray5", + innerborderscolor="gray5", + gridlinewidth=1.5, + gridcolor="gray5", + gridalpha=0.5, + gridminor=True, + gridminorlinewidth=0.5, + coast=True, + borders=True, + borderslinewidth=0.8, ) -ax = fig.subplot(gs[0, 2], proj='ortho') +ax = fig.subplot(gs[0, 2], proj="ortho") ax.format( - title='Orthographic', reso='med', land=True, coast=True, latlines=10, lonlines=15, - landcolor='mushroom', suptitle='Projection axes formatting demo', - facecolor='petrol', coastcolor='charcoal', coastlinewidth=0.8, gridlinewidth=1 + title="Orthographic", + reso="med", + land=True, + coast=True, + latlines=10, + lonlines=15, + landcolor="mushroom", + suptitle="Projection axes formatting demo", + facecolor="petrol", + coastcolor="charcoal", + coastlinewidth=0.8, + gridlinewidth=1, ) -ax = fig.subplot(gs[1, :], proj='wintri') +ax = fig.subplot(gs[1, :], proj="wintri") ax.format( - land=True, facecolor='ocean blue', landcolor='bisque', title='Winkel tripel', - lonlines=60, latlines=15, - gridlinewidth=0.8, gridminor=True, gridminorlinestyle=':', - lonlabels=True, latlabels='r', loninline=True, - gridlabelcolor='gray8', gridlabelsize='med-large', + land=True, + facecolor="ocean blue", + landcolor="bisque", + title="Winkel tripel", + lonlines=60, + latlines=15, + gridlinewidth=0.8, + gridminor=True, + gridminorlinestyle=":", + lonlabels=True, + latlabels="r", + loninline=True, + gridlabelcolor="gray8", + gridlabelsize="med-large", ) fig.format( - suptitle='Projection axes formatting demo', - toplabels=('Column 1', 'Column 2'), - abc='A.', abcloc='ul', abcborder=False, linewidth=1.5 + suptitle="Projection axes formatting demo", + toplabels=("Column 1", "Column 2"), + abc="A.", + abcloc="ul", + abcborder=False, + linewidth=1.5, ) @@ -385,55 +445,61 @@ import proplot as pplt # Plate Carrée map projection -pplt.rc.reso = 'med' # use higher res for zoomed in geographic features -basemap = pplt.Proj('cyl', lonlim=(-20, 180), latlim=(-10, 50), backend='basemap') -fig, axs = pplt.subplots(nrows=2, refwidth=5, proj=('cyl', basemap)) +pplt.rc.reso = "med" # use higher res for zoomed in geographic features +basemap = pplt.Proj("cyl", lonlim=(-20, 180), latlim=(-10, 50), backend="basemap") +fig, axs = pplt.subplots(nrows=2, refwidth=5, proj=("cyl", basemap)) axs.format( - land=True, labels=True, lonlines=20, latlines=20, - gridminor=True, suptitle='Zooming into projections' + land=True, + labels=True, + lonlines=20, + latlines=20, + gridminor=True, + suptitle="Zooming into projections", ) axs[0].format(lonlim=(-140, 60), latlim=(-10, 50), labels=True) -axs[0].format(title='Cartopy example') -axs[1].format(title='Basemap example') +axs[0].format(title="Cartopy example") +axs[1].format(title="Basemap example") # %% import proplot as pplt # Pole-centered map projections -basemap = pplt.Proj('npaeqd', boundinglat=60, backend='basemap') -fig, axs = pplt.subplots(ncols=2, refwidth=2.7, proj=('splaea', basemap)) -fig.format(suptitle='Zooming into polar projections') +basemap = pplt.Proj("npaeqd", boundinglat=60, backend="basemap") +fig, axs = pplt.subplots(ncols=2, refwidth=2.7, proj=("splaea", basemap)) +fig.format(suptitle="Zooming into polar projections") axs.format(land=True, latmax=80) # no gridlines poleward of 80 degrees -axs[0].format(boundinglat=-60, title='Cartopy example') -axs[1].format(title='Basemap example') +axs[0].format(boundinglat=-60, title="Cartopy example") +axs[1].format(title="Basemap example") # %% import proplot as pplt # Zooming in on continents fig = pplt.figure(refwidth=3) -ax = fig.subplot(121, proj='lcc', proj_kw={'lon0': 0}) -ax.format(lonlim=(-20, 50), latlim=(30, 70), title='Cartopy example') -proj = pplt.Proj('lcc', lon0=-100, lat0=45, width=8e6, height=8e6, backend='basemap') +ax = fig.subplot(121, proj="lcc", proj_kw={"lon0": 0}) +ax.format(lonlim=(-20, 50), latlim=(30, 70), title="Cartopy example") +proj = pplt.Proj("lcc", lon0=-100, lat0=45, width=8e6, height=8e6, backend="basemap") ax = fig.subplot(122, proj=proj) -ax.format(lonlines=20, title='Basemap example') -fig.format(suptitle='Zooming into specific regions', land=True) +ax.format(lonlines=20, title="Basemap example") +fig.format(suptitle="Zooming into specific regions", land=True) # %% import proplot as pplt # Zooming in with cartopy degree-minute-second labels -pplt.rc.reso = 'hi' +pplt.rc.reso = "hi" fig = pplt.figure(refwidth=2.5) -ax = fig.subplot(121, proj='cyl') +ax = fig.subplot(121, proj="cyl") ax.format(lonlim=(-7.5, 2), latlim=(49.5, 59)) -ax = fig.subplot(122, proj='cyl') +ax = fig.subplot(122, proj="cyl") ax.format(lonlim=(-6, -2), latlim=(54.5, 58.5)) fig.format( - land=True, labels=True, - borders=True, borderscolor='white', - suptitle='Cartopy degree-minute-second labels', + land=True, + labels=True, + borders=True, + borderscolor="white", + suptitle="Cartopy degree-minute-second labels", ) pplt.rc.reset() @@ -461,35 +527,73 @@ # Table of cartopy projections projs = [ - 'cyl', 'merc', 'mill', 'lcyl', 'tmerc', - 'robin', 'hammer', 'moll', 'kav7', 'aitoff', 'wintri', 'sinu', - 'geos', 'ortho', 'nsper', 'aea', 'eqdc', 'lcc', 'gnom', - 'npstere', 'nplaea', 'npaeqd', 'npgnom', 'igh', - 'eck1', 'eck2', 'eck3', 'eck4', 'eck5', 'eck6' + "cyl", + "merc", + "mill", + "lcyl", + "tmerc", + "robin", + "hammer", + "moll", + "kav7", + "aitoff", + "wintri", + "sinu", + "geos", + "ortho", + "nsper", + "aea", + "eqdc", + "lcc", + "gnom", + "npstere", + "nplaea", + "npaeqd", + "npgnom", + "igh", + "eck1", + "eck2", + "eck3", + "eck4", + "eck5", + "eck6", ] fig, axs = pplt.subplots(ncols=3, nrows=10, figwidth=7, proj=projs) -axs.format( - land=True, reso='lo', labels=False, - suptitle='Table of cartopy projections' -) +axs.format(land=True, reso="lo", labels=False, suptitle="Table of cartopy projections") for proj, ax in zip(projs, axs): - ax.format(title=proj, titleweight='bold', labels=False) + ax.format(title=proj, titleweight="bold", labels=False) # %% import proplot as pplt # Table of basemap projections projs = [ - 'cyl', 'merc', 'mill', 'cea', 'gall', 'sinu', - 'eck4', 'robin', 'moll', 'kav7', 'hammer', 'mbtfpq', - 'geos', 'ortho', 'nsper', - 'vandg', 'aea', 'eqdc', 'gnom', 'cass', 'lcc', - 'npstere', 'npaeqd', 'nplaea' + "cyl", + "merc", + "mill", + "cea", + "gall", + "sinu", + "eck4", + "robin", + "moll", + "kav7", + "hammer", + "mbtfpq", + "geos", + "ortho", + "nsper", + "vandg", + "aea", + "eqdc", + "gnom", + "cass", + "lcc", + "npstere", + "npaeqd", + "nplaea", ] -fig, axs = pplt.subplots(ncols=3, nrows=8, figwidth=7, proj=projs, backend='basemap') -axs.format( - land=True, labels=False, - suptitle='Table of basemap projections' -) +fig, axs = pplt.subplots(ncols=3, nrows=8, figwidth=7, proj=projs, backend="basemap") +axs.format(land=True, labels=False, suptitle="Table of basemap projections") for proj, ax in zip(projs, axs): - ax.format(title=proj, titleweight='bold', labels=False) + ax.format(title=proj, titleweight="bold", labels=False) diff --git a/docs/sphinxext/custom_roles.py b/docs/sphinxext/custom_roles.py index 36a4e4570..465a563cb 100644 --- a/docs/sphinxext/custom_roles.py +++ b/docs/sphinxext/custom_roles.py @@ -14,22 +14,24 @@ def _node_list(rawtext, text, inliner): """ Return a singleton node list or an empty list if source is unknown. """ - source = inliner.document.attributes['source'].replace(os.path.sep, '/') - relsource = source.split('/docs/', 1) + source = inliner.document.attributes["source"].replace(os.path.sep, "/") + relsource = source.split("/docs/", 1) if len(relsource) == 1: return [] if text in rcParams: - refuri = 'https://matplotlib.org/stable/tutorials/introductory/customizing.html' - refuri = f'{refuri}?highlight={text}#the-matplotlibrc-file' + refuri = "https://matplotlib.org/stable/tutorials/introductory/customizing.html" + refuri = f"{refuri}?highlight={text}#the-matplotlibrc-file" else: - path = '../' * relsource[1].count('/') + 'en/stable' - refuri = f'{path}/configuration.html?highlight={text}#table-of-settings' - node = nodes.Text(f'rc[{text!r}]' if '.' in text else f'rc.{text}') + path = "../" * relsource[1].count("/") + "en/stable" + refuri = f"{path}/configuration.html?highlight={text}#table-of-settings" + node = nodes.Text(f"rc[{text!r}]" if "." in text else f"rc.{text}") ref = nodes.reference(rawtext, node, refuri=refuri) - return [nodes.literal('', '', ref)] + return [nodes.literal("", "", ref)] -def rc_raw_role(name, rawtext, text, lineno, inliner, options={}, content=[]): # noqa: U100, E501 +def rc_raw_role( + name, rawtext, text, lineno, inliner, options={}, content=[] +): # noqa: U100, E501 """ The :rcraw: role. Includes a link to the setting. """ @@ -47,8 +49,8 @@ def rc_role(name, rawtext, text, lineno, inliner, options={}, content=[]): # no except KeyError: pass else: - node_list.append(nodes.Text(' = ')) - node_list.append(nodes.literal('', '', nodes.Text(repr(default)))) + node_list.append(nodes.Text(" = ")) + node_list.append(nodes.literal("", "", nodes.Text(repr(default)))) return node_list, [] @@ -56,6 +58,6 @@ def setup(app): """ Set up the roles. """ - app.add_role('rc', rc_role) - app.add_role('rcraw', rc_raw_role) - return {'parallel_read_safe': True, 'parallel_write_safe': True} + app.add_role("rc", rc_role) + app.add_role("rcraw", rc_raw_role) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/stats.py b/docs/stats.py index b4bdb453e..f401165b0 100644 --- a/docs/stats.py +++ b/docs/stats.py @@ -68,8 +68,8 @@ data = state.rand(20, 8).cumsum(axis=0).cumsum(axis=1)[:, ::-1] data = data + 20 * state.normal(size=(20, 8)) + 30 data = pd.DataFrame(data, columns=np.arange(0, 16, 2)) -data.columns.name = 'column number' -data.name = 'variable' +data.columns.name = "column number" +data.name = "variable" # Calculate error data # Passed to 'errdata' in the 3rd subplot example @@ -85,51 +85,65 @@ # Loop through "vertical" and "horizontal" versions varray = [[1], [2], [3]] harray = [[1, 1], [2, 3], [2, 3]] -for orientation, array in zip(('vertical', 'horizontal'), (varray, harray)): +for orientation, array in zip(("vertical", "horizontal"), (varray, harray)): # Figure fig = pplt.figure(refwidth=4, refaspect=1.5, share=False) axs = fig.subplots(array, hratios=(2, 1, 1)) - axs.format(abc='A.', suptitle=f'Indicating {orientation} error bounds') + axs.format(abc="A.", suptitle=f"Indicating {orientation} error bounds") # Medians and percentile ranges ax = axs[0] kw = dict( - color='light red', edgecolor='k', legend=True, - median=True, barpctile=90, boxpctile=True, + color="light red", + edgecolor="k", + legend=True, + median=True, + barpctile=90, + boxpctile=True, # median=True, barpctile=(5, 95), boxpctile=(25, 75) # equivalent ) - if orientation == 'horizontal': + if orientation == "horizontal": ax.barh(data, **kw) else: ax.bar(data, **kw) - ax.format(title='Bar plot') + ax.format(title="Bar plot") # Means and standard deviation range ax = axs[1] kw = dict( - color='denim', marker='x', markersize=8**2, linewidth=0.8, - label='mean', shadelabel=True, - mean=True, shadestd=1, + color="denim", + marker="x", + markersize=8**2, + linewidth=0.8, + label="mean", + shadelabel=True, + mean=True, + shadestd=1, # mean=True, shadestd=(-1, 1) # equivalent ) - if orientation == 'horizontal': - ax.scatterx(data, legend='b', legend_kw={'ncol': 1}, **kw) + if orientation == "horizontal": + ax.scatterx(data, legend="b", legend_kw={"ncol": 1}, **kw) else: - ax.scatter(data, legend='ll', **kw) - ax.format(title='Marker plot') + ax.scatter(data, legend="ll", **kw) + ax.format(title="Marker plot") # User-defined error bars ax = axs[2] kw = dict( - shadedata=shadedata, fadedata=fadedata, - label='mean', shadelabel='50% CI', fadelabel='90% CI', - color='ocean blue', barzorder=0, boxmarker=False, + shadedata=shadedata, + fadedata=fadedata, + label="mean", + shadelabel="50% CI", + fadelabel="90% CI", + color="ocean blue", + barzorder=0, + boxmarker=False, ) - if orientation == 'horizontal': - ax.linex(means, legend='b', legend_kw={'ncol': 1}, **kw) + if orientation == "horizontal": + ax.linex(means, legend="b", legend_kw={"ncol": 1}, **kw) else: - ax.line(means, legend='ll', **kw) - ax.format(title='Line plot') + ax.line(means, legend="ll", **kw) + ax.format(title="Line plot") # %% [raw] raw_mimetype="text/restructuredtext" @@ -158,31 +172,28 @@ N = 500 state = np.random.RandomState(51423) data1 = state.normal(size=(N, 5)) + 2 * (state.rand(N, 5) - 0.5) * np.arange(5) -data1 = pd.DataFrame(data1, columns=pd.Index(list('abcde'), name='label')) +data1 = pd.DataFrame(data1, columns=pd.Index(list("abcde"), name="label")) data2 = state.rand(100, 7) -data2 = pd.DataFrame(data2, columns=pd.Index(list('abcdefg'), name='label')) +data2 = pd.DataFrame(data2, columns=pd.Index(list("abcdefg"), name="label")) # Figure fig, axs = pplt.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], span=False) -axs.format( - abc='A.', titleloc='l', grid=False, - suptitle='Boxes and violins demo' -) +axs.format(abc="A.", titleloc="l", grid=False, suptitle="Boxes and violins demo") # Box plots ax = axs[0] -obj1 = ax.box(data1, means=True, marker='x', meancolor='r', fillcolor='gray4') -ax.format(title='Box plots') +obj1 = ax.box(data1, means=True, marker="x", meancolor="r", fillcolor="gray4") +ax.format(title="Box plots") # Violin plots ax = axs[1] -obj2 = ax.violin(data1, fillcolor='gray6', means=True, points=100) -ax.format(title='Violin plots') +obj2 = ax.violin(data1, fillcolor="gray6", means=True, points=100) +ax.format(title="Violin plots") # Boxes with different colors ax = axs[2] -ax.boxh(data2, cycle='pastel2') -ax.format(title='Multiple colors', ymargin=0.15) +ax.boxh(data2, cycle="pastel2") +ax.format(title="Multiple colors", ymargin=0.15) # %% [raw] raw_mimetype="text/restructuredtext" tags=[] @@ -220,10 +231,16 @@ # Sample overlayed histograms fig, ax = pplt.subplots(refwidth=4, refaspect=(3, 2)) -ax.format(suptitle='Overlaid histograms', xlabel='distribution', ylabel='count') +ax.format(suptitle="Overlaid histograms", xlabel="distribution", ylabel="count") res = ax.hist( - x, pplt.arange(-3, 8, 0.2), filled=True, alpha=0.7, edgecolor='k', - cycle=('indigo9', 'gray3', 'red9'), labels=list('abc'), legend='ul', + x, + pplt.arange(-3, 8, 0.2), + filled=True, + alpha=0.7, + edgecolor="k", + cycle=("indigo9", "gray3", "red9"), + labels=list("abc"), + legend="ul", ) # %% @@ -240,20 +257,30 @@ # Histogram with marginal distributions fig, axs = pplt.subplots(ncols=2, refwidth=2.3) axs.format( - abc='A.', abcloc='l', titleabove=True, - ylabel='y axis', suptitle='Histograms with marginal distributions' + abc="A.", + abcloc="l", + titleabove=True, + ylabel="y axis", + suptitle="Histograms with marginal distributions", ) -colors = ('indigo9', 'red9') -titles = ('Group 1', 'Group 2') -for ax, which, color, title in zip(axs, 'lr', colors, titles): +colors = ("indigo9", "red9") +titles = ("Group 1", "Group 2") +for ax, which, color, title in zip(axs, "lr", colors, titles): ax.hist2d( - x, y, bins, vmin=0, vmax=10, levels=50, - cmap=color, colorbar='b', colorbar_kw={'label': 'count'} + x, + y, + bins, + vmin=0, + vmax=10, + levels=50, + cmap=color, + colorbar="b", + colorbar_kw={"label": "count"}, ) color = pplt.scale_luminance(color, 1.5) # histogram colors px = ax.panel(which, space=0) - px.histh(y, bins, color=color, fill=True, ec='k') - px.format(grid=False, xlocator=[], xreverse=(which == 'l')) - px = ax.panel('t', space=0) - px.hist(x, bins, color=color, fill=True, ec='k') - px.format(grid=False, ylocator=[], title=title, titleloc='l') + px.histh(y, bins, color=color, fill=True, ec="k") + px.format(grid=False, xlocator=[], xreverse=(which == "l")) + px = ax.panel("t", space=0) + px.hist(x, bins, color=color, fill=True, ec="k") + px.format(grid=False, ylocator=[], title=title, titleloc="l") diff --git a/docs/subplots.py b/docs/subplots.py index 0af5e74b4..05a437d93 100644 --- a/docs/subplots.py +++ b/docs/subplots.py @@ -78,26 +78,36 @@ # %% import proplot as pplt -fig = pplt.figure(space=0, refwidth='10em') + +fig = pplt.figure(space=0, refwidth="10em") axs = fig.subplots(nrows=3, ncols=3) axs.format( - abc='A.', abcloc='ul', - xticks='null', yticks='null', facecolor='gray5', - xlabel='x axis', ylabel='y axis', - suptitle='A-b-c label offsetting, borders, and boxes', + abc="A.", + abcloc="ul", + xticks="null", + yticks="null", + facecolor="gray5", + xlabel="x axis", + ylabel="y axis", + suptitle="A-b-c label offsetting, borders, and boxes", ) -axs[:3].format(abcloc='l', titleloc='l', title='Title') +axs[:3].format(abcloc="l", titleloc="l", title="Title") axs[-3:].format(abcbbox=True) # also disables abcborder # axs[:-3].format(abcborder=True) # this is already the default # %% import proplot as pplt + fig = pplt.figure(space=0, refwidth=0.7) axs = fig.subplots(nrows=8, ncols=8) axs.format( - abc=True, abcloc='ur', - xlabel='x axis', ylabel='y axis', xticks=[], yticks=[], - suptitle='A-b-c label stress test' + abc=True, + abcloc="ur", + xlabel="x axis", + ylabel="y axis", + xticks=[], + yticks=[], + suptitle="A-b-c label stress test", ) @@ -177,40 +187,44 @@ state = np.random.RandomState(51423) colors = np.tile(state.rand(8, 12, 1), (1, 1, 3)) fig, axs = pplt.subplots(ncols=3, nrows=2, refwidth=1.7) -fig.format(suptitle='Auto figure dimensions for grid of images') +fig.format(suptitle="Auto figure dimensions for grid of images") for ax in axs: ax.imshow(colors) # Grid of cartopy projections -fig, axs = pplt.subplots(ncols=2, nrows=3, proj='robin') -axs.format(land=True, landcolor='k') -fig.format(suptitle='Auto figure dimensions for grid of cartopy projections') +fig, axs = pplt.subplots(ncols=2, nrows=3, proj="robin") +axs.format(land=True, landcolor="k") +fig.format(suptitle="Auto figure dimensions for grid of cartopy projections") # %% import proplot as pplt -pplt.rc.update(grid=False, titleloc='uc', titleweight='bold', titlecolor='red9') + +pplt.rc.update(grid=False, titleloc="uc", titleweight="bold", titlecolor="red9") # Change the reference subplot width -suptitle = 'Effect of subplot width on figure size' -for refwidth in ('3cm', '5cm'): - fig, axs = pplt.subplots(ncols=2, refwidth=refwidth,) - axs[0].format(title=f'refwidth = {refwidth}', suptitle=suptitle) +suptitle = "Effect of subplot width on figure size" +for refwidth in ("3cm", "5cm"): + fig, axs = pplt.subplots( + ncols=2, + refwidth=refwidth, + ) + axs[0].format(title=f"refwidth = {refwidth}", suptitle=suptitle) # Change the reference subplot aspect ratio -suptitle = 'Effect of subplot aspect ratio on figure size' +suptitle = "Effect of subplot aspect ratio on figure size" for refaspect in (1, 2): fig, axs = pplt.subplots(ncols=2, refwidth=1.6, refaspect=refaspect) - axs[0].format(title=f'refaspect = {refaspect}', suptitle=suptitle) + axs[0].format(title=f"refaspect = {refaspect}", suptitle=suptitle) # Change the reference subplot -suptitle = 'Effect of reference subplot on figure size' +suptitle = "Effect of reference subplot on figure size" for ref in (1, 2): # with different width ratios fig, axs = pplt.subplots(ncols=3, wratios=(3, 2, 2), ref=ref, refwidth=1.1) - axs[ref - 1].format(title='reference', suptitle=suptitle) + axs[ref - 1].format(title="reference", suptitle=suptitle) for ref in (1, 2): # with complex subplot grid fig, axs = pplt.subplots([[1, 2], [1, 3]], refnum=ref, refwidth=1.8) - axs[ref - 1].format(title='reference', suptitle=suptitle) + axs[ref - 1].format(title="reference", suptitle=suptitle) pplt.rc.reset() @@ -272,25 +286,33 @@ # Stress test of the tight layout algorithm # This time override the algorithm between selected subplot rows/columns fig, axs = pplt.subplots( - ncols=4, nrows=3, refwidth=1.1, span=False, - bottom='5em', right='5em', # margin spacing overrides - wspace=(0, 0, None), hspace=(0, None), # column and row spacing overrides + ncols=4, + nrows=3, + refwidth=1.1, + span=False, + bottom="5em", + right="5em", # margin spacing overrides + wspace=(0, 0, None), + hspace=(0, None), # column and row spacing overrides ) axs.format( grid=False, - xlocator=1, ylocator=1, tickdir='inout', - xlim=(-1.5, 1.5), ylim=(-1.5, 1.5), - suptitle='Tight layout with user overrides', - toplabels=('Column 1', 'Column 2', 'Column 3', 'Column 4'), - leftlabels=('Row 1', 'Row 2', 'Row 3'), + xlocator=1, + ylocator=1, + tickdir="inout", + xlim=(-1.5, 1.5), + ylim=(-1.5, 1.5), + suptitle="Tight layout with user overrides", + toplabels=("Column 1", "Column 2", "Column 3", "Column 4"), + leftlabels=("Row 1", "Row 2", "Row 3"), ) -axs[0, :].format(xtickloc='top') -axs[2, :].format(xtickloc='both') -axs[:, 1].format(ytickloc='neither') -axs[:, 2].format(ytickloc='right') -axs[:, 3].format(ytickloc='both') -axs[-1, :].format(xlabel='xlabel', title='Title\nTitle\nTitle') -axs[:, 0].format(ylabel='ylabel') +axs[0, :].format(xtickloc="top") +axs[2, :].format(xtickloc="both") +axs[:, 1].format(ytickloc="neither") +axs[:, 2].format(ytickloc="right") +axs[:, 3].format(ytickloc="both") +axs[-1, :].format(xlabel="xlabel", title="Title\nTitle\nTitle") +axs[:, 0].format(ylabel="ylabel") # %% @@ -298,26 +320,28 @@ # Stress test of the tight layout algorithm # Add large labels along the edge of one subplot -equals = [('unequal', False), ('unequal', False), ('equal', True)] -groups = [('grouped', True), ('ungrouped', False), ('grouped', True)] +equals = [("unequal", False), ("unequal", False), ("equal", True)] +groups = [("grouped", True), ("ungrouped", False), ("grouped", True)] for (name1, equal), (name2, group) in zip(equals, groups): - suffix = ' (default)' if group and not equal else '' + suffix = " (default)" if group and not equal else "" suptitle = f'Tight layout with "{name1}" and "{name2}" row-column spacing{suffix}' fig, axs = pplt.subplots( - nrows=3, ncols=3, refwidth=1.1, share=False, equal=equal, group=group, - ) - axs[1].format( - xlabel='xlabel\nxlabel', - ylabel='ylabel\nylabel\nylabel\nylabel' + nrows=3, + ncols=3, + refwidth=1.1, + share=False, + equal=equal, + group=group, ) + axs[1].format(xlabel="xlabel\nxlabel", ylabel="ylabel\nylabel\nylabel\nylabel") axs[3:6:2].format( - title='Title\nTitle', - titlesize='med', + title="Title\nTitle", + titlesize="med", ) axs.format( grid=False, - toplabels=('Column 1', 'Column 2', 'Column 3'), - leftlabels=('Row 1', 'Row 2', 'Row 3'), + toplabels=("Column 1", "Column 2", "Column 3"), + leftlabels=("Row 1", "Row 2", "Row 3"), suptitle=suptitle, ) @@ -367,50 +391,59 @@ # %% import proplot as pplt import numpy as np + N = 50 M = 40 state = np.random.RandomState(51423) -cycle = pplt.Cycle('grays_r', M, left=0.1, right=0.8) +cycle = pplt.Cycle("grays_r", M, left=0.1, right=0.8) datas = [] for scale in (1, 3, 7, 0.2): - data = scale * (state.rand(N, M) - 0.5).cumsum(axis=0)[N // 2:, :] + data = scale * (state.rand(N, M) - 0.5).cumsum(axis=0)[N // 2 :, :] datas.append(data) # Plots with different sharing and spanning settings # Note that span=True and share=True are the defaults spans = (False, False, True, True) -shares = (False, 'labels', 'limits', True) +shares = (False, "labels", "limits", True) for i, (span, share) in enumerate(zip(spans, shares)): fig = pplt.figure(refaspect=1, refwidth=1.06, spanx=span, sharey=share) axs = fig.subplots(ncols=4) for ax, data in zip(axs, datas): - on = ('off', 'on')[int(span)] + on = ("off", "on")[int(span)] ax.plot(data, cycle=cycle) ax.format( - grid=False, xlabel='spanning axis', ylabel='shared axis', - suptitle=f'Sharing mode {share!r} (level {i}) with spanning labels {on}' + grid=False, + xlabel="spanning axis", + ylabel="shared axis", + suptitle=f"Sharing mode {share!r} (level {i}) with spanning labels {on}", ) # %% import proplot as pplt import numpy as np + state = np.random.RandomState(51423) # Plots with minimum and maximum sharing settings # Note that all x and y axis limits and ticks are identical spans = (False, True) -shares = (False, 'all') -titles = ('Minimum sharing', 'Maximum sharing') +shares = (False, "all") +titles = ("Minimum sharing", "Maximum sharing") for span, share, title in zip(spans, shares, titles): fig = pplt.figure(refwidth=1, span=span, share=share) axs = fig.subplots(nrows=4, ncols=4) for ax in axs: data = (state.rand(100, 20) - 0.4).cumsum(axis=0) - ax.plot(data, cycle='Set3') + ax.plot(data, cycle="Set3") axs.format( - abc=True, abcloc='ul', suptitle=title, - xlabel='xlabel', ylabel='ylabel', - grid=False, xticks=25, yticks=5 + abc=True, + abcloc="ul", + suptitle=title, + xlabel="xlabel", + ylabel="ylabel", + grid=False, + xticks=25, + yticks=5, ) @@ -449,17 +482,27 @@ # %% import proplot as pplt import numpy as np -with pplt.rc.context(fontsize='12px'): # depends on rc['figure.dpi'] + +with pplt.rc.context(fontsize="12px"): # depends on rc['figure.dpi'] fig, axs = pplt.subplots( - ncols=3, figwidth='15cm', figheight='3in', - wspace=('10pt', '20pt'), right='10mm', + ncols=3, + figwidth="15cm", + figheight="3in", + wspace=("10pt", "20pt"), + right="10mm", ) cb = fig.colorbar( - 'Mono', loc='b', extend='both', label='colorbar', - width='2em', extendsize='3em', shrink=0.8, + "Mono", + loc="b", + extend="both", + label="colorbar", + width="2em", + extendsize="3em", + shrink=0.8, ) - pax = axs[2].panel_axes('r', width='5en') + pax = axs[2].panel_axes("r", width="5en") axs.format( - suptitle='Arguments with arbitrary units', - xlabel='x axis', ylabel='y axis', + suptitle="Arguments with arbitrary units", + xlabel="x axis", + ylabel="y axis", ) diff --git a/proplot/__init__.py b/proplot/__init__.py index a4809e6d5..e6bb9e3fb 100644 --- a/proplot/__init__.py +++ b/proplot/__init__.py @@ -3,97 +3,113 @@ A succinct matplotlib wrapper for making beautiful, publication-quality graphics. """ # SCM versioning -import pkg_resources as pkg -name = 'proplot' +name = "proplot" + +try: + from importlib.metadata import version as get_version +except ImportError: # for Python < 3.8 + from importlib_metadata import version as get_version try: - version = __version__ = pkg.get_distribution(__name__).version -except pkg.DistributionNotFound: - version = __version__ = 'unknown' + version = __version__ = get_version(name) +except Exception: + version = __version__ = "unknown" # Import dependencies early to isolate import times from . import internals, externals, tests # noqa: F401 from .internals.benchmarks import _benchmark -with _benchmark('pyplot'): + +with _benchmark("pyplot"): from matplotlib import pyplot # noqa: F401 -with _benchmark('cartopy'): +with _benchmark("cartopy"): try: import cartopy # noqa: F401 except ImportError: pass -with _benchmark('basemap'): +with _benchmark("basemap"): try: from mpl_toolkits import basemap # noqa: F401 except ImportError: pass # Import everything to top level -with _benchmark('config'): +with _benchmark("config"): from .config import * # noqa: F401 F403 -with _benchmark('proj'): +with _benchmark("proj"): from .proj import * # noqa: F401 F403 -with _benchmark('utils'): +with _benchmark("utils"): from .utils import * # noqa: F401 F403 -with _benchmark('colors'): +with _benchmark("colors"): from .colors import * # noqa: F401 F403 -with _benchmark('ticker'): +with _benchmark("ticker"): from .ticker import * # noqa: F401 F403 -with _benchmark('scale'): +with _benchmark("scale"): from .scale import * # noqa: F401 F403 -with _benchmark('axes'): +with _benchmark("axes"): from .axes import * # noqa: F401 F403 -with _benchmark('gridspec'): +with _benchmark("gridspec"): from .gridspec import * # noqa: F401 F403 -with _benchmark('figure'): +with _benchmark("figure"): from .figure import * # noqa: F401 F403 -with _benchmark('constructor'): +with _benchmark("constructor"): from .constructor import * # noqa: F401 F403 -with _benchmark('ui'): +with _benchmark("ui"): from .ui import * # noqa: F401 F403 -with _benchmark('demos'): +with _benchmark("demos"): from .demos import * # noqa: F401 F403 # Dynamically add registered classes to top-level namespace from . import proj as crs # backwards compatibility # noqa: F401 from .constructor import NORMS, LOCATORS, FORMATTERS, SCALES, PROJS + _globals = globals() for _src in (NORMS, LOCATORS, FORMATTERS, SCALES, PROJS): for _key, _cls in _src.items(): if isinstance(_cls, type): # i.e. not a scale preset _globals[_cls.__name__] = _cls # may overwrite proplot names - # Register objects from .config import register_cmaps, register_cycles, register_colors, register_fonts -with _benchmark('cmaps'): + +with _benchmark("cmaps"): register_cmaps(default=True) -with _benchmark('cycles'): +with _benchmark("cycles"): register_cycles(default=True) -with _benchmark('colors'): +with _benchmark("colors"): register_colors(default=True) -with _benchmark('fonts'): +with _benchmark("fonts"): register_fonts(default=True) # Validate colormap names and propagate 'cycle' to 'axes.prop_cycle' # NOTE: cmap.sequential also updates siblings 'cmap' and 'image.cmap' from .config import rc from .internals import rcsetup, warnings + + rcsetup.VALIDATE_REGISTERED_CMAPS = True -for _key in ('cycle', 'cmap.sequential', 'cmap.diverging', 'cmap.cyclic', 'cmap.qualitative'): # noqa: E501 +for _key in ( + "cycle", + "cmap.sequential", + "cmap.diverging", + "cmap.cyclic", + "cmap.qualitative", +): # noqa: E501 try: rc[_key] = rc[_key] except ValueError as err: - warnings._warn_proplot(f'Invalid user rc file setting: {err}') - rc[_key] = 'Greys' # fill value + warnings._warn_proplot(f"Invalid user rc file setting: {err}") + rc[_key] = "Greys" # fill value # Validate color names now that colors are registered # NOTE: This updates all settings with 'color' in name (harmless if it's not a color) from .config import rc_proplot, rc_matplotlib + rcsetup.VALIDATE_REGISTERED_COLORS = True for _src in (rc_proplot, rc_matplotlib): for _key in _src: # loop through unsynced properties - if 'color' not in _key: + if "color" not in _key: continue try: _src[_key] = _src[_key] except ValueError as err: - warnings._warn_proplot(f'Invalid user rc file setting: {err}') - _src[_key] = 'black' # fill value + warnings._warn_proplot(f"Invalid user rc file setting: {err}") + _src[_key] = "black" # fill value +from .colors import _cmap_database as colormaps diff --git a/proplot/axes/__init__.py b/proplot/axes/__init__.py index 413f28c97..96ad2dde3 100644 --- a/proplot/axes/__init__.py +++ b/proplot/axes/__init__.py @@ -16,12 +16,12 @@ # Prevent importing module names and set order of appearance for objects __all__ = [ - 'Axes', - 'PlotAxes', - 'CartesianAxes', - 'PolarAxes', - 'GeoAxes', - 'ThreeAxes', + "Axes", + "PlotAxes", + "CartesianAxes", + "PolarAxes", + "GeoAxes", + "ThreeAxes", ] # Register projections with package prefix to avoid conflicts @@ -30,11 +30,13 @@ _cls_dict = {} # track valid names for _cls in (CartesianAxes, PolarAxes, _CartopyAxes, _BasemapAxes, ThreeAxes): for _name in (_cls._name, *_cls._name_aliases): - with context._state_context(_cls, name='proplot_' + _name): + with context._state_context(_cls, name="proplot_" + _name): mproj.register_projection(_cls) _cls_dict[_name] = _cls -_cls_table = '\n'.join( - ' ' + key + ' ' * (max(map(len, _cls_dict)) - len(key) + 7) - + ('GeoAxes' if cls.__name__[:1] == '_' else cls.__name__) +_cls_table = "\n".join( + " " + + key + + " " * (max(map(len, _cls_dict)) - len(key) + 7) + + ("GeoAxes" if cls.__name__[:1] == "_" else cls.__name__) for key, cls in _cls_dict.items() ) diff --git a/proplot/axes/base.py b/proplot/axes/base.py index 95bd06bdb..9806090b2 100644 --- a/proplot/axes/base.py +++ b/proplot/axes/base.py @@ -51,41 +51,33 @@ except Exception: CRS = PlateCarree = object -__all__ = ['Axes'] +__all__ = ["Axes"] # A-b-c label string -ABC_STRING = 'abcdefghijklmnopqrstuvwxyz' +ABC_STRING = "abcdefghijklmnopqrstuvwxyz" # Legend align options ALIGN_OPTS = { None: { - 'center': 'center', - 'left': 'center left', - 'right': 'center right', - 'top': 'upper center', - 'bottom': 'lower center', + "center": "center", + "left": "center left", + "right": "center right", + "top": "upper center", + "bottom": "lower center", }, - 'left': { - 'top': 'upper right', - 'center': 'center right', - 'bottom': 'lower right', + "left": { + "top": "upper right", + "center": "center right", + "bottom": "lower right", }, - 'right': { - 'top': 'upper left', - 'center': 'center left', - 'bottom': 'lower left', - }, - 'top': { - 'left': 'lower left', - 'center': 'lower center', - 'right': 'lower right' - }, - 'bottom': { - 'left': 'upper left', - 'center': 'upper center', - 'right': 'upper right' + "right": { + "top": "upper left", + "center": "center left", + "bottom": "lower left", }, + "top": {"left": "lower left", "center": "lower center", "right": "lower right"}, + "bottom": {"left": "upper left", "center": "upper center", "right": "upper right"}, } @@ -111,9 +103,9 @@ Whether to use `~mpl_toolkits.basemap.Basemap` or `~cartopy.crs.Projection` for map projections. """ -docstring._snippet_manager['axes.proj'] = _proj_docstring -docstring._snippet_manager['axes.proj_kw'] = _proj_kw_docstring -docstring._snippet_manager['axes.backend'] = _backend_docstring +docstring._snippet_manager["axes.proj"] = _proj_docstring +docstring._snippet_manager["axes.proj_kw"] = _proj_kw_docstring +docstring._snippet_manager["axes.backend"] = _backend_docstring # Colorbar and legend space @@ -143,11 +135,11 @@ and ``'left'`` and ``'right'`` are valid for top and bottom {name}s. The default is always ``'center'``. """ -docstring._snippet_manager['axes.legend_space'] = _space_docstring.format( - name='legend', default='legend.borderaxespad' +docstring._snippet_manager["axes.legend_space"] = _space_docstring.format( + name="legend", default="legend.borderaxespad" ) -docstring._snippet_manager['axes.colorbar_space'] = _space_docstring.format( - name='colorbar', default='colorbar.insetpad' +docstring._snippet_manager["axes.colorbar_space"] = _space_docstring.format( + name="colorbar", default="colorbar.insetpad" ) @@ -162,7 +154,7 @@ `~matplotlib.figure.Figure.transFigure`, or `~matplotlib.figure.Figure.transSubfigure`, transforms. """ -docstring._snippet_manager['axes.transform'] = _transform_docstring +docstring._snippet_manager["axes.transform"] = _transform_docstring # Inset docstring @@ -235,8 +227,8 @@ matplotlib.axes.Axes.indicate_inset matplotlib.axes.Axes.indicate_inset_zoom """ -docstring._snippet_manager['axes.inset'] = _inset_docstring -docstring._snippet_manager['axes.indicate_inset'] = _indicate_inset_docstring +docstring._snippet_manager["axes.inset"] = _inset_docstring +docstring._snippet_manager["axes.indicate_inset"] = _indicate_inset_docstring # Panel docstring @@ -290,8 +282,8 @@ proplot.axes.CartesianAxes The panel axes. """ -docstring._snippet_manager['axes.panel_loc'] = _panel_loc_docstring -docstring._snippet_manager['axes.panel'] = _panel_docstring +docstring._snippet_manager["axes.panel_loc"] = _panel_loc_docstring +docstring._snippet_manager["axes.panel"] = _panel_docstring # Format docstrings @@ -406,12 +398,12 @@ of the keyword arguments documented above are internally applied by retrieving settings passed to `~proplot.config.Configurator.context`. """ -docstring._snippet_manager['rc.init'] = _rc_format_docstring.format( - 'Remaining keyword arguments are passed to `matplotlib.axes.Axes`.\n ' +docstring._snippet_manager["rc.init"] = _rc_format_docstring.format( + "Remaining keyword arguments are passed to `matplotlib.axes.Axes`.\n " ) -docstring._snippet_manager['rc.format'] = _rc_format_docstring.format('') -docstring._snippet_manager['axes.format'] = _axes_format_docstring -docstring._snippet_manager['figure.format'] = _figure_format_docstring +docstring._snippet_manager["rc.format"] = _rc_format_docstring.format("") +docstring._snippet_manager["axes.format"] = _axes_format_docstring +docstring._snippet_manager["figure.format"] = _figure_format_docstring # Colorbar docstrings @@ -558,9 +550,9 @@ this specific linewidth is used to cover up the white lines. This feature is automatically disabled when the patches have transparency. """ -docstring._snippet_manager['axes.edgefix'] = _edgefix_docstring -docstring._snippet_manager['axes.colorbar_args'] = _colorbar_args_docstring -docstring._snippet_manager['axes.colorbar_kwargs'] = _colorbar_kwargs_docstring +docstring._snippet_manager["axes.edgefix"] = _edgefix_docstring +docstring._snippet_manager["axes.colorbar_args"] = _colorbar_args_docstring +docstring._snippet_manager["axes.colorbar_kwargs"] = _colorbar_kwargs_docstring # Legend docstrings @@ -637,22 +629,22 @@ **kwargs Passed to `~matplotlib.axes.Axes.legend`. """ -docstring._snippet_manager['axes.legend_args'] = _legend_args_docstring -docstring._snippet_manager['axes.legend_kwargs'] = _legend_kwargs_docstring +docstring._snippet_manager["axes.legend_args"] = _legend_args_docstring +docstring._snippet_manager["axes.legend_kwargs"] = _legend_kwargs_docstring def _align_bbox(align, length): """ Return a simple alignment bounding box for intersection calculations. """ - if align in ('left', 'bottom'): + if align in ("left", "bottom"): bounds = [[0, 0], [length, 0]] - elif align in ('top', 'right'): + elif align in ("top", "right"): bounds = [[1 - length, 0], [1, 0]] - elif align == 'center': + elif align == "center": bounds = [[0.5 * (1 - length), 0], [0.5 * (1 + length), 0]] else: - raise ValueError(f'Invalid align {align!r}.') + raise ValueError(f"Invalid align {align!r}.") return mtransforms.Bbox(bounds) @@ -660,12 +652,13 @@ class _TransformedBoundsLocator: """ Axes locator for `~Axes.inset_axes` and other axes. """ + def __init__(self, bounds, transform): self._bounds = bounds self._transform = transform def __call__(self, ax, renderer): # noqa: U100 - transfig = getattr(ax.figure, 'transSubfigure', ax.figure.transFigure) + transfig = getattr(ax.figure, "transSubfigure", ax.figure.transFigure) bbox = mtransforms.Bbox.from_bounds(*self._bounds) bbox = mtransforms.TransformedBbox(bbox, self._transform) bbox = mtransforms.TransformedBbox(bbox, transfig.inverted()) @@ -677,6 +670,7 @@ class Axes(maxes.Axes): The lowest-level `~matplotlib.axes.Axes` subclass used by proplot. Implements basic universal features. """ + _name = None # derived must override _name_aliases = () _make_inset_locator = _TransformedBoundsLocator @@ -688,33 +682,39 @@ def __repr__(self): # minor releases) because native __repr__ is defined in SubplotBase. ax = self._get_topmost_axes() name = type(self).__name__ - prefix = '' if ax is self else 'parent_' + prefix = "" if ax is self else "parent_" params = {} - if self._name in ('cartopy', 'basemap'): - name = name.replace('_' + self._name.title(), 'Geo') - params['backend'] = self._name + if self._name in ("cartopy", "basemap"): + name = name.replace("_" + self._name.title(), "Geo") + params["backend"] = self._name if self._inset_parent: - name = re.sub('Axes(Subplot)?', 'AxesInset', name) - params['bounds'] = tuple(np.round(self._inset_bounds, 2)) + name = re.sub("Axes(Subplot)?", "AxesInset", name) + params["bounds"] = tuple(np.round(self._inset_bounds, 2)) if self._altx_parent or self._alty_parent: - name = re.sub('Axes(Subplot)?', 'AxesTwin', name) - params['axis'] = 'x' if self._altx_parent else 'y' + name = re.sub("Axes(Subplot)?", "AxesTwin", name) + params["axis"] = "x" if self._altx_parent else "y" if self._colorbar_fill: - name = re.sub('Axes(Subplot)?', 'AxesFill', name) - params['side'] = self._axes._panel_side + name = re.sub("Axes(Subplot)?", "AxesFill", name) + params["side"] = self._axes._panel_side if self._panel_side: - name = re.sub('Axes(Subplot)?', 'AxesPanel', name) - params['side'] = self._panel_side + name = re.sub("Axes(Subplot)?", "AxesPanel", name) + params["side"] = self._panel_side try: - nrows, ncols, num1, num2 = ax.get_subplotspec().get_topmost_subplotspec()._get_geometry() # noqa: E501 - params[prefix + 'index'] = (num1, num2) + nrows, ncols, num1, num2 = ( + ax.get_subplotspec().get_topmost_subplotspec()._get_geometry() + ) # noqa: E501 + params[prefix + "index"] = (num1, num2) except (IndexError, ValueError, AttributeError): # e.g. a loose axes left, bottom, width, height = np.round(self._position.bounds, 2) - params['left'], params['bottom'], params['size'] = (left, bottom, (width, bottom)) # noqa: E501 + params["left"], params["bottom"], params["size"] = ( + left, + bottom, + (width, bottom), + ) # noqa: E501 if ax.number: - params[prefix + 'number'] = ax.number - params = ', '.join(f'{key}={value!r}' for key, value in params.items()) - return f'{name}({params})' + params[prefix + "number"] = ax.number + params = ", ".join(f"{key}={value!r}" for key, value in params.items()) + return f"{name}({params})" def __str__(self): return self.__repr__() @@ -745,31 +745,31 @@ def __init__(self, *args, **kwargs): """ # Remove subplot-related args # NOTE: These are documented on add_subplot() - ss = kwargs.pop('_subplot_spec', None) # see below - number = kwargs.pop('number', None) - autoshare = kwargs.pop('autoshare', None) + ss = kwargs.pop("_subplot_spec", None) # see below + number = kwargs.pop("number", None) + autoshare = kwargs.pop("autoshare", None) autoshare = _not_none(autoshare, True) # Remove format-related args and initialize rc_kw, rc_mode = _pop_rc(kwargs) - kw_format = _pop_props(kwargs, 'patch') # background properties - if 'zorder' in kw_format: # special case: refers to the entire axes - kwargs['zorder'] = kw_format.pop('zorder') + kw_format = _pop_props(kwargs, "patch") # background properties + if "zorder" in kw_format: # special case: refers to the entire axes + kwargs["zorder"] = kw_format.pop("zorder") for cls, sig in self._format_signatures.items(): if isinstance(self, cls): kw_format.update(_pop_params(kwargs, sig)) super().__init__(*args, **kwargs) # Varous scalar properties - self._active_cycle = rc['axes.prop_cycle'] + self._active_cycle = rc["axes.prop_cycle"] self._auto_format = None # manipulated by wrapper functions self._abc_border_kwargs = {} self._abc_loc = None - self._abc_title_pad = rc['abc.titlepad'] - self._title_above = rc['title.above'] + self._abc_title_pad = rc["abc.titlepad"] + self._title_above = rc["title.above"] self._title_border_kwargs = {} # title border properties self._title_loc = None - self._title_pad = rc['title.pad'] + self._title_pad = rc["title.pad"] self._title_pad_current = None self._altx_parent = None # for cartesian axes only self._alty_parent = None @@ -794,22 +794,22 @@ def __init__(self, *args, **kwargs): self._legend_dict = {} self._colorbar_dict = {} d = self._panel_dict = {} - d['left'] = [] # NOTE: panels will be sorted inside-to-outside - d['right'] = [] - d['bottom'] = [] - d['top'] = [] + d["left"] = [] # NOTE: panels will be sorted inside-to-outside + d["right"] = [] + d["bottom"] = [] + d["top"] = [] d = self._title_dict = {} - kw = {'zorder': 3.5, 'transform': self.transAxes} - d['abc'] = self.text(0, 0, '', **kw) - d['left'] = self._left_title # WARNING: track in case mpl changes this - d['center'] = self.title - d['right'] = self._right_title - d['upper left'] = self.text(0, 0, '', va='top', ha='left', **kw) - d['upper center'] = self.text(0, 0.5, '', va='top', ha='center', **kw) - d['upper right'] = self.text(0, 1, '', va='top', ha='right', **kw) - d['lower left'] = self.text(0, 0, '', va='bottom', ha='left', **kw) - d['lower center'] = self.text(0, 0.5, '', va='bottom', ha='center', **kw) - d['lower right'] = self.text(0, 1, '', va='bottom', ha='right', **kw) + kw = {"zorder": 3.5, "transform": self.transAxes} + d["abc"] = self.text(0, 0, "", **kw) + d["left"] = self._left_title # WARNING: track in case mpl changes this + d["center"] = self.title + d["right"] = self._right_title + d["upper left"] = self.text(0, 0, "", va="top", ha="left", **kw) + d["upper center"] = self.text(0, 0.5, "", va="top", ha="center", **kw) + d["upper right"] = self.text(0, 1, "", va="top", ha="right", **kw) + d["lower left"] = self.text(0, 0, "", va="bottom", ha="left", **kw) + d["lower center"] = self.text(0, 0.5, "", va="bottom", ha="center", **kw) + d["lower right"] = self.text(0, 1, "", va="bottom", ha="right", **kw) # Subplot-specific settings # NOTE: Default number for any axes is None (i.e., no a-b-c labels allowed) @@ -833,24 +833,32 @@ def __init__(self, *args, **kwargs): self.format(rc_kw=rc_kw, rc_mode=1, skip_figure=True, **kw_format) def _add_inset_axes( - self, bounds, transform=None, *, proj=None, projection=None, - zoom=None, zoom_kw=None, zorder=None, **kwargs + self, + bounds, + transform=None, + *, + proj=None, + projection=None, + zoom=None, + zoom_kw=None, + zorder=None, + **kwargs, ): """ Add an inset axes using arbitrary projection. """ # Converting transform to figure-relative coordinates - transform = self._get_transform(transform, 'axes') + transform = self._get_transform(transform, "axes") locator = self._make_inset_locator(bounds, transform) bounds = locator(self, None).bounds - label = kwargs.pop('label', 'inset_axes') + label = kwargs.pop("label", "inset_axes") zorder = _not_none(zorder, 4) # Parse projection and inherit from the current axes by default # NOTE: The _parse_proj method also accepts axes classes. proj = _not_none(proj=proj, projection=projection) if proj is None: - if self._name in ('cartopy', 'basemap'): + if self._name in ("cartopy", "basemap"): proj = copy.copy(self.projection) else: proj = self._name @@ -858,7 +866,7 @@ def _add_inset_axes( # Create axes and apply locator. The locator lets the axes adjust # automatically if we used data coords. Called by ax.apply_aspect() - cls = mproj.get_projection_class(kwargs.pop('projection')) + cls = mproj.get_projection_class(kwargs.pop("projection")) ax = cls(self.figure, bounds, zorder=zorder, label=label, **kwargs) ax.set_axes_locator(locator) ax._inset_parent = self @@ -866,7 +874,7 @@ def _add_inset_axes( self.add_child_axes(ax) # Add zoom indicator (NOTE: requires matplotlib >= 3.0) - zoom_default = self._name == 'cartesian' and ax._name == 'cartesian' + zoom_default = self._name == "cartesian" and ax._name == "cartesian" zoom = ax._inset_zoom = _not_none(zoom, zoom_default) if zoom: zoom_kw = zoom_kw or {} @@ -890,7 +898,9 @@ def _add_queued_guides(self): # WARNING: Passing empty list labels=[] to legend causes matplotlib # _parse_legend_args to search for everything. Ensure None if empty. for (loc, align), legend in tuple(self._legend_dict.items()): - if not isinstance(legend, tuple) or any(isinstance(_, mlegend.Legend) for _ in legend): # noqa: E501 + if not isinstance(legend, tuple) or any( + isinstance(_, mlegend.Legend) for _ in legend + ): # noqa: E501 continue handles, labels, kwargs = legend leg = self._add_legend(handles, labels, loc=loc, align=align, **kwargs) @@ -905,28 +915,30 @@ def _add_guide_frame( # TODO: Shadow patch does not seem to work. Unsure why. # TODO: Add basic 'colorbar' and 'legend' artists with # shared control over background frame. - shadow = kwargs.pop('shadow', None) # noqa: F841 + shadow = kwargs.pop("shadow", None) # noqa: F841 renderer = self.figure._get_renderer() fontsize = _fontsize_to_pt(fontsize) fontsize = (fontsize / 72) / self._get_size_inches()[0] # axes relative units fontsize = renderer.points_to_pixels(fontsize) patch = mpatches.FancyBboxPatch( - (xmin, ymin), width, height, + (xmin, ymin), + width, + height, snap=True, zorder=4.5, mutation_scale=fontsize, - transform=self.transAxes + transform=self.transAxes, ) patch.set_clip_on(False) if fancybox: - patch.set_boxstyle('round', pad=0, rounding_size=0.2) + patch.set_boxstyle("round", pad=0, rounding_size=0.2) else: - patch.set_boxstyle('square', pad=0) + patch.set_boxstyle("square", pad=0) patch.update(kwargs) self.add_artist(patch) return patch - def _add_guide_panel(self, loc='fill', align='center', length=0, **kwargs): + def _add_guide_panel(self, loc="fill", align="center", length=0, **kwargs): """ Add a panel to be filled by an "outer" colorbar or legend. """ @@ -936,9 +948,9 @@ def _add_guide_panel(self, loc='fill', align='center', length=0, **kwargs): # tight layout will include legend and colorbar and 2) do not use # ax.clear() so that top panel title and a-b-c label can remain. bbox = _align_bbox(align, length) - if loc == 'fill': + if loc == "fill": ax = self - elif loc in ('left', 'right', 'top', 'bottom'): + elif loc in ("left", "right", "top", "bottom"): ax = None for pax in self._panel_dict[loc]: if not pax._panel_hidden or align in pax._panel_align: @@ -949,33 +961,71 @@ def _add_guide_panel(self, loc='fill', align='center', length=0, **kwargs): if ax is None: ax = self.panel_axes(loc, filled=True, **kwargs) else: - raise ValueError(f'Invalid filled panel location {loc!r}.') + raise ValueError(f"Invalid filled panel location {loc!r}.") for s in ax.spines.values(): s.set_visible(False) ax.xaxis.set_visible(False) ax.yaxis.set_visible(False) - ax.patch.set_facecolor('none') + ax.patch.set_facecolor("none") ax._panel_hidden = True ax._panel_align[align] = bbox return ax - @warnings._rename_kwargs('0.10', rasterize='rasterized') + @warnings._rename_kwargs("0.10", rasterize="rasterized") def _add_colorbar( - self, mappable, values=None, *, - loc=None, align=None, space=None, pad=None, - width=None, length=None, shrink=None, - label=None, title=None, reverse=False, - rotation=None, grid=None, edges=None, drawedges=None, - extend=None, extendsize=None, extendfrac=None, - ticks=None, locator=None, locator_kw=None, - format=None, formatter=None, ticklabels=None, formatter_kw=None, - minorticks=None, minorlocator=None, minorlocator_kw=None, - tickminor=None, ticklen=None, ticklenratio=None, - tickdir=None, tickdirection=None, tickwidth=None, tickwidthratio=None, - ticklabelsize=None, ticklabelweight=None, ticklabelcolor=None, - labelloc=None, labellocation=None, labelsize=None, labelweight=None, - labelcolor=None, c=None, color=None, lw=None, linewidth=None, - edgefix=None, rasterized=None, **kwargs + self, + mappable, + values=None, + *, + loc=None, + align=None, + space=None, + pad=None, + width=None, + length=None, + shrink=None, + label=None, + title=None, + reverse=False, + rotation=None, + grid=None, + edges=None, + drawedges=None, + extend=None, + extendsize=None, + extendfrac=None, + ticks=None, + locator=None, + locator_kw=None, + format=None, + formatter=None, + ticklabels=None, + formatter_kw=None, + minorticks=None, + minorlocator=None, + minorlocator_kw=None, + tickminor=None, + ticklen=None, + ticklenratio=None, + tickdir=None, + tickdirection=None, + tickwidth=None, + tickwidthratio=None, + ticklabelsize=None, + ticklabelweight=None, + ticklabelcolor=None, + labelloc=None, + labellocation=None, + labelsize=None, + labelweight=None, + labelcolor=None, + c=None, + color=None, + lw=None, + linewidth=None, + edgefix=None, + rasterized=None, + **kwargs, ): """ The driver function for adding axes colorbars. @@ -983,22 +1033,24 @@ def _add_colorbar( # Parse input arguments and apply defaults # TODO: Get the 'best' inset colorbar location using the legend algorithm # and implement inset colorbars the same as inset legends. - grid = _not_none(grid=grid, edges=edges, drawedges=drawedges, default=rc['colorbar.grid']) # noqa: E501 + grid = _not_none( + grid=grid, edges=edges, drawedges=drawedges, default=rc["colorbar.grid"] + ) # noqa: E501 length = _not_none(length=length, shrink=shrink) label = _not_none(title=title, label=label) labelloc = _not_none(labelloc=labelloc, labellocation=labellocation) locator = _not_none(ticks=ticks, locator=locator) formatter = _not_none(ticklabels=ticklabels, formatter=formatter, format=format) minorlocator = _not_none(minorticks=minorticks, minorlocator=minorlocator) - color = _not_none(c=c, color=color, default=rc['axes.edgecolor']) + color = _not_none(c=c, color=color, default=rc["axes.edgecolor"]) linewidth = _not_none(lw=lw, linewidth=linewidth) - ticklen = units(_not_none(ticklen, rc['tick.len']), 'pt') + ticklen = units(_not_none(ticklen, rc["tick.len"]), "pt") tickdir = _not_none(tickdir=tickdir, tickdirection=tickdirection) - tickwidth = units(_not_none(tickwidth, linewidth, rc['tick.width']), 'pt') - linewidth = units(_not_none(linewidth, default=rc['axes.linewidth']), 'pt') - ticklenratio = _not_none(ticklenratio, rc['tick.lenratio']) - tickwidthratio = _not_none(tickwidthratio, rc['tick.widthratio']) - rasterized = _not_none(rasterized, rc['colorbar.rasterized']) + tickwidth = units(_not_none(tickwidth, linewidth, rc["tick.width"]), "pt") + linewidth = units(_not_none(linewidth, default=rc["axes.linewidth"]), "pt") + ticklenratio = _not_none(ticklenratio, rc["tick.lenratio"]) + tickwidthratio = _not_none(tickwidthratio, rc["tick.widthratio"]) + rasterized = _not_none(rasterized, rc["colorbar.rasterized"]) # Build label and locator keyword argument dicts # NOTE: This carefully handles the 'maxn' and 'maxn_minor' deprecations @@ -1007,29 +1059,29 @@ def _add_colorbar( formatter_kw = formatter_kw or {} minorlocator_kw = minorlocator_kw or {} for key, value in ( - ('size', labelsize), - ('weight', labelweight), - ('color', labelcolor), + ("size", labelsize), + ("weight", labelweight), + ("color", labelcolor), ): if value is not None: kw_label[key] = value kw_ticklabels = {} for key, value in ( - ('size', ticklabelsize), - ('weight', ticklabelweight), - ('color', ticklabelcolor), - ('rotation', rotation), + ("size", ticklabelsize), + ("weight", ticklabelweight), + ("color", ticklabelcolor), + ("rotation", rotation), ): if value is not None: kw_ticklabels[key] = value for b, kw in enumerate((locator_kw, minorlocator_kw)): - key = 'maxn_minor' if b else 'maxn' - name = 'minorlocator' if b else 'locator' - nbins = kwargs.pop('maxn_minor' if b else 'maxn', None) + key = "maxn_minor" if b else "maxn" + name = "minorlocator" if b else "locator" + nbins = kwargs.pop("maxn_minor" if b else "maxn", None) if nbins is not None: - kw['nbins'] = nbins + kw["nbins"] = nbins warnings._warn_proplot( - f'The colorbar() keyword {key!r} was deprecated in v0.10. To ' + f"The colorbar() keyword {key!r} was deprecated in v0.10. To " "achieve the same effect, you can pass 'nbins' to the new default " f"locator DiscreteLocator using {name}_kw={{'nbins': {nbins}}}." ) @@ -1037,21 +1089,29 @@ def _add_colorbar( # Generate and prepare the colorbar axes # NOTE: The inset axes function needs 'label' to know how to pad the box # TODO: Use seperate keywords for frame properties vs. colorbar edge properties? - if loc in ('fill', 'left', 'right', 'top', 'bottom'): - length = _not_none(length, rc['colorbar.length']) # for _add_guide_panel - kwargs.update({'align': align, 'length': length}) - extendsize = _not_none(extendsize, rc['colorbar.extend']) - ax = self._add_guide_panel(loc, align, length=length, width=width, space=space, pad=pad) # noqa: E501 + if loc in ("fill", "left", "right", "top", "bottom"): + length = _not_none(length, rc["colorbar.length"]) # for _add_guide_panel + kwargs.update({"align": align, "length": length}) + extendsize = _not_none(extendsize, rc["colorbar.extend"]) + ax = self._add_guide_panel( + loc, align, length=length, width=width, space=space, pad=pad + ) # noqa: E501 cax, kwargs = ax._parse_colorbar_filled(**kwargs) else: - kwargs.update({'label': label, 'length': length, 'width': width}) - extendsize = _not_none(extendsize, rc['colorbar.insetextend']) - cax, kwargs = self._parse_colorbar_inset(loc=loc, pad=pad, **kwargs) # noqa: E501 + kwargs.update({"label": label, "length": length, "width": width}) + extendsize = _not_none(extendsize, rc["colorbar.insetextend"]) + cax, kwargs = self._parse_colorbar_inset( + loc=loc, pad=pad, **kwargs + ) # noqa: E501 # Parse the colorbar mappable # NOTE: Account for special case where auto colorbar is generated from 1D # methods that construct an 'artist list' (i.e. colormap scatter object) - if np.iterable(mappable) and len(mappable) == 1 and isinstance(mappable[0], mcm.ScalarMappable): # noqa: E501 + if ( + np.iterable(mappable) + and len(mappable) == 1 + and isinstance(mappable[0], mcm.ScalarMappable) + ): # noqa: E501 mappable = mappable[0] if not isinstance(mappable, mcm.ScalarMappable): mappable, kwargs = cax._parse_colorbar_arg(mappable, values, **kwargs) @@ -1059,30 +1119,30 @@ def _add_colorbar( pop = _pop_params(kwargs, cax._parse_colorbar_arg, ignore_internal=True) if pop: warnings._warn_proplot( - f'Input is already a ScalarMappable. ' - f'Ignoring unused keyword arg(s): {pop}' + f"Input is already a ScalarMappable. " + f"Ignoring unused keyword arg(s): {pop}" ) # Parse 'extendsize' and 'extendfrac' keywords # TODO: Make this auto-adjust to the subplot size - vert = kwargs['orientation'] == 'vertical' + vert = kwargs["orientation"] == "vertical" if extendsize is not None and extendfrac is not None: warnings._warn_proplot( - f'You cannot specify both an absolute extendsize={extendsize!r} ' + f"You cannot specify both an absolute extendsize={extendsize!r} " f"and a relative extendfrac={extendfrac!r}. Ignoring 'extendfrac'." ) extendfrac = None if extendfrac is None: width, height = cax._get_size_inches() scale = height if vert else width - extendsize = units(extendsize, 'em', 'in') - extendfrac = extendsize / max(scale - 2 * extendsize, units(1, 'em', 'in')) + extendsize = units(extendsize, "em", "in") + extendfrac = extendsize / max(scale - 2 * extendsize, units(1, "em", "in")) # Parse the tick locators and formatters # NOTE: In presence of BoundaryNorm or similar handle ticks with special # DiscreteLocator or else get issues (see mpl #22233). norm = mappable.norm - formatter = _not_none(formatter, getattr(norm, '_labels', None), 'auto') + formatter = _not_none(formatter, getattr(norm, "_labels", None), "auto") formatter = constructor.Formatter(formatter, **formatter_kw) categorical = isinstance(formatter, mticker.FixedFormatter) if locator is not None: @@ -1090,10 +1150,10 @@ def _add_colorbar( if minorlocator is not None: # overrides tickminor minorlocator = constructor.Locator(minorlocator, **minorlocator_kw) elif tickminor is None: - tickminor = False if categorical else rc['xy'[vert] + 'tick.minor.visible'] + tickminor = False if categorical else rc["xy"[vert] + "tick.minor.visible"] if isinstance(norm, mcolors.BoundaryNorm): # DiscreteNorm or BoundaryNorm - ticks = getattr(norm, '_ticks', norm.boundaries) - segmented = isinstance(getattr(norm, '_norm', None), pcolors.SegmentedNorm) + ticks = getattr(norm, "_ticks", norm.boundaries) + segmented = isinstance(getattr(norm, "_norm", None), pcolors.SegmentedNorm) if locator is None: if categorical or segmented: locator = mticker.FixedLocator(ticks) @@ -1120,19 +1180,20 @@ def _add_colorbar( # verisons recognize that DiscreteLocator will behave like FixedLocator. axis = cax.yaxis if vert else cax.xaxis if not isinstance(mappable, mcontour.ContourSet): - extend = _not_none(extend, 'neither') - kwargs['extend'] = extend + extend = _not_none(extend, "neither") + kwargs["extend"] = extend elif extend is not None and extend != mappable.extend: warnings._warn_proplot( - 'Ignoring extend={extend!r}. ContourSet extend cannot be changed.' + "Ignoring extend={extend!r}. ContourSet extend cannot be changed." ) if ( isinstance(locator, mticker.NullLocator) - or hasattr(locator, 'locs') and len(locator.locs) == 0 + or hasattr(locator, "locs") + and len(locator.locs) == 0 ): minorlocator, tickminor = None, False # attempted fix for ticker in (locator, formatter, minorlocator): - if _version_mpl < '3.2': + if _version_mpl < "3.2": pass # see notes above elif isinstance(ticker, mticker.TickHelper): ticker.set_axis(axis) @@ -1141,10 +1202,15 @@ def _add_colorbar( # NOTE: This also adds the guides._update_ticks() monkey patch that triggers # updates to DiscreteLocator when parent axes is drawn. obj = cax._colorbar_fill = cax.figure.colorbar( - mappable, cax=cax, ticks=locator, format=formatter, - drawedges=grid, extendfrac=extendfrac, **kwargs + mappable, + cax=cax, + ticks=locator, + format=formatter, + drawedges=grid, + extendfrac=extendfrac, + **kwargs, ) - obj.minorlocator = minorlocator # backwards compatibility + # obj.minorlocator = minorlocator # backwards compatibility obj.update_ticks = guides._update_ticks.__get__(obj) # backwards compatible if minorlocator is not None: obj.update_ticks() @@ -1152,7 +1218,7 @@ def _add_colorbar( obj.minorticks_on() else: obj.minorticks_off() - if getattr(norm, 'descending', None): + if getattr(norm, "descending", None): axis.set_inverted(True) if reverse: # potentially double reverse, although that would be weird... axis.set_inverted(True) @@ -1160,9 +1226,13 @@ def _add_colorbar( # Update other colorbar settings # WARNING: Must use the colorbar set_label to set text. Calling set_label # on the actual axis will do nothing! - axis.set_tick_params(which='both', color=color, direction=tickdir) - axis.set_tick_params(which='major', length=ticklen, width=tickwidth) - axis.set_tick_params(which='minor', length=ticklen * ticklenratio, width=tickwidth * tickwidthratio) # noqa: E501 + axis.set_tick_params(which="both", color=color, direction=tickdir) + axis.set_tick_params(which="major", length=ticklen, width=tickwidth) + axis.set_tick_params( + which="minor", + length=ticklen * ticklenratio, + width=tickwidth * tickwidthratio, + ) # noqa: E501 if label is not None: obj.set_label(label) if labelloc is not None: @@ -1170,110 +1240,136 @@ def _add_colorbar( axis.label.update(kw_label) for label in axis.get_ticklabels(): label.update(kw_ticklabels) - kw_outline = {'edgecolor': color, 'linewidth': linewidth} + kw_outline = {"edgecolor": color, "linewidth": linewidth} if obj.outline is not None: obj.outline.update(kw_outline) if obj.dividers is not None: obj.dividers.update(kw_outline) if obj.solids: from . import PlotAxes + obj.solids.set_rasterized(rasterized) PlotAxes._fix_patch_edges(obj.solids, edgefix=edgefix) # Register location and return - self._register_guide('colorbar', obj, (loc, align)) # possibly replace another + self._register_guide("colorbar", obj, (loc, align)) # possibly replace another return obj def _add_legend( - self, handles=None, labels=None, *, - loc=None, align=None, width=None, pad=None, space=None, - frame=None, frameon=None, ncol=None, ncols=None, - alphabetize=False, center=None, order=None, label=None, title=None, - fontsize=None, fontweight=None, fontcolor=None, - titlefontsize=None, titlefontweight=None, titlefontcolor=None, - handle_kw=None, handler_map=None, **kwargs + self, + handles=None, + labels=None, + *, + loc=None, + align=None, + width=None, + pad=None, + space=None, + frame=None, + frameon=None, + ncol=None, + ncols=None, + alphabetize=False, + center=None, + order=None, + label=None, + title=None, + fontsize=None, + fontweight=None, + fontcolor=None, + titlefontsize=None, + titlefontweight=None, + titlefontcolor=None, + handle_kw=None, + handler_map=None, + **kwargs, ): """ The driver function for adding axes legends. """ # Parse input argument units ncol = _not_none(ncols=ncols, ncol=ncol) - order = _not_none(order, 'C') - frameon = _not_none(frame=frame, frameon=frameon, default=rc['legend.frameon']) - fontsize = _not_none(fontsize, rc['legend.fontsize']) + order = _not_none(order, "C") + frameon = _not_none(frame=frame, frameon=frameon, default=rc["legend.frameon"]) + fontsize = _not_none(fontsize, rc["legend.fontsize"]) titlefontsize = _not_none( - title_fontsize=kwargs.pop('title_fontsize', None), + title_fontsize=kwargs.pop("title_fontsize", None), titlefontsize=titlefontsize, - default=rc['legend.title_fontsize'] + default=rc["legend.title_fontsize"], ) fontsize = _fontsize_to_pt(fontsize) titlefontsize = _fontsize_to_pt(titlefontsize) - if order not in ('F', 'C'): + if order not in ("F", "C"): raise ValueError( - f'Invalid order {order!r}. Please choose from ' + f"Invalid order {order!r}. Please choose from " "'C' (row-major, default) or 'F' (column-major)." ) # Convert relevant keys to em-widths for setting in rcsetup.EM_KEYS: # em-width keys - pair = setting.split('legend.', 1) + pair = setting.split("legend.", 1) if len(pair) == 1: continue _, key = pair value = kwargs.pop(key, None) if isinstance(value, str): - value = units(kwargs[key], 'em', fontsize=fontsize) + value = units(kwargs[key], "em", fontsize=fontsize) if value is not None: kwargs[key] = value # Generate and prepare the legend axes - if loc in ('fill', 'left', 'right', 'top', 'bottom'): + if loc in ("fill", "left", "right", "top", "bottom"): lax = self._add_guide_panel(loc, align, width=width, space=space, pad=pad) - kwargs.setdefault('borderaxespad', 0) + kwargs.setdefault("borderaxespad", 0) if not frameon: - kwargs.setdefault('borderpad', 0) + kwargs.setdefault("borderpad", 0) try: - kwargs['loc'] = ALIGN_OPTS[lax._panel_side][align] + kwargs["loc"] = ALIGN_OPTS[lax._panel_side][align] except KeyError: - raise ValueError(f'Invalid align={align!r} for legend loc={loc!r}.') + raise ValueError(f"Invalid align={align!r} for legend loc={loc!r}.") else: lax = self - pad = kwargs.pop('borderaxespad', pad) - kwargs['loc'] = loc # simply pass to legend - kwargs['borderaxespad'] = units(pad, 'em', fontsize=fontsize) + pad = kwargs.pop("borderaxespad", pad) + kwargs["loc"] = loc # simply pass to legend + kwargs["borderaxespad"] = units(pad, "em", fontsize=fontsize) # Handle and text properties that are applied after-the-fact # NOTE: Set solid_capstyle to 'butt' so line does not extend past error bounds # shading in legend entry. This change is not noticable in other situations. - kw_frame, kwargs = lax._parse_frame('legend', **kwargs) + kw_frame, kwargs = lax._parse_frame("legend", **kwargs) kw_text = {} if fontcolor is not None: - kw_text['color'] = fontcolor + kw_text["color"] = fontcolor if fontweight is not None: - kw_text['weight'] = fontweight + kw_text["weight"] = fontweight kw_title = {} if titlefontcolor is not None: - kw_title['color'] = titlefontcolor + kw_title["color"] = titlefontcolor if titlefontweight is not None: - kw_title['weight'] = titlefontweight - kw_handle = _pop_props(kwargs, 'line') - kw_handle.setdefault('solid_capstyle', 'butt') + kw_title["weight"] = titlefontweight + kw_handle = _pop_props(kwargs, "line") + kw_handle.setdefault("solid_capstyle", "butt") kw_handle.update(handle_kw or {}) # Parse the legend arguments using axes for auto-handle detection # TODO: Update this when we no longer use "filled panels" for outer legends pairs, multi = lax._parse_legend_handles( - handles, labels, ncol=ncol, order=order, center=center, - alphabetize=alphabetize, handler_map=handler_map + handles, + labels, + ncol=ncol, + order=order, + center=center, + alphabetize=alphabetize, + handler_map=handler_map, ) title = _not_none(label=label, title=title) kwargs.update( { - 'title': title, - 'frameon': frameon, - 'fontsize': fontsize, - 'handler_map': handler_map, - 'title_fontsize': titlefontsize, + "title": title, + "frameon": frameon, + "fontsize": fontsize, + "handler_map": handler_map, + "title_fontsize": titlefontsize, } ) @@ -1283,11 +1379,11 @@ def _add_legend( if multi: objs = lax._parse_legend_centered(pairs, kw_frame=kw_frame, **kwargs) else: - kwargs.update({key: kw_frame.pop(key) for key in ('shadow', 'fancybox')}) + kwargs.update({key: kw_frame.pop(key) for key in ("shadow", "fancybox")}) objs = [lax._parse_legend_aligned(pairs, ncol=ncol, order=order, **kwargs)] objs[0].legendPatch.update(kw_frame) for obj in objs: - if hasattr(lax, 'legend_') and lax.legend_ is None: + if hasattr(lax, "legend_") and lax.legend_ is None: lax.legend_ = obj # make first legend accessible with get_legend() else: lax.add_artist(obj) @@ -1298,21 +1394,25 @@ def _add_legend( # returns the first artist. Instead we try to iterate through offset boxes. for obj in objs: obj.set_clip_on(False) # needed for tight bounding box calculations - box = getattr(obj, '_legend_handle_box', None) + box = getattr(obj, "_legend_handle_box", None) for obj in guides._iter_children(box): if isinstance(obj, mtext.Text): kw = kw_text else: - kw = {key: val for key, val in kw_handle.items() if hasattr(obj, 'set_' + key)} # noqa: E501 - if hasattr(obj, 'set_sizes') and 'markersize' in kw_handle: - kw['sizes'] = np.atleast_1d(kw_handle['markersize']) + kw = { + key: val + for key, val in kw_handle.items() + if hasattr(obj, "set_" + key) + } # noqa: E501 + if hasattr(obj, "set_sizes") and "markersize" in kw_handle: + kw["sizes"] = np.atleast_1d(kw_handle["markersize"]) obj.update(kw) # Register location and return if isinstance(objs[0], mpatches.FancyBboxPatch): objs = objs[1:] obj = objs[0] if len(objs) == 1 else tuple(objs) - self._register_guide('legend', obj, (loc, align)) # possibly replace another + self._register_guide("legend", obj, (loc, align)) # possibly replace another return obj @@ -1323,16 +1423,16 @@ def _apply_title_above(self): """ # NOTE: Similar to how _apply_axis_sharing() is called in _align_axis_labels() # this is called in _align_super_labels() so we get the correct offset. - paxs = self._panel_dict['top'] + paxs = self._panel_dict["top"] if not paxs: return pax = paxs[-1] - names = ('left', 'center', 'right') + names = ("left", "center", "right") if self._abc_loc in names: - names += ('abc',) + names += ("abc",) if not self._title_above: return - if pax._panel_hidden and self._title_above == 'panels': + if pax._panel_hidden and self._title_above == "panels": return pax._title_pad = self._title_pad pax._abc_title_pad = self._abc_title_pad @@ -1344,6 +1444,7 @@ def _apply_auto_share(self): Automatically configure axis sharing based on the horizontal and vertical extent of subplots in the figure gridspec. """ + # Panel axes sharing, between main subplot and its panels # NOTE: _panel_share means "include this panel in the axis sharing group" while # _panel_sharex_group indicates the group itself and may include main axes @@ -1357,39 +1458,39 @@ def shared(paxs): if not self._panel_side: # this is a main axes # Top and bottom bottom = self - paxs = shared(self._panel_dict['bottom']) + paxs = shared(self._panel_dict["bottom"]) if paxs: bottom = paxs[-1] bottom._panel_sharex_group = False for iax in (self, *paxs[:-1]): iax._panel_sharex_group = True iax._sharex_setup(bottom) # parent is bottom-most - paxs = shared(self._panel_dict['top']) + paxs = shared(self._panel_dict["top"]) for iax in paxs: iax._panel_sharex_group = True iax._sharex_setup(bottom) # Left and right # NOTE: Order of panel lists is always inside-to-outside left = self - paxs = shared(self._panel_dict['left']) + paxs = shared(self._panel_dict["left"]) if paxs: left = paxs[-1] left._panel_sharey_group = False for iax in (self, *paxs[:-1]): iax._panel_sharey_group = True iax._sharey_setup(left) # parent is left-most - paxs = shared(self._panel_dict['right']) + paxs = shared(self._panel_dict["right"]) for iax in paxs: iax._panel_sharey_group = True iax._sharey_setup(left) # External axes sharing, sometimes overrides panel axes sharing # Share x axes - parent, *children = self._get_share_axes('x') + parent, *children = self._get_share_axes("x") for child in children: child._sharex_setup(parent) # Share y axes - parent, *children = self._get_share_axes('y') + parent, *children = self._get_share_axes("y") for child in children: child._sharey_setup(parent) # Global sharing, use the reference subplot because why not @@ -1408,15 +1509,15 @@ def _artist_fully_clipped(self, artist): clip_box = artist.get_clip_box() clip_path = artist.get_clip_path() types_noclip = ( - maxes.Axes, maxis.Axis, moffsetbox.AnnotationBbox, moffsetbox.OffsetBox + maxes.Axes, + maxis.Axis, + moffsetbox.AnnotationBbox, + moffsetbox.OffsetBox, ) return not isinstance(artist, types_noclip) and ( artist.get_clip_on() and (clip_box is not None or clip_path is not None) - and ( - clip_box is None - or np.all(clip_box.extents == self.bbox.extents) - ) + and (clip_box is None or np.all(clip_box.extents == self.bbox.extents)) and ( clip_path is None or isinstance(clip_path, mtransforms.TransformedPatchPath) @@ -1439,11 +1540,13 @@ def _get_legend_handles(self, handler_map=None): handler_map_full = handler_map_full.copy() handler_map_full.update(handler_map or {}) for ax in axs: - for attr in ('lines', 'patches', 'collections', 'containers'): + for attr in ("lines", "patches", "collections", "containers"): for handle in getattr(ax, attr, []): # guard against API changes label = handle.get_label() - handler = mlegend.Legend.get_legend_handler(handler_map_full, handle) # noqa: E501 - if handler and label and label[0] != '_': + handler = mlegend.Legend.get_legend_handler( + handler_map_full, handle + ) # noqa: E501 + if handler and label and label[0] != "_": handles.append(handle) return handles @@ -1455,9 +1558,9 @@ def _get_share_axes(self, sx, panels=False): # NOTE: The lefmost or bottommost axes are at the start of the list. if not isinstance(self, maxes.SubplotBase): return [self] - i = 0 if sx == 'x' else 1 - sy = 'y' if sx == 'x' else 'x' - argfunc = np.argmax if sx == 'x' else np.argmin + i = 0 if sx == "x" else 1 + sy = "y" if sx == "x" else "x" + argfunc = np.argmax if sx == "x" else np.argmin irange = self._range_subplotspec(sx) axs = self.figure._iter_axes(hidden=False, children=False, panels=panels) axs = [ax for ax in axs if ax._range_subplotspec(sx) == irange] @@ -1470,18 +1573,18 @@ def _get_span_axes(self, side, panels=False): Return the axes whose left, right, top, or bottom sides abutt against the same row or column as this axes. Deflect to shared panels. """ - if side not in ('left', 'right', 'bottom', 'top'): - raise ValueError(f'Invalid side {side!r}.') + if side not in ("left", "right", "bottom", "top"): + raise ValueError(f"Invalid side {side!r}.") if not isinstance(self, maxes.SubplotBase): return [self] - x, y = 'xy' if side in ('left', 'right') else 'yx' - idx = 0 if side in ('left', 'top') else 1 # which side to test + x, y = "xy" if side in ("left", "right") else "yx" + idx = 0 if side in ("left", "top") else 1 # which side to test coord = self._range_subplotspec(x)[idx] # side for a particular axes axs = self.figure._iter_axes(hidden=False, children=False, panels=panels) axs = [ax for ax in axs if ax._range_subplotspec(x)[idx] == coord] or [self] out = [] for ax in axs: - other = getattr(ax, '_share' + y) + other = getattr(ax, "_share" + y) if other and other._panel_parent: # this is a shared panel ax = other out.append(ax) @@ -1506,7 +1609,7 @@ def _get_topmost_axes(self): self = self._panel_parent or self return self - def _get_transform(self, transform, default='data'): + def _get_transform(self, transform, default="data"): """ Translates user input transform. Also used in an axes method. """ @@ -1517,43 +1620,45 @@ def _get_transform(self, transform, default='data'): return transform elif CRS is not object and isinstance(transform, CRS): return transform - elif PlateCarree is not object and transform == 'map': + elif PlateCarree is not object and transform == "map": return PlateCarree() - elif transform == 'data': + elif transform == "data": return self.transData - elif transform == 'axes': + elif transform == "axes": return self.transAxes - elif transform == 'figure': + elif transform == "figure": return self.figure.transFigure - elif transform == 'subfigure': + elif transform == "subfigure": return self.figure.transSubfigure else: - raise ValueError(f'Unknown transform {transform!r}.') + raise ValueError(f"Unknown transform {transform!r}.") def _register_guide(self, guide, obj, key, **kwargs): """ Queue up or replace objects for legends and list-of-artist style colorbars. """ # Initial stuff - if guide not in ('legend', 'colorbar'): - raise TypeError(f'Invalid type {guide!r}.') - dict_ = self._legend_dict if guide == 'legend' else self._colorbar_dict + if guide not in ("legend", "colorbar"): + raise TypeError(f"Invalid type {guide!r}.") + dict_ = self._legend_dict if guide == "legend" else self._colorbar_dict # Remove previous instances # NOTE: No good way to remove inset colorbars right now until the bounding # box and axes are merged into some kind of subclass. Just fine for now. if key in dict_ and not isinstance(dict_[key], tuple): prev = dict_.pop(key) # possibly pop a queued object - if guide == 'colorbar': + if guide == "colorbar": pass - elif hasattr(self, 'legend_') and prev.axes.legend_ is prev: + elif hasattr(self, "legend_") and prev.axes.legend_ is prev: self.legend_ = None # was never added as artist else: prev.remove() # remove legends and inner colorbars # Replace with instance or update the queue # NOTE: This is valid for both mappable-values pairs and handles-labels pairs - if not isinstance(obj, tuple) or any(isinstance(_, mlegend.Legend) for _ in obj): # noqa: E501 + if not isinstance(obj, tuple) or any( + isinstance(_, mlegend.Legend) for _ in obj + ): # noqa: E501 dict_[key] = obj else: handles, labels = obj @@ -1568,8 +1673,14 @@ def _register_guide(self, guide, obj, key, **kwargs): kwargs_full.update(kwargs) def _update_guide( - self, objs, legend=None, legend_kw=None, queue_legend=True, - colorbar=None, colorbar_kw=None, queue_colorbar=True, + self, + objs, + legend=None, + legend_kw=None, + queue_legend=True, + colorbar=None, + colorbar_kw=None, + queue_colorbar=True, ): """ Update queues for on-the-fly legends and colorbars or track keyword arguments. @@ -1581,15 +1692,15 @@ def _update_guide( # and standardize functions both modify colorbar_kw and legend_kw. legend_kw = legend_kw or {} colorbar_kw = colorbar_kw or {} - guides._cache_guide_kw(objs, 'legend', legend_kw) - guides._cache_guide_kw(objs, 'colorbar', colorbar_kw) + guides._cache_guide_kw(objs, "legend", legend_kw) + guides._cache_guide_kw(objs, "colorbar", colorbar_kw) if legend: - align = legend_kw.pop('align', None) - queue = legend_kw.pop('queue', queue_legend) + align = legend_kw.pop("align", None) + queue = legend_kw.pop("queue", queue_legend) self.legend(objs, loc=legend, align=align, queue=queue, **legend_kw) if colorbar: - align = colorbar_kw.pop('align', None) - queue = colorbar_kw.pop('queue', queue_colorbar) + align = colorbar_kw.pop("align", None) + queue = colorbar_kw.pop("queue", queue_colorbar) self.colorbar(objs, loc=colorbar, align=align, queue=queue, **colorbar_kw) @staticmethod @@ -1601,25 +1712,25 @@ def _parse_frame(guide, fancybox=None, shadow=None, **kwargs): # 'linewidth' used for legend handles and colorbar edge. kw_frame = _pop_kwargs( kwargs, - alpha=('a', 'framealpha', 'facealpha'), - facecolor=('fc', 'framecolor', 'facecolor'), - edgecolor=('ec',), - edgewidth=('ew',), + alpha=("a", "framealpha", "facealpha"), + facecolor=("fc", "framecolor", "facecolor"), + edgecolor=("ec",), + edgewidth=("ew",), ) _kw_frame_default = { - 'alpha': f'{guide}.framealpha', - 'facecolor': f'{guide}.facecolor', - 'edgecolor': f'{guide}.edgecolor', - 'edgewidth': 'axes.linewidth', + "alpha": f"{guide}.framealpha", + "facecolor": f"{guide}.facecolor", + "edgecolor": f"{guide}.edgecolor", + "edgewidth": "axes.linewidth", } for key, name in _kw_frame_default.items(): kw_frame.setdefault(key, rc[name]) - for key in ('facecolor', 'edgecolor'): - if kw_frame[key] == 'inherit': - kw_frame[key] = rc['axes.' + key] - kw_frame['linewidth'] = kw_frame.pop('edgewidth') - kw_frame['fancybox'] = _not_none(fancybox, rc[f'{guide}.fancybox']) - kw_frame['shadow'] = _not_none(shadow, rc[f'{guide}.shadow']) + for key in ("facecolor", "edgecolor"): + if kw_frame[key] == "inherit": + kw_frame[key] = rc["axes." + key] + kw_frame["linewidth"] = kw_frame.pop("edgewidth") + kw_frame["fancybox"] = _not_none(fancybox, rc[f"{guide}.fancybox"]) + kw_frame["shadow"] = _not_none(shadow, rc[f"{guide}.shadow"]) return kw_frame, kwargs @staticmethod @@ -1648,28 +1759,35 @@ def _parse_colorbar_arg( # List of colors elif np.iterable(mappable) and all(map(mcolors.is_color_like, mappable)): - cmap = pcolors.DiscreteColormap(list(mappable), '_no_name') + cmap = pcolors.DiscreteColormap(list(mappable), "_no_name") if values is None: values = [None] * len(mappable) # always use discrete norm # List of artists # NOTE: Do not check for isinstance(Artist) in case it is an mpl collection elif np.iterable(mappable) and all( - hasattr(obj, 'get_color') or hasattr(obj, 'get_facecolor') for obj in mappable # noqa: E501 + hasattr(obj, "get_color") or hasattr(obj, "get_facecolor") + for obj in mappable # noqa: E501 ): # Generate colormap from colors and infer tick labels colors = [] for obj in mappable: - if hasattr(obj, 'update_scalarmappable'): # for e.g. pcolor + if hasattr(obj, "update_scalarmappable"): # for e.g. pcolor obj.update_scalarmappable() - color = obj.get_color() if hasattr(obj, 'get_color') else obj.get_facecolor() # noqa: E501 + color = ( + obj.get_color() + if hasattr(obj, "get_color") + else obj.get_facecolor() + ) # noqa: E501 if isinstance(color, np.ndarray): color = color.squeeze() # e.g. single color scatter plot if not mcolors.is_color_like(color): - raise ValueError('Cannot make colorbar from artists with more than one color.') # noqa: E501 + raise ValueError( + "Cannot make colorbar from artists with more than one color." + ) # noqa: E501 colors.append(color) # Try to infer tick values and tick labels from Artist labels - cmap = pcolors.DiscreteColormap(colors, '_no_name') + cmap = pcolors.DiscreteColormap(colors, "_no_name") if values is None: values = [None] * len(mappable) else: @@ -1678,22 +1796,22 @@ def _parse_colorbar_arg( if val is not None: continue val = obj.get_label() - if val and val[0] == '_': + if val and val[0] == "_": continue values[i] = val else: raise ValueError( - 'Input colorbar() argument must be a scalar mappable, colormap name ' - f'or object, list of colors, or list of artists. Got {mappable!r}.' + "Input colorbar() argument must be a scalar mappable, colormap name " + f"or object, list of colors, or list of artists. Got {mappable!r}." ) # Generate continuous normalizer, and possibly discrete normalizer. Update # the outgoing locator and formatter if user does not override. norm_kw = norm_kw or {} - norm = norm or 'linear' - vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop('vmin', None), default=0) - vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop('vmax', None), default=1) + norm = norm or "linear" + vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop("vmin", None), default=0) + vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop("vmax", None), default=1) norm = constructor.Norm(norm, vmin=vmin, vmax=vmax, **norm_kw) if values is not None: ticks = [] @@ -1714,6 +1832,7 @@ def _parse_colorbar_arg( else: levels = edges(ticks) from . import PlotAxes + norm, cmap, _ = PlotAxes._parse_level_norm( levels, norm, cmap, discrete_ticks=ticks, discrete_labels=labels ) @@ -1724,85 +1843,102 @@ def _parse_colorbar_arg( return mappable, kwargs def _parse_colorbar_filled( - self, length=None, align=None, tickloc=None, ticklocation=None, - orientation=None, **kwargs + self, + length=None, + align=None, + tickloc=None, + ticklocation=None, + orientation=None, + **kwargs, ): """ Return the axes and adjusted keyword args for a panel-filling colorbar. """ # Parse input arguments side = self._panel_side - side = _not_none(side, 'left' if orientation == 'vertical' else 'bottom') - align = _not_none(align, 'center') - length = _not_none(length=length, default=rc['colorbar.length']) + side = _not_none(side, "left" if orientation == "vertical" else "bottom") + align = _not_none(align, "center") + length = _not_none(length=length, default=rc["colorbar.length"]) ticklocation = _not_none(tickloc=tickloc, ticklocation=ticklocation) # Calculate inset bounds for the colorbar delta = 0.5 * (1 - length) - if side in ('bottom', 'top'): - if align == 'left': + if side in ("bottom", "top"): + if align == "left": bounds = (0, 0, length, 1) - elif align == 'center': + elif align == "center": bounds = (delta, 0, length, 1) - elif align == 'right': + elif align == "right": bounds = (2 * delta, 0, length, 1) else: - raise ValueError(f'Invalid align={align!r} for colorbar loc={side!r}.') + raise ValueError(f"Invalid align={align!r} for colorbar loc={side!r}.") else: - if align == 'bottom': + if align == "bottom": bounds = (0, 0, 1, length) - elif align == 'center': + elif align == "center": bounds = (0, delta, 1, length) - elif align == 'top': + elif align == "top": bounds = (0, 2 * delta, 1, length) else: - raise ValueError(f'Invalid align={align!r} for colorbar loc={side!r}.') + raise ValueError(f"Invalid align={align!r} for colorbar loc={side!r}.") # Add the axes as a child of the original axes - cls = mproj.get_projection_class('proplot_cartesian') + cls = mproj.get_projection_class("proplot_cartesian") locator = self._make_inset_locator(bounds, self.transAxes) ax = cls(self.figure, locator(self, None).bounds, zorder=5) ax.set_axes_locator(locator) self.add_child_axes(ax) - ax.patch.set_facecolor('none') # ignore axes.alpha application + ax.patch.set_facecolor("none") # ignore axes.alpha application # Handle default keyword args if orientation is None: - orientation = 'horizontal' if side in ('bottom', 'top') else 'vertical' - if orientation == 'horizontal': - outside, inside = 'bottom', 'top' - if side == 'top': + orientation = "horizontal" if side in ("bottom", "top") else "vertical" + if orientation == "horizontal": + outside, inside = "bottom", "top" + if side == "top": outside, inside = inside, outside ticklocation = _not_none(ticklocation, outside) else: - outside, inside = 'left', 'right' - if side == 'right': + outside, inside = "left", "right" + if side == "right": outside, inside = inside, outside ticklocation = _not_none(ticklocation, outside) - kwargs.update({'orientation': orientation, 'ticklocation': ticklocation}) + kwargs.update({"orientation": orientation, "ticklocation": ticklocation}) return ax, kwargs def _parse_colorbar_inset( - self, loc=None, width=None, length=None, shrink=None, - frame=None, frameon=None, label=None, pad=None, - tickloc=None, ticklocation=None, orientation=None, **kwargs, + self, + loc=None, + width=None, + length=None, + shrink=None, + frame=None, + frameon=None, + label=None, + pad=None, + tickloc=None, + ticklocation=None, + orientation=None, + **kwargs, ): """ Return the axes and adjusted keyword args for an inset colorbar. """ # Basic colorbar properties - frame = _not_none(frame=frame, frameon=frameon, default=rc['colorbar.frameon']) - length = _not_none(length=length, shrink=shrink, default=rc['colorbar.insetlength']) # noqa: E501 - width = _not_none(width, rc['colorbar.insetwidth']) - pad = _not_none(pad, rc['colorbar.insetpad']) - length = units(length, 'em', 'ax', axes=self, width=True) # x direction - width = units(width, 'em', 'ax', axes=self, width=False) # y direction - xpad = units(pad, 'em', 'ax', axes=self, width=True) - ypad = units(pad, 'em', 'ax', axes=self, width=False) + frame = _not_none(frame=frame, frameon=frameon, default=rc["colorbar.frameon"]) + length = _not_none( + length=length, shrink=shrink, default=rc["colorbar.insetlength"] + ) # noqa: E501 + width = _not_none(width, rc["colorbar.insetwidth"]) + pad = _not_none(pad, rc["colorbar.insetpad"]) + length = units(length, "em", "ax", axes=self, width=True) # x direction + width = units(width, "em", "ax", axes=self, width=False) # y direction + xpad = units(pad, "em", "ax", axes=self, width=True) + ypad = units(pad, "em", "ax", axes=self, width=False) # Extra space accounting for colorbar label and tick labels - labspace = rc['xtick.major.size'] / 72 - fontsize = rc['xtick.labelsize'] + labspace = rc["xtick.major.size"] / 72 + fontsize = rc["xtick.labelsize"] fontsize = _fontsize_to_pt(fontsize) if label is not None: labspace += 2.4 * fontsize / 72 @@ -1812,13 +1948,13 @@ def _parse_colorbar_inset( # Location in axes-relative coordinates # Bounds are x0, y0, width, height in axes-relative coordinates - if loc == 'upper right': + if loc == "upper right": bounds_inset = [1 - xpad - length, 1 - ypad - width] bounds_frame = [1 - 2 * xpad - length, 1 - 2 * ypad - width - labspace] - elif loc == 'upper left': + elif loc == "upper left": bounds_inset = [xpad, 1 - ypad - width] bounds_frame = [0, 1 - 2 * ypad - width - labspace] - elif loc == 'lower left': + elif loc == "lower left": bounds_inset = [xpad, ypad + labspace] bounds_frame = [0, 0] else: @@ -1828,26 +1964,26 @@ def _parse_colorbar_inset( bounds_frame.extend((2 * xpad + length, 2 * ypad + width + labspace)) # Make axes and frame with zorder matching default legend zorder - cls = mproj.get_projection_class('proplot_cartesian') + cls = mproj.get_projection_class("proplot_cartesian") locator = self._make_inset_locator(bounds_inset, self.transAxes) ax = cls(self.figure, locator(self, None).bounds, zorder=5) - ax.patch.set_facecolor('none') + ax.patch.set_facecolor("none") ax.set_axes_locator(locator) self.add_child_axes(ax) - kw_frame, kwargs = self._parse_frame('colorbar', **kwargs) + kw_frame, kwargs = self._parse_frame("colorbar", **kwargs) if frame: frame = self._add_guide_frame(*bounds_frame, fontsize=fontsize, **kw_frame) # Handle default keyword args - if orientation is not None and orientation != 'horizontal': + if orientation is not None and orientation != "horizontal": warnings._warn_proplot( - f'Orientation for inset colorbars must be horizontal. ' - f'Ignoring orientation={orientation!r}.' + f"Orientation for inset colorbars must be horizontal. " + f"Ignoring orientation={orientation!r}." ) ticklocation = _not_none(tickloc=tickloc, ticklocation=ticklocation) - if ticklocation is not None and ticklocation != 'bottom': - warnings._warn_proplot('Inset colorbars can only have ticks on the bottom.') - kwargs.update({'orientation': 'horizontal', 'ticklocation': 'bottom'}) + if ticklocation is not None and ticklocation != "bottom": + warnings._warn_proplot("Inset colorbars can only have ticks on the bottom.") + kwargs.update({"orientation": "horizontal", "ticklocation": "bottom"}) return ax, kwargs def _parse_legend_aligned(self, pairs, ncol=None, order=None, **kwargs): @@ -1862,7 +1998,7 @@ def _parse_legend_aligned(self, pairs, ncol=None, order=None, **kwargs): array = np.empty((nrow, ncol), dtype=object) for i, pair in enumerate(pairs): array.flat[i] = pair # must be assigned individually - if order == 'C': + if order == "C": array = array.T # Return a legend @@ -1872,8 +2008,15 @@ def _parse_legend_aligned(self, pairs, ncol=None, order=None, **kwargs): return mlegend.Legend(self, *args, ncol=ncol, **kwargs) def _parse_legend_centered( - self, pairs, *, fontsize, - loc=None, title=None, frameon=None, kw_frame=None, **kwargs + self, + pairs, + *, + fontsize, + loc=None, + title=None, + frameon=None, + kw_frame=None, + **kwargs, ): """ Draw "legend" with centered rows by creating separate legends for @@ -1883,18 +2026,18 @@ def _parse_legend_centered( # NOTE: Main legend() function applies default 'legend.loc' of 'best' when # users pass legend=True or call legend without 'loc'. Cannot issue warning. kw_frame = kw_frame or {} - kw_frame['fontsize'] = fontsize - if loc is None or loc == 'best': # white lie - loc = 'upper center' + kw_frame["fontsize"] = fontsize + if loc is None or loc == "best": # white lie + loc = "upper center" if not isinstance(loc, str): raise ValueError( - f'Invalid loc={loc!r} for centered-row legend. Must be string.' + f"Invalid loc={loc!r} for centered-row legend. Must be string." ) - keys = ('bbox_transform', 'bbox_to_anchor') + keys = ("bbox_transform", "bbox_to_anchor") kw_ignore = {key: kwargs.pop(key) for key in keys if key in kwargs} if kw_ignore: warnings._warn_proplot( - f'Ignoring invalid centered-row legend keyword args: {kw_ignore!r}' + f"Ignoring invalid centered-row legend keyword args: {kw_ignore!r}" ) # Iterate and draw @@ -1904,22 +2047,27 @@ def _parse_legend_centered( # confine it in *x*-direction. Matplotlib will automatically move # left-to-right if you request this. legs = [] - kwargs.update({'loc': loc, 'frameon': False}) - space = kwargs.get('labelspacing', None) or rc['legend.labelspacing'] + kwargs.update({"loc": loc, "frameon": False}) + space = kwargs.get("labelspacing", None) or rc["legend.labelspacing"] height = (((1 + space * 0.85) * fontsize) / 72) / self._get_size_inches()[1] for i, ipairs in enumerate(pairs): extra = int(i > 0 and title is not None) - if 'upper' in loc: + if "upper" in loc: base, offset = 1, -extra - elif 'lower' in loc: + elif "lower" in loc: base, offset = 0, len(pairs) else: # center base, offset = 0.5, 0.5 * (len(pairs) - extra) y0, y1 = base + (offset - np.array([i + 1, i])) * height bb = mtransforms.Bbox([[0, y0], [1, y1]]) leg = mlegend.Legend( - self, *zip(*ipairs), bbox_to_anchor=bb, bbox_transform=self.transAxes, - ncol=len(ipairs), title=title if i == 0 else None, **kwargs + self, + *zip(*ipairs), + bbox_to_anchor=bb, + bbox_transform=self.transAxes, + ncol=len(ipairs), + title=title if i == 0 else None, + **kwargs, ) legs.append(leg) @@ -1945,14 +2093,15 @@ def _parse_legend_group(handles, labels=None): """ Parse possibly tuple-grouped input handles. """ + # Helper function. Retrieve labels from a tuple group or from objects # in a container. Multiple labels lead to multiple legend entries. def _legend_label(*objs): # noqa: E301 labs = [] for obj in objs: - if hasattr(obj, 'get_label'): # e.g. silent list + if hasattr(obj, "get_label"): # e.g. silent list lab = obj.get_label() - if lab is not None and str(lab)[:1] != '_': + if lab is not None and str(lab)[:1] != "_": labs.append(lab) return tuple(labs) @@ -1961,16 +2110,17 @@ def _legend_label(*objs): # noqa: E301 # matplotlib containers (important for histogram plots). ignore = (mcontainer.ErrorbarContainer,) containers = (cbook.silent_list, mcontainer.Container) + def _legend_tuple(*objs): # noqa: E306 handles = [] for obj in objs: if isinstance(obj, ignore) and not _legend_label(obj): continue - if hasattr(obj, 'update_scalarmappable'): # for e.g. pcolor + if hasattr(obj, "update_scalarmappable"): # for e.g. pcolor obj.update_scalarmappable() if isinstance(obj, mcontour.ContourSet): # extract single element hs, _ = obj.legend_elements() - label = getattr(obj, '_legend_label', '_no_label') + label = getattr(obj, "_legend_label", "_no_label") if hs: # non-empty obj = hs[len(hs) // 2] obj.set_label(label) @@ -1983,10 +2133,10 @@ def _legend_tuple(*objs): # noqa: E306 handles.append(obj[0]) else: handles.append(obj) - elif hasattr(obj, 'get_label'): + elif hasattr(obj, "get_label"): handles.append(obj) else: - warnings._warn_proplot(f'Ignoring invalid legend handle {obj!r}.') + warnings._warn_proplot(f"Ignoring invalid legend handle {obj!r}.") return tuple(handles) # Sanitize labels. Ignore e.g. extra hist() or hist2d() return values, @@ -2012,14 +2162,20 @@ def _legend_tuple(*objs): # noqa: E306 # Append this handle with some name else: hs = hs[0] if len(hs) == 1 else hs # unfurl for better error messages - label = label if label is not None else labs[0] if labs else '_no_label' + label = label if label is not None else labs[0] if labs else "_no_label" ihandles.append(hs) ilabels.append(label) return ihandles, ilabels def _parse_legend_handles( - self, handles, labels, ncol=None, order=None, center=None, - alphabetize=None, handler_map=None, + self, + handles, + labels, + ncol=None, + order=None, + center=None, + alphabetize=None, + handler_map=None, ): """ Parse input handles and labels. @@ -2033,31 +2189,32 @@ def _parse_legend_handles( np.iterable(obj) and not isinstance(obj, (str, tuple)) ) to_list = lambda obj: ( # noqa: E731 - obj.tolist() if isinstance(obj, np.ndarray) + obj.tolist() + if isinstance(obj, np.ndarray) else obj if obj is None or is_list(obj) else [obj] ) handles, labels = to_list(handles), to_list(labels) if handles and not labels and all(isinstance(h, str) for h in handles): handles, labels = labels, handles multi = any(is_list(h) and len(h) > 1 for h in (handles or ())) - if multi and order == 'F': + if multi and order == "F": warnings._warn_proplot( - 'Column-major ordering of legend handles is not supported ' - 'for horizontally-centered legends.' + "Column-major ordering of legend handles is not supported " + "for horizontally-centered legends." ) if multi and ncol is not None: warnings._warn_proplot( - 'Detected list of *lists* of legend handles. Ignoring ' + "Detected list of *lists* of legend handles. Ignoring " 'the user input property "ncol".' ) if labels and not handles: warnings._warn_proplot( - 'Passing labels without handles is unsupported in proplot. ' - 'Please explicitly pass the handles to legend() or pass labels ' + "Passing labels without handles is unsupported in proplot. " + "Please explicitly pass the handles to legend() or pass labels " "to plotting commands with e.g. plot(data_1d, label='label') or " "plot(data_2d, labels=['label1', 'label2', ...]). After passing " - 'labels to plotting commands you can call legend() without any ' - 'arguments or with the handles as a sole positional argument.' + "labels to plotting commands you can call legend() without any " + "arguments or with the handles as a sole positional argument." ) ncol = _not_none(ncol, 3) center = _not_none(center, multi) @@ -2083,7 +2240,7 @@ def _parse_legend_handles( pairs = pairs[0] if center: multi = True - pairs = [pairs[i * ncol:(i + 1) * ncol] for i in range(len(pairs))] + pairs = [pairs[i * ncol : (i + 1) * ncol] for i in range(len(pairs))] else: if not center: # standardize format based on input multi = False # no longer is list of lists @@ -2098,10 +2255,10 @@ def _range_subplotspec(self, s): Return the column or row range for the subplotspec. """ if not isinstance(self, maxes.SubplotBase): - raise RuntimeError('Axes must be a subplot.') + raise RuntimeError("Axes must be a subplot.") ss = self.get_subplotspec().get_topmost_subplotspec() row1, row2, col1, col2 = ss._get_rows_columns() - if s == 'x': + if s == "x": return (col1, col2) else: return (row1, row2) @@ -2114,7 +2271,7 @@ def _range_tightbbox(self, s): bbox = self._tight_bbox if bbox is None: return np.nan, np.nan - if s == 'x': + if s == "x": return bbox.xmin, bbox.xmax else: return bbox.ymin, bbox.ymax @@ -2123,19 +2280,19 @@ def _sharex_setup(self, sharex, **kwargs): """ Configure x-axis sharing for panels. See also `~CartesianAxes._sharex_setup`. """ - self._share_short_axis(sharex, 'left', **kwargs) # x axis of left panels - self._share_short_axis(sharex, 'right', **kwargs) - self._share_long_axis(sharex, 'bottom', **kwargs) # x axis of bottom panels - self._share_long_axis(sharex, 'top', **kwargs) + self._share_short_axis(sharex, "left", **kwargs) # x axis of left panels + self._share_short_axis(sharex, "right", **kwargs) + self._share_long_axis(sharex, "bottom", **kwargs) # x axis of bottom panels + self._share_long_axis(sharex, "top", **kwargs) def _sharey_setup(self, sharey, **kwargs): """ Configure y-axis sharing for panels. See also `~CartesianAxes._sharey_setup`. """ - self._share_short_axis(sharey, 'bottom', **kwargs) # y axis of bottom panels - self._share_short_axis(sharey, 'top', **kwargs) - self._share_long_axis(sharey, 'left', **kwargs) # y axis of left panels - self._share_long_axis(sharey, 'right', **kwargs) + self._share_short_axis(sharey, "bottom", **kwargs) # y axis of bottom panels + self._share_short_axis(sharey, "top", **kwargs) + self._share_long_axis(sharey, "left", **kwargs) # y axis of left panels + self._share_long_axis(sharey, "right", **kwargs) def _share_short_axis(self, share, side, **kwargs): """ @@ -2143,13 +2300,13 @@ def _share_short_axis(self, share, side, **kwargs): """ if share is None or self._panel_side: return # if this is a panel - s = 'x' if side in ('left', 'right') else 'y' + s = "x" if side in ("left", "right") else "y" caxs = self._panel_dict[side] paxs = share._panel_dict[side] caxs = [pax for pax in caxs if not pax._panel_hidden] paxs = [pax for pax in paxs if not pax._panel_hidden] for cax, pax in zip(caxs, paxs): # may be uneven - getattr(cax, f'_share{s}_setup')(pax, **kwargs) + getattr(cax, f"_share{s}_setup")(pax, **kwargs) def _share_long_axis(self, share, side, **kwargs): """ @@ -2159,11 +2316,11 @@ def _share_long_axis(self, share, side, **kwargs): # sharing with main subplot, not other subplots if share is None or self._panel_side: return # if this is a panel - s = 'x' if side in ('top', 'bottom') else 'y' + s = "x" if side in ("top", "bottom") else "y" paxs = self._panel_dict[side] paxs = [pax for pax in paxs if not pax._panel_hidden] for pax in paxs: - getattr(pax, f'_share{s}_setup')(share, **kwargs) + getattr(pax, f"_share{s}_setup")(share, **kwargs) def _reposition_subplot(self): """ @@ -2177,9 +2334,9 @@ def _reposition_subplot(self): # calls set_in_layout(False) which removes children from get_tightbbox(). # Therefore try to use _set_position() even though it is private if not isinstance(self, maxes.SubplotBase): - raise RuntimeError('Axes must be a subplot.') - setter = getattr(self, '_set_position', self.set_position) - if _version_mpl >= '3.4': + raise RuntimeError("Axes must be a subplot.") + setter = getattr(self, "_set_position", self.set_position) + if _version_mpl >= "3.4": setter(self.get_subplotspec().get_position(self.figure)) else: self.update_params() @@ -2195,22 +2352,22 @@ def _update_abc(self, **kwargs): # is an 'outer' location then re-apply in case 'loc' is subsequently changed kw = rc.fill( { - 'size': 'abc.size', - 'weight': 'abc.weight', - 'color': 'abc.color', - 'family': 'font.family', + "size": "abc.size", + "weight": "abc.weight", + "color": "abc.color", + "family": "font.family", }, - context=True + context=True, ) kwb = rc.fill( { - 'border': 'abc.border', - 'borderwidth': 'abc.borderwidth', - 'bbox': 'abc.bbox', - 'bboxpad': 'abc.bboxpad', - 'bboxcolor': 'abc.bboxcolor', - 'bboxstyle': 'abc.bboxstyle', - 'bboxalpha': 'abc.bboxalpha', + "border": "abc.border", + "borderwidth": "abc.borderwidth", + "bbox": "abc.bbox", + "bboxpad": "abc.bboxpad", + "bboxcolor": "abc.bboxcolor", + "bboxstyle": "abc.bboxstyle", + "bboxalpha": "abc.bboxalpha", }, context=True, ) @@ -2218,37 +2375,37 @@ def _update_abc(self, **kwargs): # A-b-c labels. Build as a...z...aa...zz...aaa...zzz # NOTE: The abc string should already be validated here - abc = rc.find('abc', context=True) # 1st run, or changed + abc = rc.find("abc", context=True) # 1st run, or changed if abc is True: - abc = 'a' + abc = "a" if abc is False: - abc = '' + abc = "" if abc is None or self.number is None: pass elif isinstance(abc, str): nabc, iabc = divmod(self.number - 1, 26) if abc: # should have been validated to contain 'a' or 'A' - old = re.search('[aA]', abc).group() # return first occurrence + old = re.search("[aA]", abc).group() # return first occurrence new = (nabc + 1) * ABC_STRING[iabc] - new = new.upper() if old == 'A' else new + new = new.upper() if old == "A" else new abc = abc.replace(old, new, 1) # replace first occurrence - kw['text'] = abc + kw["text"] = abc else: if self.number > len(abc): raise ValueError( - f'Invalid abc list length {len(abc)} ' - f'for axes with number {self.number}.' + f"Invalid abc list length {len(abc)} " + f"for axes with number {self.number}." ) else: - kw['text'] = abc[self._number - 1] + kw["text"] = abc[self._number - 1] # Update a-b-c label - loc = rc.find('abc.loc', context=True) - loc = self._abc_loc = _translate_loc(loc or self._abc_loc, 'text') - if loc not in ('left', 'right', 'center'): + loc = rc.find("abc.loc", context=True) + loc = self._abc_loc = _translate_loc(loc or self._abc_loc, "text") + if loc not in ("left", "right", "center"): kw.update(self._abc_border_kwargs) kw.update(kwargs) - self._title_dict['abc'].update(kw) + self._title_dict["abc"].update(kw) def _update_title(self, loc, title=None, **kwargs): """ @@ -2265,24 +2422,24 @@ def _update_title(self, loc, title=None, **kwargs): # support in older matplotlib versions. First get params and update kwargs. kw = rc.fill( { - 'size': 'title.size', - 'weight': 'title.weight', - 'color': 'title.color', - 'family': 'font.family', + "size": "title.size", + "weight": "title.weight", + "color": "title.color", + "family": "font.family", }, - context=True + context=True, ) - if 'color' in kw and kw['color'] == 'auto': - del kw['color'] # WARNING: matplotlib permits invalid color here + if "color" in kw and kw["color"] == "auto": + del kw["color"] # WARNING: matplotlib permits invalid color here kwb = rc.fill( { - 'border': 'title.border', - 'borderwidth': 'title.borderwidth', - 'bbox': 'title.bbox', - 'bboxpad': 'title.bboxpad', - 'bboxcolor': 'title.bboxcolor', - 'bboxstyle': 'title.bboxstyle', - 'bboxalpha': 'title.bboxalpha', + "border": "title.border", + "borderwidth": "title.borderwidth", + "bbox": "title.bbox", + "bboxpad": "title.bboxpad", + "bboxcolor": "title.bboxcolor", + "bboxstyle": "title.bboxstyle", + "bboxalpha": "title.bboxalpha", }, context=True, ) @@ -2290,10 +2447,10 @@ def _update_title(self, loc, title=None, **kwargs): # Update the padding settings read at drawtime. Make sure to # update them on the panel axes if 'title.above' is active. - pad = rc.find('abc.titlepad', context=True) + pad = rc.find("abc.titlepad", context=True) if pad is not None: self._abc_title_pad = pad - pad = rc.find('title.pad', context=True) # title + pad = rc.find("title.pad", context=True) # title if pad is not None: self._title_pad = pad self._set_title_offset_trans(pad) @@ -2301,34 +2458,34 @@ def _update_title(self, loc, title=None, **kwargs): # Get the title location. If 'titleloc' was used then transfer text # from the old location to the new location. if loc is not None: - loc = _translate_loc(loc, 'text') + loc = _translate_loc(loc, "text") else: old = self._title_loc - loc = rc.find('title.loc', context=True) - loc = self._title_loc = _translate_loc(loc or self._title_loc, 'text') + loc = rc.find("title.loc", context=True) + loc = self._title_loc = _translate_loc(loc or self._title_loc, "text") if loc != old and old is not None: labels._transfer_label(self._title_dict[old], self._title_dict[loc]) # Update the title text. For outer panels, add text to the panel if # necesssary. For inner panels, use the border and bbox settings. - if loc not in ('left', 'right', 'center'): + if loc not in ("left", "right", "center"): kw.update(self._title_border_kwargs) if title is None: pass elif isinstance(title, str): - kw['text'] = title + kw["text"] = title elif np.iterable(title) and all(isinstance(_, str) for _ in title): if self.number is None: pass elif self.number > len(title): raise ValueError( - f'Invalid title list length {len(title)} ' - f'for axes with number {self.number}.' + f"Invalid title list length {len(title)} " + f"for axes with number {self.number}." ) else: - kw['text'] = title[self.number - 1] + kw["text"] = title[self.number - 1] else: - raise ValueError(f'Invalid title {title!r}. Must be string(s).') + raise ValueError(f"Invalid title {title!r}. Must be string(s).") kw.update(kwargs) self._title_dict[loc].update(kw) @@ -2345,23 +2502,23 @@ def _update_title_position(self, renderer): y_pad = self._title_pad / (72 * height) for loc, obj in self._title_dict.items(): x, y = (0, 1) - if loc == 'abc': # redirect + if loc == "abc": # redirect loc = self._abc_loc - if loc == 'left': + if loc == "left": x = 0 - elif loc == 'center': + elif loc == "center": x = 0.5 - elif loc == 'right': + elif loc == "right": x = 1 - if loc in ('upper center', 'lower center'): + if loc in ("upper center", "lower center"): x = 0.5 - elif loc in ('upper left', 'lower left'): + elif loc in ("upper left", "lower left"): x = x_pad - elif loc in ('upper right', 'lower right'): + elif loc in ("upper right", "lower right"): x = 1 - x_pad - if loc in ('upper left', 'upper right', 'upper center'): + if loc in ("upper left", "upper right", "upper center"): y = 1 - y_pad - elif loc in ('lower left', 'lower right', 'lower center'): + elif loc in ("lower left", "lower right", "lower center"): y = y_pad obj.set_position((x, y)) @@ -2388,7 +2545,7 @@ def _update_title_position(self, renderer): super()._update_title_position(renderer) # Sync the title position with the a-b-c label position - aobj = self._title_dict['abc'] + aobj = self._title_dict["abc"] tobj = self._title_dict[self._abc_loc] aobj.set_transform(tobj.get_transform()) aobj.set_position(tobj.get_position()) @@ -2400,15 +2557,15 @@ def _update_title_position(self, renderer): if not tobj.get_text() or not aobj.get_text(): return awidth, twidth = ( - obj.get_window_extent(renderer).transformed(self.transAxes.inverted()) - .width for obj in (aobj, tobj) + obj.get_window_extent(renderer).transformed(self.transAxes.inverted()).width + for obj in (aobj, tobj) ) ha = aobj.get_ha() pad = (abcpad / 72) / self._get_size_inches()[0] aoffset = toffset = 0 - if ha == 'left': + if ha == "left": toffset = awidth + pad - elif ha == 'right': + elif ha == "right": aoffset = -(twidth + pad) else: # guaranteed center, there are others toffset = 0.5 * (awidth + pad) @@ -2430,10 +2587,10 @@ def _update_super_title(self, suptitle=None, **kwargs): return kw = rc.fill( { - 'size': 'suptitle.size', - 'weight': 'suptitle.weight', - 'color': 'suptitle.color', - 'family': 'font.family' + "size": "suptitle.size", + "weight": "suptitle.weight", + "color": "suptitle.color", + "family": "font.family", }, context=True, ) @@ -2450,11 +2607,11 @@ def _update_super_labels(self, side, labels=None, **kwargs): return # NOTE: see above kw = rc.fill( { - 'color': side + 'label.color', - 'rotation': side + 'label.rotation', - 'size': side + 'label.size', - 'weight': side + 'label.weight', - 'family': 'font.family' + "color": side + "label.color", + "rotation": side + "label.rotation", + "size": side + "label.size", + "weight": side + "label.weight", + "family": "font.family", }, context=True, ) @@ -2464,17 +2621,30 @@ def _update_super_labels(self, side, labels=None, **kwargs): @docstring._snippet_manager def format( - self, *, title=None, title_kw=None, abc_kw=None, - ltitle=None, lefttitle=None, - ctitle=None, centertitle=None, - rtitle=None, righttitle=None, - ultitle=None, upperlefttitle=None, - uctitle=None, uppercentertitle=None, - urtitle=None, upperrighttitle=None, - lltitle=None, lowerlefttitle=None, - lctitle=None, lowercentertitle=None, - lrtitle=None, lowerrighttitle=None, - **kwargs + self, + *, + title=None, + title_kw=None, + abc_kw=None, + ltitle=None, + lefttitle=None, + ctitle=None, + centertitle=None, + rtitle=None, + righttitle=None, + ultitle=None, + upperlefttitle=None, + uctitle=None, + uppercentertitle=None, + urtitle=None, + upperrighttitle=None, + lltitle=None, + lowerlefttitle=None, + lctitle=None, + lowercentertitle=None, + lrtitle=None, + lowerrighttitle=None, + **kwargs, ): """ Modify the a-b-c label, axes title(s), and background patch, @@ -2506,14 +2676,14 @@ def format( proplot.gridspec.SubplotGrid.format proplot.config.Configurator.context """ - skip_figure = kwargs.pop('skip_figure', False) # internal keyword arg + skip_figure = kwargs.pop("skip_figure", False) # internal keyword arg params = _pop_params(kwargs, self.figure._format_signature) # Initiate context block rc_kw, rc_mode = _pop_rc(kwargs) with rc.context(rc_kw, mode=rc_mode): # Behavior of titles in presence of panels - above = rc.find('title.above', context=True) + above = rc.find("title.above", context=True) if above is not None: self._title_above = above # used for future titles @@ -2521,60 +2691,56 @@ def format( abc_kw = abc_kw or {} title_kw = title_kw or {} self._update_abc(**abc_kw) + self._update_title(None, title, **title_kw) self._update_title( - None, - title, - **title_kw - ) - self._update_title( - 'left', + "left", _not_none(ltitle=ltitle, lefttitle=lefttitle), **title_kw, ) self._update_title( - 'center', + "center", _not_none(ctitle=ctitle, centertitle=centertitle), **title_kw, ) self._update_title( - 'right', + "right", _not_none(rtitle=rtitle, righttitle=righttitle), **title_kw, ) self._update_title( - 'upper left', + "upper left", _not_none(ultitle=ultitle, upperlefttitle=upperlefttitle), **title_kw, ) self._update_title( - 'upper center', + "upper center", _not_none(uctitle=uctitle, uppercentertitle=uppercentertitle), - **title_kw + **title_kw, ) self._update_title( - 'upper right', + "upper right", _not_none(urtitle=urtitle, upperrighttitle=upperrighttitle), - **title_kw + **title_kw, ) self._update_title( - 'lower left', + "lower left", _not_none(lltitle=lltitle, lowerlefttitle=lowerlefttitle), - **title_kw + **title_kw, ) self._update_title( - 'lower center', + "lower center", _not_none(lctitle=lctitle, lowercentertitle=lowercentertitle), - **title_kw + **title_kw, ) self._update_title( - 'lower right', + "lower right", _not_none(lrtitle=lrtitle, lowerrighttitle=lowerrighttitle), - **title_kw + **title_kw, ) # Update the axes style # NOTE: This will also raise an error if unknown args are encountered - cycle = rc.find('axes.prop_cycle', context=True) + cycle = rc.find("axes.prop_cycle", context=True) if cycle is not None: self.set_prop_cycle(cycle) self._update_background(**kwargs) @@ -2622,7 +2788,8 @@ def get_default_bbox_extra_artists(self): # NOTE: Matplotlib already tries to do this inside get_tightbbox() but # their approach fails for cartopy axes clipped by paths and not boxes. return [ - artist for artist in super().get_default_bbox_extra_artists() + artist + for artist in super().get_default_bbox_extra_artists() if not self._artist_fully_clipped(artist) ] @@ -2631,7 +2798,7 @@ def set_prop_cycle(self, *args, **kwargs): # Includes both proplot syntax with positional arguments interpreted as # color arguments and oldschool matplotlib cycler(key, value) syntax. if len(args) == 2 and isinstance(args[0], str) and np.iterable(args[1]): - if _pop_props({args[0]: object()}, 'line'): # if a valid line property + if _pop_props({args[0]: object()}, "line"): # if a valid line property kwargs = {args[0]: args[1]} # pass as keyword argument args = () cycle = self._active_cycle = constructor.Cycle(*args, **kwargs) @@ -2659,12 +2826,12 @@ def indicate_inset_zoom(self, **kwargs): # Add the inset indicators parent = self._inset_parent if not parent: - raise ValueError('This command can only be called from an inset axes.') - kwargs.update(_pop_props(kwargs, 'patch')) # impose alternative defaults + raise ValueError("This command can only be called from an inset axes.") + kwargs.update(_pop_props(kwargs, "patch")) # impose alternative defaults if not self._inset_zoom_artists: - kwargs.setdefault('zorder', 3.5) - kwargs.setdefault('linewidth', rc['axes.linewidth']) - kwargs.setdefault('edgecolor', rc['axes.edgecolor']) + kwargs.setdefault("zorder", 3.5) + kwargs.setdefault("linewidth", rc["axes.linewidth"]) + kwargs.setdefault("edgecolor", rc["axes.edgecolor"]) xlim, ylim = self.get_xlim(), self.get_ylim() rect = (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) rectpatch, connects = parent.indicate_inset(rect, self) @@ -2757,23 +2924,25 @@ def colorbar(self, mappable, values=None, loc=None, location=None, **kwargs): """ # Translate location and possibly infer from orientation. Also optionally # infer align setting from keywords stored on object. - orientation = kwargs.get('orientation', None) - kwargs = guides._flush_guide_kw(mappable, 'colorbar', kwargs) + orientation = kwargs.get("orientation", None) + kwargs = guides._flush_guide_kw(mappable, "colorbar", kwargs) loc = _not_none(loc=loc, location=location) if orientation is not None: # possibly infer loc from orientation - if orientation not in ('vertical', 'horizontal'): - raise ValueError(f"Invalid colorbar orientation {orientation!r}. Must be 'vertical' or 'horizontal'.") # noqa: E501 + if orientation not in ("vertical", "horizontal"): + raise ValueError( + f"Invalid colorbar orientation {orientation!r}. Must be 'vertical' or 'horizontal'." + ) # noqa: E501 if loc is None: - loc = {'vertical': 'right', 'horizontal': 'bottom'}[orientation] - loc = _translate_loc(loc, 'colorbar', default=rc['colorbar.loc']) - align = kwargs.pop('align', None) - align = _translate_loc(align, 'align', default='center') + loc = {"vertical": "right", "horizontal": "bottom"}[orientation] + loc = _translate_loc(loc, "colorbar", default=rc["colorbar.loc"]) + align = kwargs.pop("align", None) + align = _translate_loc(align, "align", default="center") # Either draw right now or queue up for later. The queue option lets us # successively append objects (e.g. lines) to a colorbar artist list. - queue = kwargs.pop('queue', False) + queue = kwargs.pop("queue", False) if queue: - self._register_guide('colorbar', (mappable, values), (loc, align), **kwargs) + self._register_guide("colorbar", (mappable, values), (loc, align), **kwargs) else: return self._add_colorbar(mappable, values, loc=loc, align=align, **kwargs) @@ -2829,26 +2998,36 @@ def legend(self, handles=None, labels=None, loc=None, location=None, **kwargs): """ # Translate location and possibly infer from orientation. Also optionally # infer align setting from keywords stored on object. - kwargs = guides._flush_guide_kw(handles, 'legend', kwargs) + kwargs = guides._flush_guide_kw(handles, "legend", kwargs) loc = _not_none(loc=loc, location=location) - loc = _translate_loc(loc, 'legend', default=rc['legend.loc']) - align = kwargs.pop('align', None) - align = _translate_loc(align, 'align', default='center') + loc = _translate_loc(loc, "legend", default=rc["legend.loc"]) + align = kwargs.pop("align", None) + align = _translate_loc(align, "align", default="center") # Either draw right now or queue up for later. Handles can be successively # added to a single location this way. Used for on-the-fly legends. - queue = kwargs.pop('queue', False) + queue = kwargs.pop("queue", False) if queue: - self._register_guide('legend', (handles, labels), (loc, align), **kwargs) + self._register_guide("legend", (handles, labels), (loc, align), **kwargs) else: return self._add_legend(handles, labels, loc=loc, align=align, **kwargs) @docstring._concatenate_inherited @docstring._snippet_manager def text( - self, *args, border=False, bbox=False, - bordercolor='w', borderwidth=2, borderinvert=False, borderstyle='miter', - bboxcolor='w', bboxstyle='round', bboxalpha=0.5, bboxpad=None, **kwargs + self, + *args, + border=False, + bbox=False, + bordercolor="w", + borderwidth=2, + borderinvert=False, + borderstyle="miter", + bboxcolor="w", + bboxstyle="round", + bboxalpha=0.5, + bboxpad=None, + **kwargs, ): """ Add text to the axes. @@ -2899,41 +3078,41 @@ def text( # Translate positional args # Audo-redirect to text2D for 3D axes if not enough arguments passed # NOTE: The transform must be passed positionally for 3D axes with 2D coords - keys = 'xy' + keys = "xy" func = super().text - if self._name == 'three': - if len(args) >= 4 or 'z' in kwargs: - keys += 'z' + if self._name == "three": + if len(args) >= 4 or "z" in kwargs: + keys += "z" else: func = self.text2D - keys = (*keys, ('s', 'text'), 'transform') + keys = (*keys, ("s", "text"), "transform") args, kwargs = _kwargs_to_args(keys, *args, **kwargs) *args, transform = args if any(arg is None for arg in args): - raise TypeError('Missing required positional argument.') + raise TypeError("Missing required positional argument.") if transform is None: transform = self.transData else: transform = self._get_transform(transform) with warnings.catch_warnings(): # ignore duplicates (internal issues?) - warnings.simplefilter('ignore', warnings.ProplotWarning) - kwargs.update(_pop_props(kwargs, 'text')) + warnings.simplefilter("ignore", warnings.ProplotWarning) + kwargs.update(_pop_props(kwargs, "text")) # Update the text object using a monkey patch obj = func(*args, transform=transform, **kwargs) obj.update = labels._update_label.__get__(obj) obj.update( { - 'border': border, - 'bordercolor': bordercolor, - 'borderinvert': borderinvert, - 'borderwidth': borderwidth, - 'borderstyle': borderstyle, - 'bbox': bbox, - 'bboxcolor': bboxcolor, - 'bboxstyle': bboxstyle, - 'bboxalpha': bboxalpha, - 'bboxpad': bboxpad, + "border": border, + "bordercolor": bordercolor, + "borderinvert": borderinvert, + "borderwidth": borderwidth, + "borderstyle": borderstyle, + "bbox": bbox, + "bboxcolor": bboxcolor, + "bboxstyle": bboxstyle, + "bboxalpha": bboxalpha, + "bboxpad": bboxpad, } ) return obj @@ -2955,11 +3134,11 @@ def _iter_axes(self, hidden=False, children=False, panels=True): if panels is False: panels = () elif panels is True or panels is None: - panels = ('left', 'right', 'bottom', 'top') + panels = ("left", "right", "bottom", "top") elif isinstance(panels, str): panels = (panels,) - if not set(panels) <= {'left', 'right', 'bottom', 'top'}: - raise ValueError(f'Invalid sides {panels!r}.') + if not set(panels) <= {"left", "right", "bottom", "top"}: + raise ValueError(f"Invalid sides {panels!r}.") # Iterate axs = (self, *(ax for side in panels for ax in self._panel_dict[side])) for iax in axs: @@ -2985,7 +3164,7 @@ def number(self, num): if num is None or isinstance(num, Integral) and num > 0: self._number = num else: - raise ValueError(f'Invalid number {num!r}. Must be integer >=1.') + raise ValueError(f"Invalid number {num!r}. Must be integer >=1.") # Apply signature obfuscation after storing previous signature diff --git a/proplot/axes/cartesian.py b/proplot/axes/cartesian.py index 1dc88ebf8..09facba2d 100644 --- a/proplot/axes/cartesian.py +++ b/proplot/axes/cartesian.py @@ -17,20 +17,20 @@ from ..internals import _not_none, _pop_rc, _version_mpl, docstring, labels, warnings from . import plot, shared -__all__ = ['CartesianAxes'] +__all__ = ["CartesianAxes"] # Tuple of date converters DATE_CONVERTERS = (mdates.DateConverter,) -if hasattr(mdates, '_SwitchableDateConverter'): +if hasattr(mdates, "_SwitchableDateConverter"): DATE_CONVERTERS += (mdates._SwitchableDateConverter,) # Opposite side keywords OPPOSITE_SIDE = { - 'left': 'right', - 'right': 'left', - 'bottom': 'top', - 'top': 'bottom', + "left": "right", + "right": "left", + "bottom": "top", + "top": "bottom", } @@ -208,17 +208,25 @@ If your axis ticks are doing weird things (for example, ticks are drawn outside of the axis spine) you can try setting this to ``True``. """ -docstring._snippet_manager['cartesian.format'] = _format_docstring +docstring._snippet_manager["cartesian.format"] = _format_docstring # Shared docstring _shared_x_keys = { - 'x': 'x', 'x1': 'bottom', 'x2': 'top', - 'y': 'y', 'y1': 'left', 'y2': 'right', + "x": "x", + "x1": "bottom", + "x2": "top", + "y": "y", + "y1": "left", + "y2": "right", } _shared_y_keys = { - 'x': 'y', 'x1': 'left', 'x2': 'right', - 'y': 'x', 'y1': 'bottom', 'y2': 'top', + "x": "y", + "x1": "left", + "x2": "right", + "y": "x", + "y1": "bottom", + "y2": "top", } _shared_docstring = """ %(descrip)s @@ -261,9 +269,9 @@ `~proplot.axes.CartesianAxes.twin{y}`, which generates two {x} axes with a shared ("twin") {y} axes. """ -_alt_docstring = _shared_docstring % {'descrip': _alt_descrip, 'extra': ''} -docstring._snippet_manager['axes.altx'] = _alt_docstring.format(**_shared_x_keys) -docstring._snippet_manager['axes.alty'] = _alt_docstring.format(**_shared_y_keys) +_alt_docstring = _shared_docstring % {"descrip": _alt_descrip, "extra": ""} +docstring._snippet_manager["axes.altx"] = _alt_docstring.format(**_shared_x_keys) +docstring._snippet_manager["axes.alty"] = _alt_docstring.format(**_shared_y_keys) # Twin docstrings # NOTE: Used by SubplotGrid.twinx @@ -272,9 +280,9 @@ distinct {x} axis. This builds upon `matplotlib.axes.Axes.twin{y}`. """ -_twin_docstring = _shared_docstring % {'descrip': _twin_descrip, 'extra': ''} -docstring._snippet_manager['axes.twinx'] = _twin_docstring.format(**_shared_y_keys) -docstring._snippet_manager['axes.twiny'] = _twin_docstring.format(**_shared_x_keys) +_twin_docstring = _shared_docstring % {"descrip": _twin_descrip, "extra": ""} +docstring._snippet_manager["axes.twinx"] = _twin_docstring.format(**_shared_y_keys) +docstring._snippet_manager["axes.twiny"] = _twin_docstring.format(**_shared_x_keys) # Dual docstrings # NOTE: Used by SubplotGrid.dualx @@ -293,9 +301,12 @@ will be used to build a `~proplot.scale.FuncScale` and applied to the dual axis (see `~proplot.scale.FuncScale` for details). """ -_dual_docstring = _shared_docstring % {'descrip': _dual_descrip, 'extra': _dual_extra.lstrip()} # noqa: E501 -docstring._snippet_manager['axes.dualx'] = _dual_docstring.format(**_shared_x_keys) -docstring._snippet_manager['axes.dualy'] = _dual_docstring.format(**_shared_y_keys) +_dual_docstring = _shared_docstring % { + "descrip": _dual_descrip, + "extra": _dual_extra.lstrip(), +} # noqa: E501 +docstring._snippet_manager["axes.dualx"] = _dual_docstring.format(**_shared_x_keys) +docstring._snippet_manager["axes.dualy"] = _dual_docstring.format(**_shared_y_keys) class CartesianAxes(shared._SharedAxes, plot.PlotAxes): @@ -310,8 +321,9 @@ class CartesianAxes(shared._SharedAxes, plot.PlotAxes): to axes-creation commands like `~proplot.figure.Figure.add_axes`, `~proplot.figure.Figure.add_subplot`, and `~proplot.figure.Figure.subplots`. """ - _name = 'cartesian' - _name_aliases = ('cart', 'rect', 'rectilinar') # include matplotlib name + + _name = "cartesian" + _name_aliases = ("cart", "rect", "rectilinar") # include matplotlib name @docstring._snippet_manager def __init__(self, *args, **kwargs): @@ -336,8 +348,8 @@ def __init__(self, *args, **kwargs): proplot.figure.Figure.add_subplot """ # Initialize axes - self._xaxis_current_rotation = 'horizontal' # current rotation - self._yaxis_current_rotation = 'horizontal' + self._xaxis_current_rotation = "horizontal" # current rotation + self._yaxis_current_rotation = "horizontal" self._xaxis_isdefault_rotation = True # whether to auto rotate the axis self._yaxis_isdefault_rotation = True super().__init__(*args, **kwargs) @@ -378,7 +390,7 @@ def _apply_axis_sharing(self): if level > 2: # WARNING: Cannot set NullFormatter because shared axes share the # same Ticker(). Instead use approach copied from mpl subplots(). - axis.set_tick_params(which='both', labelbottom=False, labeltop=False) + axis.set_tick_params(which="both", labelbottom=False, labeltop=False) # Y axis axis = self.yaxis if self._sharey is not None and axis.get_visible(): @@ -387,7 +399,7 @@ def _apply_axis_sharing(self): labels._transfer_label(axis.label, self._sharey.yaxis.label) axis.label.set_visible(False) if level > 2: - axis.set_tick_params(which='both', labelleft=False, labelright=False) + axis.set_tick_params(which="both", labelleft=False, labelright=False) axis.set_minor_formatter(mticker.NullFormatter()) def _add_alt(self, sx, **kwargs): @@ -402,37 +414,39 @@ def _add_alt(self, sx, **kwargs): # To restore matplotlib behavior, which draws "child" artists on top simply # because the axes was created after the "parent" one, use the inset_axes # zorder of 4 and make the background transparent. - sy = 'y' if sx == 'x' else 'x' + sy = "y" if sx == "x" else "x" sig = self._format_signatures[CartesianAxes] keys = tuple(key[1:] for key in sig.parameters if key[0] == sx) - kwargs = {(sx + key if key in keys else key): val for key, val in kwargs.items()} # noqa: E501 - if f'{sy}spineloc' not in kwargs: # acccount for aliases - kwargs.setdefault(f'{sy}loc', 'neither') - if f'{sx}spineloc' not in kwargs: # account for aliases - kwargs.setdefault(f'{sx}loc', 'top' if sx == 'x' else 'right') - kwargs.setdefault(f'autoscale{sy}_on', getattr(self, f'get_autoscale{sy}_on')()) - kwargs.setdefault(f'share{sy}', self) + kwargs = { + (sx + key if key in keys else key): val for key, val in kwargs.items() + } # noqa: E501 + if f"{sy}spineloc" not in kwargs: # acccount for aliases + kwargs.setdefault(f"{sy}loc", "neither") + if f"{sx}spineloc" not in kwargs: # account for aliases + kwargs.setdefault(f"{sx}loc", "top" if sx == "x" else "right") + kwargs.setdefault(f"autoscale{sy}_on", getattr(self, f"get_autoscale{sy}_on")()) + kwargs.setdefault(f"share{sy}", self) # Initialize child axes - kwargs.setdefault('grid', False) # note xgrid=True would override this - kwargs.setdefault('zorder', 4) # increased default zorder - kwargs.setdefault('number', None) - kwargs.setdefault('autoshare', False) - if 'sharex' in kwargs and 'sharey' in kwargs: - raise ValueError('Twinned axes may share only one axis.') + kwargs.setdefault("grid", False) # note xgrid=True would override this + kwargs.setdefault("zorder", 4) # increased default zorder + kwargs.setdefault("number", None) + kwargs.setdefault("autoshare", False) + if "sharex" in kwargs and "sharey" in kwargs: + raise ValueError("Twinned axes may share only one axis.") locator = self._make_inset_locator([0, 0, 1, 1], self.transAxes) ax = CartesianAxes(self.figure, locator(self, None).bounds, **kwargs) ax.set_axes_locator(locator) - ax.set_adjustable('datalim') + ax.set_adjustable("datalim") self.add_child_axes(ax) # to facilitate tight layout - self.set_adjustable('datalim') + self.set_adjustable("datalim") self._twinned_axes.join(self, ax) # Format parent and child axes - self.format(**{f'{sx}loc': OPPOSITE_SIDE.get(kwargs[f'{sx}loc'], None)}) - setattr(ax, f'_alt{sx}_parent', self) - getattr(ax, f'{sy}axis').set_visible(False) - getattr(ax, 'patch').set_visible(False) + self.format(**{f"{sx}loc": OPPOSITE_SIDE.get(kwargs[f"{sx}loc"], None)}) + setattr(ax, f"_alt{sx}_parent", self) + getattr(ax, f"{sy}axis").set_visible(False) + getattr(ax, "patch").set_visible(False) return ax def _dual_scale(self, s, funcscale=None): @@ -448,27 +462,27 @@ def _dual_scale(self, s, funcscale=None): # and limits have changed, and limits are always applied before we reach # the child.draw() because always called after parent.draw() child = self - parent = getattr(self, f'_alt{s}_parent') + parent = getattr(self, f"_alt{s}_parent") if funcscale is not None: - setattr(self, f'_dual{s}_funcscale', funcscale) + setattr(self, f"_dual{s}_funcscale", funcscale) else: - funcscale = getattr(self, f'_dual{s}_funcscale') + funcscale = getattr(self, f"_dual{s}_funcscale") if parent is None or funcscale is None: return - olim = getattr(parent, f'get_{s}lim')() - scale = getattr(parent, f'{s}axis')._scale - if (scale, *olim) == getattr(child, f'_dual{s}_prevstate'): + olim = getattr(parent, f"get_{s}lim")() + scale = getattr(parent, f"{s}axis")._scale + if (scale, *olim) == getattr(child, f"_dual{s}_prevstate"): return funcscale = pscale.FuncScale(funcscale, invert=True, parent_scale=scale) - caxis = getattr(child, f'{s}axis') + caxis = getattr(child, f"{s}axis") caxis._scale = funcscale child._update_transScale() funcscale.set_default_locators_and_formatters(caxis, only_if_default=True) nlim = list(map(funcscale.functions[1], np.array(olim))) if np.sign(np.diff(olim)) != np.sign(np.diff(nlim)): nlim = nlim[::-1] # if function flips limits, so will set_xlim! - getattr(child, f'set_{s}lim')(nlim, emit=False) - setattr(child, f'_dual{s}_prevstate', (scale, *olim)) + getattr(child, f"set_{s}lim")(nlim, emit=False) + setattr(child, f"_dual{s}_prevstate", (scale, *olim)) def _fix_ticks(self, s, fixticks=False): """ @@ -481,23 +495,26 @@ def _fix_ticks(self, s, fixticks=False): # successfully calling this and messing up the ticks for some reason. # So avoid using this when possible, and try to make behavior consistent # by cacheing the locators before we use them for ticks. - axis = getattr(self, f'{s}axis') - sides = ('bottom', 'top') if s == 'x' else ('left', 'right') - l0, l1 = getattr(self, f'get_{s}lim')() + axis = getattr(self, f"{s}axis") + sides = ("bottom", "top") if s == "x" else ("left", "right") + l0, l1 = getattr(self, f"get_{s}lim")() bounds = tuple(self.spines[side].get_bounds() or (None, None) for side in sides) skipticks = lambda ticks: [ # noqa: E731 - x for x in ticks - if not any(x < _not_none(b0, l0) or x > _not_none(b1, l1) for (b0, b1) in bounds) # noqa: E501 + x + for x in ticks + if not any( + x < _not_none(b0, l0) or x > _not_none(b1, l1) for (b0, b1) in bounds + ) # noqa: E501 ] if fixticks or any(x is not None for b in bounds for x in b): # Major locator - locator = getattr(axis, '_major_locator_cached', None) + locator = getattr(axis, "_major_locator_cached", None) if locator is None: locator = axis._major_locator_cached = axis.get_major_locator() locator = constructor.Locator(skipticks(locator())) axis.set_major_locator(locator) # Minor locator - locator = getattr(axis, '_minor_locator_cached', None) + locator = getattr(axis, "_minor_locator_cached", None) if locator is None: locator = axis._minor_locator_cached = axis.get_minor_locator() locator = constructor.Locator(skipticks(locator())) @@ -510,14 +527,14 @@ def _get_spine_side(self, s, loc): """ # NOTE: Could defer error to CartesianAxes.format but instead use our # own error message with info on coordinate position options. - sides = ('bottom', 'top') if s == 'x' else ('left', 'right') - centers = ('zero', 'center') - options = (*(s[0] for s in sides), *sides, 'both', 'neither', 'none') - if np.iterable(loc) and len(loc) == 2 and loc[0] in ('axes', 'data', 'outward'): - lim = getattr(self, f'get_{s}lim')() - if loc[0] == 'outward': # ambiguous so just choose first side + sides = ("bottom", "top") if s == "x" else ("left", "right") + centers = ("zero", "center") + options = (*(s[0] for s in sides), *sides, "both", "neither", "none") + if np.iterable(loc) and len(loc) == 2 and loc[0] in ("axes", "data", "outward"): + lim = getattr(self, f"get_{s}lim")() + if loc[0] == "outward": # ambiguous so just choose first side side = sides[0] - elif loc[0] == 'axes': + elif loc[0] == "axes": side = sides[int(loc[1] > 0.5)] else: side = sides[int(loc[1] > lim[0] + 0.5 * (lim[1] - lim[0]))] @@ -527,8 +544,8 @@ def _get_spine_side(self, s, loc): side = loc else: raise ValueError( - f'Invalid {s} spine location {loc!r}. Options are: ' - + ', '.join(map(repr, (*options, *centers))) + f"Invalid {s} spine location {loc!r}. Options are: " + + ", ".join(map(repr, (*options, *centers))) + " or a coordinate position ('axes', coord), " + " ('data', coord), or ('outward', coord)." ) @@ -541,7 +558,8 @@ def _is_panel_group_member(self, other): return ( self._panel_parent is other # other is child panel or other._panel_parent is self # other is main subplot - or other._panel_parent and self._panel_parent # ... + or other._panel_parent + and self._panel_parent # ... and other._panel_parent is self._panel_parent # other is sibling panel ) @@ -552,13 +570,13 @@ def _sharex_limits(self, sharex): # Copy non-default limits and scales. Either this axes or the input # axes could be a newly-created subplot while the other is a subplot # with possibly-modified user settings we are careful to preserve. - for (ax1, ax2) in ((self, sharex), (sharex, self)): - if ax1.get_xscale() == 'linear' and ax2.get_xscale() != 'linear': + for ax1, ax2 in ((self, sharex), (sharex, self)): + if ax1.get_xscale() == "linear" and ax2.get_xscale() != "linear": ax1.set_xscale(ax2.get_xscale()) # non-default scale if ax1.get_autoscalex_on() and not ax2.get_autoscalex_on(): ax1.set_xlim(ax2.get_xlim()) # non-default limits # Copy non-default locators and formatters - self.get_shared_x_axes().join(self, sharex) # share limit/scale changes + self.get_shared_x_axes().joined(self, sharex) # share limit/scale changes if sharex.xaxis.isDefault_majloc and not self.xaxis.isDefault_majloc: sharex.xaxis.set_major_locator(self.xaxis.get_major_locator()) if sharex.xaxis.isDefault_minloc and not self.xaxis.isDefault_minloc: @@ -575,12 +593,12 @@ def _sharey_limits(self, sharey): Safely share limits and tickers without resetting things. """ # NOTE: See _sharex_limits for notes - for (ax1, ax2) in ((self, sharey), (sharey, self)): - if ax1.get_yscale() == 'linear' and ax2.get_yscale() != 'linear': + for ax1, ax2 in ((self, sharey), (sharey, self)): + if ax1.get_yscale() == "linear" and ax2.get_yscale() != "linear": ax1.set_yscale(ax2.get_yscale()) if ax1.get_autoscaley_on() and not ax2.get_autoscaley_on(): ax1.set_ylim(ax2.get_ylim()) - self.get_shared_y_axes().join(self, sharey) # share limit/scale changes + self.get_shared_y_axes().joined(self, sharey) # share limit/scale changes if sharey.yaxis.isDefault_majloc and not self.yaxis.isDefault_majloc: sharey.yaxis.set_major_locator(self.yaxis.get_major_locator()) if sharey.yaxis.isDefault_minloc and not self.yaxis.isDefault_minloc: @@ -601,11 +619,12 @@ def _sharex_setup(self, sharex, *, labels=True, limits=True): super()._sharex_setup(sharex) # Get the axis sharing level level = ( - 3 if self._panel_sharex_group and self._is_panel_group_member(sharex) + 3 + if self._panel_sharex_group and self._is_panel_group_member(sharex) else self.figure._sharex ) if level not in range(5): # must be internal error - raise ValueError(f'Invalid sharing level sharex={level!r}.') + raise ValueError(f"Invalid sharing level sharex={level!r}.") if sharex in (None, self) or not isinstance(sharex, CartesianAxes): return # Share future axis label changes. Implemented in _apply_axis_sharing(). @@ -627,11 +646,12 @@ def _sharey_setup(self, sharey, *, labels=True, limits=True): # NOTE: See _sharex_setup for notes super()._sharey_setup(sharey) level = ( - 3 if self._panel_sharey_group and self._is_panel_group_member(sharey) + 3 + if self._panel_sharey_group and self._is_panel_group_member(sharey) else self.figure._sharey ) if level not in range(5): # must be internal error - raise ValueError(f'Invalid sharing level sharey={level!r}.') + raise ValueError(f"Invalid sharing level sharey={level!r}.") if sharey in (None, self) or not isinstance(sharey, CartesianAxes): return if level > 0 and labels: @@ -640,8 +660,13 @@ def _sharey_setup(self, sharey, *, labels=True, limits=True): self._sharey_limits(sharey) def _update_formatter( - self, s, formatter=None, *, formatter_kw=None, - tickrange=None, wraprange=None, + self, + s, + formatter=None, + *, + formatter_kw=None, + tickrange=None, + wraprange=None, ): """ Update the axis formatter. Passes `formatter` through `Formatter` with kwargs. @@ -649,7 +674,7 @@ def _update_formatter( # Test if this is date axes # See: https://matplotlib.org/api/units_api.html # And: https://matplotlib.org/api/dates_api.html - axis = getattr(self, f'{s}axis') + axis = getattr(self, f"{s}axis") date = isinstance(axis.converter, DATE_CONVERTERS) # Major formatter @@ -658,25 +683,30 @@ def _update_formatter( # formatter_kw input without formatter and use 'auto' as the default. formatter_kw = formatter_kw or {} formatter_kw = formatter_kw.copy() - if formatter is not None or tickrange is not None or wraprange is not None or formatter_kw: # noqa: E501 + if ( + formatter is not None + or tickrange is not None + or wraprange is not None + or formatter_kw + ): # noqa: E501 # Tick range - formatter = _not_none(formatter, 'auto') + formatter = _not_none(formatter, "auto") if tickrange is not None or wraprange is not None: - if formatter != 'auto': + if formatter != "auto": warnings._warn_proplot( - 'The tickrange and autorange features require ' - 'proplot.AutoFormatter formatter. Overriding the input.' + "The tickrange and autorange features require " + "proplot.AutoFormatter formatter. Overriding the input." ) if tickrange is not None: - formatter_kw.setdefault('tickrange', tickrange) + formatter_kw.setdefault("tickrange", tickrange) if wraprange is not None: - formatter_kw.setdefault('wraprange', wraprange) + formatter_kw.setdefault("wraprange", wraprange) # Set the formatter # Note some formatters require 'locator' as keyword arg - if formatter in ('date', 'concise'): + if formatter in ("date", "concise"): locator = axis.get_major_locator() - formatter_kw.setdefault('locator', locator) + formatter_kw.setdefault("locator", locator) formatter = constructor.Formatter(formatter, date=date, **formatter_kw) axis.set_major_formatter(formatter) @@ -696,21 +726,27 @@ def _update_labels(self, s, *args, **kwargs): no_kwargs = all(v is None for v in kwargs.values()) if no_args and no_kwargs: return # also returns if args and kwargs are empty - setter = getattr(self, f'set_{s}label') - getter = getattr(self, f'get_{s}label') + setter = getattr(self, f"set_{s}label") + getter = getattr(self, f"get_{s}label") if no_args: # otherwise label text is reset! args = (getter(),) setter(*args, **kwargs) def _update_locators( - self, s, locator=None, minorlocator=None, *, - tickminor=None, locator_kw=None, minorlocator_kw=None, + self, + s, + locator=None, + minorlocator=None, + *, + tickminor=None, + locator_kw=None, + minorlocator_kw=None, ): """ Update the locators. Requires `Locator` instances. """ # Apply input major locator - axis = getattr(self, f'{s}axis') + axis = getattr(self, f"{s}axis") locator_kw = locator_kw or {} if locator is not None: locator = constructor.Locator(locator, **locator_kw) @@ -728,9 +764,9 @@ def _update_locators( if not isdefault: minorlocator = constructor.Locator(minorlocator, **minorlocator_kw) elif tickminor: - minorlocator = getattr(axis._scale, '_default_minor_locator', None) + minorlocator = getattr(axis._scale, "_default_minor_locator", None) minorlocator = copy.copy(minorlocator) - minorlocator = constructor.Locator(minorlocator or 'minor') + minorlocator = constructor.Locator(minorlocator or "minor") if minorlocator is not None: axis.set_minor_locator(minorlocator) axis.isDefault_minloc = isdefault @@ -741,7 +777,7 @@ def _update_locators( # *disable* minor ticks, do not want FuncScale applications to turn them # on. So we allow below to set isDefault_minloc to False. if tickminor is not None and not tickminor: - axis.set_minor_locator(constructor.Locator('null')) + axis.set_minor_locator(constructor.Locator("null")) def _update_limits(self, s, *, min_=None, max_=None, lim=None, reverse=None): """ @@ -750,12 +786,12 @@ def _update_limits(self, s, *, min_=None, max_=None, lim=None, reverse=None): # Set limits for just one side or both at once lim = self._min_max_lim(s, min_, max_, lim) if any(_ is not None for _ in lim): - getattr(self, f'set_{s}lim')(lim) + getattr(self, f"set_{s}lim")(lim) # Reverse direction # NOTE: 3.1+ has axis.set_inverted(), below is from source code if reverse is not None: - axis = getattr(self, f'{s}axis') + axis = getattr(self, f"{s}axis") lo, hi = axis.get_view_interval() if reverse: lim = (max(lo, hi), min(lo, hi)) @@ -771,24 +807,24 @@ def _update_rotation(self, s, *, rotation=None): # NOTE: Rotation is done *before* horizontal/vertical alignment. Cannot # change alignment with set_tick_params so we must apply to text objects. # Note fig.autofmt_date calls subplots_adjust, so we cannot use it. - current = f'_{s}axis_current_rotation' - default = f'_{s}axis_isdefault_rotation' - axis = getattr(self, f'{s}axis') + current = f"_{s}axis_current_rotation" + default = f"_{s}axis_isdefault_rotation" + axis = getattr(self, f"{s}axis") if rotation is not None: setattr(self, default, False) elif not getattr(self, default): return # do not rotate - elif s == 'x' and isinstance(axis.converter, DATE_CONVERTERS): - rotation = rc['formatter.timerotation'] + elif s == "x" and isinstance(axis.converter, DATE_CONVERTERS): + rotation = rc["formatter.timerotation"] else: - rotation = 'horizontal' + rotation = "horizontal" # Apply tick label rotation if necessary if rotation != getattr(self, current): - rotation = {'horizontal': 0, 'vertical': 90}.get(rotation, rotation) - kw = {'rotation': rotation} + rotation = {"horizontal": 0, "vertical": 90}.get(rotation, rotation) + kw = {"rotation": rotation} if rotation not in (0, 90, -90): - kw['ha'] = 'right' if rotation > 0 else 'left' + kw["ha"] = "right" if rotation > 0 else "left" for label in axis.get_ticklabels(): label.update(kw) setattr(self, current, rotation) @@ -799,7 +835,7 @@ def _update_spines(self, s, *, loc=None, bounds=None): """ # Change default spine location from 'both' to the first # relevant side if the user passes 'bounds'. - sides = ('bottom', 'top') if s == 'x' else ('left', 'right') + sides = ("bottom", "top") if s == "x" else ("left", "right") opts = (*(s[0] for s in sides), *sides) # see _get_spine_side() side = self._get_spine_side(s, loc) # side for set_position() if bounds is not None and all(self.spines[s].get_visible() for s in sides): @@ -810,9 +846,9 @@ def _update_spines(self, s, *, loc=None, bounds=None): spine = self.spines[key] if loc is None: pass - elif loc == 'neither' or loc == 'none': + elif loc == "neither" or loc == "none": spine.set_visible(False) - elif loc == 'both': + elif loc == "both": spine.set_visible(True) elif loc in opts: spine.set_visible(key[0] == loc[0]) @@ -833,107 +869,163 @@ def _update_locs( """ Update the tick, tick label, and axis label locations. """ + # Helper function and initial stuff def _validate_loc(loc, opts, descrip): try: return opts[loc] except KeyError: raise ValueError( - f'Invalid {descrip} location {loc!r}. Options are ' - + ', '.join(map(repr, sides + tuple(opts))) + '.' + f"Invalid {descrip} location {loc!r}. Options are " + + ", ".join(map(repr, sides + tuple(opts))) + + "." ) - sides = ('bottom', 'top') if s == 'x' else ('left', 'right') + + sides = ("bottom", "top") if s == "x" else ("left", "right") sides_active = tuple(side for side in sides if self.spines[side].get_visible()) label_opts = {s[:i]: s for s in sides for i in (1, None)} - tick_opts = {'both': sides, 'neither': (), 'none': (), None: None} + tick_opts = {"both": sides, "neither": (), "none": (), None: None} tick_opts.update({k: (v,) for k, v in label_opts.items()}) # Apply the tick mark and tick label locations kw = {} kw.update({side: False for side in sides if side not in sides_active}) - kw.update({'label' + side: False for side in sides if side not in sides_active}) + kw.update({"label" + side: False for side in sides if side not in sides_active}) if ticklabelloc is not None: - ticklabelloc = _validate_loc(ticklabelloc, tick_opts, 'tick label') - kw.update({'label' + side: side in ticklabelloc for side in sides}) + ticklabelloc = _validate_loc(ticklabelloc, tick_opts, "tick label") + kw.update({"label" + side: side in ticklabelloc for side in sides}) if tickloc is not None: # possibly overrides ticklabelloc - tickloc = _validate_loc(tickloc, tick_opts, 'tick mark') + tickloc = _validate_loc(tickloc, tick_opts, "tick mark") kw.update({side: side in tickloc for side in sides}) - kw.update({'label' + side: False for side in sides if side not in tickloc}) - self.tick_params(axis=s, which='both', **kw) + kw.update({"label" + side: False for side in sides if side not in tickloc}) + self.tick_params(axis=s, which="both", **kw) # Apply the axis label and offset label locations # Uses ugly mpl 3.3+ tick_top() tick_bottom() kludge for offset location # See: https://matplotlib.org/3.3.1/users/whats_new.html - axis = getattr(self, f'{s}axis') - options = tuple(_ for _ in sides if tickloc and _ in tickloc and _ in sides_active) # noqa: E501 + axis = getattr(self, f"{s}axis") + options = tuple( + _ for _ in sides if tickloc and _ in tickloc and _ in sides_active + ) # noqa: E501 if tickloc is not None and len(options) == 1: labelloc = _not_none(labelloc, options[0]) offsetloc = _not_none(offsetloc, options[0]) if labelloc is not None: - labelloc = _validate_loc(labelloc, label_opts, 'axis label') + labelloc = _validate_loc(labelloc, label_opts, "axis label") axis.set_label_position(labelloc) if offsetloc is not None: offsetloc = _not_none(offsetloc, options[0]) - if hasattr(axis, 'set_offset_position'): # y axis (and future x axis?) + if hasattr(axis, "set_offset_position"): # y axis (and future x axis?) axis.set_offset_position(offsetloc) - elif s == 'x' and _version_mpl >= '3.3': # ugly x axis kludge + elif s == "x" and _version_mpl >= "3.3": # ugly x axis kludge axis._tick_position = offsetloc axis.offsetText.set_verticalalignment(OPPOSITE_SIDE[offsetloc]) @docstring._snippet_manager def format( - self, *, + self, + *, aspect=None, - xloc=None, yloc=None, - xspineloc=None, yspineloc=None, - xoffsetloc=None, yoffsetloc=None, - xwraprange=None, ywraprange=None, - xreverse=None, yreverse=None, - xlim=None, ylim=None, - xmin=None, ymin=None, - xmax=None, ymax=None, - xscale=None, yscale=None, - xbounds=None, ybounds=None, - xmargin=None, ymargin=None, - xrotation=None, yrotation=None, - xformatter=None, yformatter=None, - xticklabels=None, yticklabels=None, - xticks=None, yticks=None, - xlocator=None, ylocator=None, - xminorticks=None, yminorticks=None, - xminorlocator=None, yminorlocator=None, - xcolor=None, ycolor=None, - xlinewidth=None, ylinewidth=None, - xtickloc=None, ytickloc=None, fixticks=False, - xtickdir=None, ytickdir=None, - xtickminor=None, ytickminor=None, - xtickrange=None, ytickrange=None, - xtickcolor=None, ytickcolor=None, - xticklen=None, yticklen=None, - xticklenratio=None, yticklenratio=None, - xtickwidth=None, ytickwidth=None, - xtickwidthratio=None, ytickwidthratio=None, - xticklabelloc=None, yticklabelloc=None, - xticklabeldir=None, yticklabeldir=None, - xticklabelpad=None, yticklabelpad=None, - xticklabelcolor=None, yticklabelcolor=None, - xticklabelsize=None, yticklabelsize=None, - xticklabelweight=None, yticklabelweight=None, - xlabel=None, ylabel=None, - xlabelloc=None, ylabelloc=None, - xlabelpad=None, ylabelpad=None, - xlabelcolor=None, ylabelcolor=None, - xlabelsize=None, ylabelsize=None, - xlabelweight=None, ylabelweight=None, - xgrid=None, ygrid=None, - xgridminor=None, ygridminor=None, - xgridcolor=None, ygridcolor=None, - xlabel_kw=None, ylabel_kw=None, - xscale_kw=None, yscale_kw=None, - xlocator_kw=None, ylocator_kw=None, - xformatter_kw=None, yformatter_kw=None, - xminorlocator_kw=None, yminorlocator_kw=None, - **kwargs + xloc=None, + yloc=None, + xspineloc=None, + yspineloc=None, + xoffsetloc=None, + yoffsetloc=None, + xwraprange=None, + ywraprange=None, + xreverse=None, + yreverse=None, + xlim=None, + ylim=None, + xmin=None, + ymin=None, + xmax=None, + ymax=None, + xscale=None, + yscale=None, + xbounds=None, + ybounds=None, + xmargin=None, + ymargin=None, + xrotation=None, + yrotation=None, + xformatter=None, + yformatter=None, + xticklabels=None, + yticklabels=None, + xticks=None, + yticks=None, + xlocator=None, + ylocator=None, + xminorticks=None, + yminorticks=None, + xminorlocator=None, + yminorlocator=None, + xcolor=None, + ycolor=None, + xlinewidth=None, + ylinewidth=None, + xtickloc=None, + ytickloc=None, + fixticks=False, + xtickdir=None, + ytickdir=None, + xtickminor=None, + ytickminor=None, + xtickrange=None, + ytickrange=None, + xtickcolor=None, + ytickcolor=None, + xticklen=None, + yticklen=None, + xticklenratio=None, + yticklenratio=None, + xtickwidth=None, + ytickwidth=None, + xtickwidthratio=None, + ytickwidthratio=None, + xticklabelloc=None, + yticklabelloc=None, + xticklabeldir=None, + yticklabeldir=None, + xticklabelpad=None, + yticklabelpad=None, + xticklabelcolor=None, + yticklabelcolor=None, + xticklabelsize=None, + yticklabelsize=None, + xticklabelweight=None, + yticklabelweight=None, + xlabel=None, + ylabel=None, + xlabelloc=None, + ylabelloc=None, + xlabelpad=None, + ylabelpad=None, + xlabelcolor=None, + ylabelcolor=None, + xlabelsize=None, + ylabelsize=None, + xlabelweight=None, + ylabelweight=None, + xgrid=None, + ygrid=None, + xgridminor=None, + ygridminor=None, + xgridcolor=None, + ygridcolor=None, + xlabel_kw=None, + ylabel_kw=None, + xscale_kw=None, + yscale_kw=None, + xlocator_kw=None, + ylocator_kw=None, + xformatter_kw=None, + yformatter_kw=None, + xminorlocator_kw=None, + yminorlocator_kw=None, + **kwargs, ): """ Modify axes limits, axis scales, axis labels, spine locations, @@ -978,39 +1070,59 @@ def format( yminorlocator_kw = yminorlocator_kw or {} # Color keyword arguments. Inherit from 'color' when necessary - color = kwargs.pop('color', None) + color = kwargs.pop("color", None) xcolor = _not_none(xcolor, color) ycolor = _not_none(ycolor, color) - if 'tick.color' not in rc_kw: + if "tick.color" not in rc_kw: xtickcolor = _not_none(xtickcolor, xcolor) ytickcolor = _not_none(ytickcolor, ycolor) - if 'tick.labelcolor' not in rc_kw: + if "tick.labelcolor" not in rc_kw: xticklabelcolor = _not_none(xticklabelcolor, xcolor) yticklabelcolor = _not_none(yticklabelcolor, ycolor) - if 'label.color' not in rc_kw: + if "label.color" not in rc_kw: xlabelcolor = _not_none(xlabelcolor, xcolor) ylabelcolor = _not_none(ylabelcolor, ycolor) # Flexible keyword args, declare defaults # NOTE: 'xtickdir' and 'ytickdir' read from 'tickdir' arguments here - xmargin = _not_none(xmargin, rc.find('axes.xmargin', context=True)) - ymargin = _not_none(ymargin, rc.find('axes.ymargin', context=True)) - xtickdir = _not_none(xtickdir, rc.find('xtick.direction', context=True)) - ytickdir = _not_none(ytickdir, rc.find('ytick.direction', context=True)) + xmargin = _not_none(xmargin, rc.find("axes.xmargin", context=True)) + ymargin = _not_none(ymargin, rc.find("axes.ymargin", context=True)) + xtickdir = _not_none(xtickdir, rc.find("xtick.direction", context=True)) + ytickdir = _not_none(ytickdir, rc.find("ytick.direction", context=True)) xlocator = _not_none(xlocator=xlocator, xticks=xticks) ylocator = _not_none(ylocator=ylocator, yticks=yticks) - xminorlocator = _not_none(xminorlocator=xminorlocator, xminorticks=xminorticks) # noqa: E501 - yminorlocator = _not_none(yminorlocator=yminorlocator, yminorticks=yminorticks) # noqa: E501 + xminorlocator = _not_none( + xminorlocator=xminorlocator, xminorticks=xminorticks + ) # noqa: E501 + yminorlocator = _not_none( + yminorlocator=yminorlocator, yminorticks=yminorticks + ) # noqa: E501 xformatter = _not_none(xformatter=xformatter, xticklabels=xticklabels) yformatter = _not_none(yformatter=yformatter, yticklabels=yticklabels) xtickminor_default = ytickminor_default = None - if isinstance(xformatter, mticker.FixedFormatter) or np.iterable(xformatter) and not isinstance(xformatter, str): # noqa: E501 + if ( + isinstance(xformatter, mticker.FixedFormatter) + or np.iterable(xformatter) + and not isinstance(xformatter, str) + ): # noqa: E501 xtickminor_default = False - if isinstance(yformatter, mticker.FixedFormatter) or np.iterable(yformatter) and not isinstance(yformatter, str): # noqa: E501 + if ( + isinstance(yformatter, mticker.FixedFormatter) + or np.iterable(yformatter) + and not isinstance(yformatter, str) + ): # noqa: E501 ytickminor_default = False - xtickminor = _not_none(xtickminor, xtickminor_default, rc.find('xtick.minor.visible', context=True)) # noqa: E501 - ytickminor = _not_none(ytickminor, ytickminor_default, rc.find('ytick.minor.visible', context=True)) # noqa: E501 - ticklabeldir = kwargs.pop('ticklabeldir', None) + xtickminor = _not_none( + xtickminor, + xtickminor_default, + rc.find("xtick.minor.visible", context=True), + ) # noqa: E501 + ytickminor = _not_none( + ytickminor, + ytickminor_default, + rc.find("ytick.minor.visible", context=True), + ) # noqa: E501 + ticklabeldir = kwargs.pop("ticklabeldir", None) xticklabeldir = _not_none(xticklabeldir, ticklabeldir) yticklabeldir = _not_none(yticklabeldir, ticklabeldir) xtickdir = _not_none(xtickdir, xticklabeldir) @@ -1021,26 +1133,26 @@ def format( # want this sometimes! Same goes for spines! xspineloc = _not_none(xloc=xloc, xspineloc=xspineloc) yspineloc = _not_none(yloc=yloc, yspineloc=yspineloc) - xside = self._get_spine_side('x', xspineloc) - yside = self._get_spine_side('y', yspineloc) - if xside is not None and xside not in ('zero', 'center', 'both'): + xside = self._get_spine_side("x", xspineloc) + yside = self._get_spine_side("y", yspineloc) + if xside is not None and xside not in ("zero", "center", "both"): xtickloc = _not_none(xtickloc, xside) - if yside is not None and yside not in ('zero', 'center', 'both'): + if yside is not None and yside not in ("zero", "center", "both"): ytickloc = _not_none(ytickloc, yside) - if xtickloc != 'both': # then infer others + if xtickloc != "both": # then infer others xticklabelloc = _not_none(xticklabelloc, xtickloc) - if xticklabelloc in ('bottom', 'top'): + if xticklabelloc in ("bottom", "top"): xlabelloc = _not_none(xlabelloc, xticklabelloc) xoffsetloc = _not_none(xoffsetloc, yticklabelloc) - if ytickloc != 'both': # then infer others + if ytickloc != "both": # then infer others yticklabelloc = _not_none(yticklabelloc, ytickloc) - if yticklabelloc in ('left', 'right'): + if yticklabelloc in ("left", "right"): ylabelloc = _not_none(ylabelloc, yticklabelloc) yoffsetloc = _not_none(yoffsetloc, yticklabelloc) - xtickloc = _not_none(xtickloc, rc._get_loc_string('x', 'xtick')) - ytickloc = _not_none(ytickloc, rc._get_loc_string('y', 'ytick')) - xspineloc = _not_none(xspineloc, rc._get_loc_string('x', 'axes.spines')) - yspineloc = _not_none(yspineloc, rc._get_loc_string('y', 'axes.spines')) + xtickloc = _not_none(xtickloc, rc._get_loc_string("x", "xtick")) + ytickloc = _not_none(ytickloc, rc._get_loc_string("y", "ytick")) + xspineloc = _not_none(xspineloc, rc._get_loc_string("x", "axes.spines")) + yspineloc = _not_none(yspineloc, rc._get_loc_string("y", "axes.spines")) # Loop over axes for ( @@ -1091,7 +1203,7 @@ def format( labelsize, labelweight, ) in zip( - ('x', 'y'), + ("x", "y"), (xmin, ymin), (xmax, ymax), (xlim, ylim), @@ -1146,40 +1258,48 @@ def format( # so critical to do it first. if scale is not None: scale = constructor.Scale(scale, **scale_kw) - getattr(self, f'set_{s}scale')(scale) + getattr(self, f"set_{s}scale")(scale) # Axis limits - self._update_limits( - s, min_=min_, max_=max_, lim=lim, reverse=reverse - ) + self._update_limits(s, min_=min_, max_=max_, lim=lim, reverse=reverse) if margin is not None: self.margins(**{s: margin}) # Axis spine settings # NOTE: This sets spine-specific color and linewidth settings. For # non-specific settings _update_background is called in Axes.format() - self._update_spines( - s, loc=spineloc, bounds=bounds - ) + self._update_spines(s, loc=spineloc, bounds=bounds) self._update_background( - s, edgecolor=color, linewidth=linewidth, - tickwidth=tickwidth, tickwidthratio=tickwidthratio, + s, + edgecolor=color, + linewidth=linewidth, + tickwidth=tickwidth, + tickwidthratio=tickwidthratio, ) # Axis tick settings self._update_locs( - s, tickloc=tickloc, ticklabelloc=ticklabelloc, - labelloc=labelloc, offsetloc=offsetloc, - ) - self._update_rotation( - s, rotation=rotation + s, + tickloc=tickloc, + ticklabelloc=ticklabelloc, + labelloc=labelloc, + offsetloc=offsetloc, ) + self._update_rotation(s, rotation=rotation) self._update_ticks( - s, grid=grid, gridminor=gridminor, - ticklen=ticklen, ticklenratio=ticklenratio, - tickdir=tickdir, labeldir=ticklabeldir, labelpad=ticklabelpad, - tickcolor=tickcolor, gridcolor=gridcolor, labelcolor=ticklabelcolor, - labelsize=ticklabelsize, labelweight=ticklabelweight, + s, + grid=grid, + gridminor=gridminor, + ticklen=ticklen, + ticklenratio=ticklenratio, + tickdir=tickdir, + labeldir=ticklabeldir, + labelpad=ticklabelpad, + tickcolor=tickcolor, + gridcolor=gridcolor, + labelcolor=ticklabelcolor, + labelsize=ticklabelsize, + labelweight=ticklabelweight, ) # Axis label settings @@ -1190,27 +1310,34 @@ def format( color=labelcolor, size=labelsize, weight=labelweight, - **label_kw + **label_kw, ) self._update_labels(s, label, **kw) # Axis locator if minorlocator is True or minorlocator is False: # must test identity warnings._warn_proplot( - f'You passed {s}minorticks={minorlocator}, but this argument ' - 'is used to specify the tick locations. If you just want to ' - f'toggle minor ticks, please use {s}tickminor={minorlocator}.' + f"You passed {s}minorticks={minorlocator}, but this argument " + "is used to specify the tick locations. If you just want to " + f"toggle minor ticks, please use {s}tickminor={minorlocator}." ) minorlocator = None self._update_locators( - s, locator, minorlocator, tickminor=tickminor, - locator_kw=locator_kw, minorlocator_kw=minorlocator_kw, + s, + locator, + minorlocator, + tickminor=tickminor, + locator_kw=locator_kw, + minorlocator_kw=minorlocator_kw, ) # Axis formatter self._update_formatter( - s, formatter, formatter_kw=formatter_kw, - tickrange=tickrange, wraprange=wraprange, + s, + formatter, + formatter_kw=formatter_kw, + tickrange=tickrange, + wraprange=wraprange, ) # Ensure ticks are within axis bounds @@ -1226,14 +1353,14 @@ def altx(self, **kwargs): """ %(axes.altx)s """ - return self._add_alt('x', **kwargs) + return self._add_alt("x", **kwargs) @docstring._snippet_manager def alty(self, **kwargs): """ %(axes.alty)s """ - return self._add_alt('y', **kwargs) + return self._add_alt("y", **kwargs) @docstring._snippet_manager def dualx(self, funcscale, **kwargs): @@ -1243,8 +1370,8 @@ def dualx(self, funcscale, **kwargs): # NOTE: Matplotlib 3.1 has a 'secondary axis' feature. For the time # being, our version is more robust (see FuncScale) and simpler, since # we do not create an entirely separate _SecondaryAxis class. - ax = self._add_alt('x', **kwargs) - ax._dual_scale('x', funcscale) + ax = self._add_alt("x", **kwargs) + ax._dual_scale("x", funcscale) return ax @docstring._snippet_manager @@ -1252,8 +1379,8 @@ def dualy(self, funcscale, **kwargs): """ %(axes.dualy)s """ - ax = self._add_alt('y', **kwargs) - ax._dual_scale('y', funcscale) + ax = self._add_alt("y", **kwargs) + ax._dual_scale("y", funcscale) return ax @docstring._snippet_manager @@ -1261,36 +1388,38 @@ def twinx(self, **kwargs): """ %(axes.twinx)s """ - return self._add_alt('y', **kwargs) + return self._add_alt("y", **kwargs) @docstring._snippet_manager def twiny(self, **kwargs): """ %(axes.twiny)s """ - return self._add_alt('x', **kwargs) + return self._add_alt("x", **kwargs) def draw(self, renderer=None, *args, **kwargs): # Perform extra post-processing steps # NOTE: In *principle* axis sharing application step goes here. But should # already be complete because auto_layout() (called by figure pre-processor) # has to run it before aligning labels. So this is harmless no-op. - self._dual_scale('x') - self._dual_scale('y') + self._dual_scale("x") + self._dual_scale("y") self._apply_axis_sharing() - self._update_rotation('x') + self._update_rotation("x") super().draw(renderer, *args, **kwargs) def get_tightbbox(self, renderer, *args, **kwargs): # Perform extra post-processing steps - self._dual_scale('x') - self._dual_scale('y') + self._dual_scale("x") + self._dual_scale("y") self._apply_axis_sharing() - self._update_rotation('x') + self._update_rotation("x") return super().get_tightbbox(renderer, *args, **kwargs) # Apply signature obfuscation after storing previous signature # NOTE: This is needed for __init__, altx, and alty -CartesianAxes._format_signatures[CartesianAxes] = inspect.signature(CartesianAxes.format) # noqa: E501 +CartesianAxes._format_signatures[CartesianAxes] = inspect.signature( + CartesianAxes.format +) # noqa: E501 CartesianAxes.format = docstring._obfuscate_kwargs(CartesianAxes.format) diff --git a/proplot/axes/geo.py b/proplot/axes/geo.py index dc98551c0..544bce902 100644 --- a/proplot/axes/geo.py +++ b/proplot/axes/geo.py @@ -33,7 +33,7 @@ except ModuleNotFoundError: Basemap = object -__all__ = ['GeoAxes'] +__all__ = ["GeoAxes"] # Format docstring @@ -176,23 +176,24 @@ labelweight : str, default: :rc:`grid.labelweight` The font weight for the gridline labels (`gridlabelweight` is also allowed). """ -docstring._snippet_manager['geo.format'] = _format_docstring +docstring._snippet_manager["geo.format"] = _format_docstring class _GeoLabel(object): """ Optionally omit overlapping check if an rc setting is disabled. """ + def check_overlapping(self, *args, **kwargs): - if rc['grid.checkoverlap']: + if rc["grid.checkoverlap"]: return super().check_overlapping(*args, **kwargs) else: return False # Add monkey patch to gridliner module -if cgridliner is not None and hasattr(cgridliner, 'Label'): # only recent versions - _cls = type('Label', (_GeoLabel, cgridliner.Label), {}) +if cgridliner is not None and hasattr(cgridliner, "Label"): # only recent versions + _cls = type("Label", (_GeoLabel, cgridliner.Label), {}) cgridliner.Label = _cls @@ -202,6 +203,7 @@ class _GeoAxis(object): longitude and latitude coordinates. Modeled after how `matplotlib.ticker._DummyAxis` and `matplotlib.ticker.TickHelper` are used to control tick locations and labels. """ + # NOTE: Due to cartopy bug (https://github.com/SciTools/cartopy/issues/1564) # we store presistent longitude and latitude locators on axes, then *call* # them whenever set_extent is called and apply *fixed* locators. @@ -215,8 +217,10 @@ def __init__(self, axes): self._interval = None self._use_dms = ( ccrs is not None - and isinstance(axes.projection, (ccrs._RectangularProjection, ccrs.Mercator)) # noqa: E501 - and _version_cartopy >= '0.18' + and isinstance( + axes.projection, (ccrs._RectangularProjection, ccrs.Mercator) + ) # noqa: E501 + and _version_cartopy >= "0.18" ) def _get_extent(self): @@ -246,7 +250,7 @@ def _pad_ticks(ticks, vmin, vmax): return ticks def get_scale(self): - return 'linear' + return "linear" def get_tick_space(self): return 9 # longstanding default of nbins=9 @@ -293,15 +297,18 @@ class _LonAxis(_GeoAxis): """ Axis with default longitude locator. """ + + axis_name = "lon" + # NOTE: Basemap accepts tick formatters with drawmeridians(fmt=Formatter()) # Try to use cartopy formatter if cartopy installed. Otherwise use # default builtin basemap formatting. def __init__(self, axes): super().__init__(axes) if self._use_dms: - locator = formatter = 'dmslon' + locator = formatter = "dmslon" else: - locator = formatter = 'deglon' + locator = formatter = "deglon" self.set_major_formatter(constructor.Formatter(formatter), default=True) self.set_major_locator(constructor.Locator(locator), default=True) self.set_minor_locator(mticker.AutoMinorLocator(), default=True) @@ -319,7 +326,7 @@ def _get_ticklocs(self, locator): ticks = np.sort(locator()) while ticks.size: if np.isclose(ticks[0] + 360, ticks[-1]): - if _version_cartopy >= '0.18' or not np.isclose(ticks[0] % 360, 0): + if _version_cartopy >= "0.18" or not np.isclose(ticks[0] % 360, 0): ticks[-1] -= eps # ensure label appears on *right* not left break elif ticks[0] + 360 < ticks[-1]: @@ -352,6 +359,9 @@ class _LatAxis(_GeoAxis): """ Axis with default latitude locator. """ + + axis_name = "lat" + def __init__(self, axes, latmax=90): # NOTE: Need to pass projection because lataxis/lonaxis are # initialized before geoaxes is initialized, because format() needs @@ -359,9 +369,9 @@ def __init__(self, axes, latmax=90): self._latmax = latmax super().__init__(axes) if self._use_dms: - locator = formatter = 'dmslat' + locator = formatter = "dmslat" else: - locator = formatter = 'deglat' + locator = formatter = "deglat" self.set_major_formatter(constructor.Formatter(formatter), default=True) self.set_major_locator(constructor.Locator(locator), default=True) self.set_minor_locator(mticker.AutoMinorLocator(), default=True) @@ -428,6 +438,7 @@ class GeoAxes(plot.PlotAxes): argument, or pass ``proj='basemap'`` with a `~mpl_toolkits.basemap.Basemap` `map_projection` keyword argument. """ + @docstring._snippet_manager def __init__(self, *args, **kwargs): """ @@ -457,7 +468,7 @@ def __init__(self, *args, **kwargs): """ super().__init__(*args, **kwargs) - def _get_lonticklocs(self, which='major'): + def _get_lonticklocs(self, which="major"): """ Retrieve longitude tick locations. """ @@ -466,18 +477,18 @@ def _get_lonticklocs(self, which='major'): # Since _axes_domain is wrong we determine tick locations ourselves with # more accurate extent tracked by _LatAxis and _LonAxis. axis = self._lonaxis - if which == 'major': + if which == "major": lines = axis.get_majorticklocs() else: lines = axis.get_minorticklocs() return lines - def _get_latticklocs(self, which='major'): + def _get_latticklocs(self, which="major"): """ Retrieve latitude tick locations. """ axis = self._lataxis - if which == 'major': + if which == "major": lines = axis.get_majorticklocs() else: lines = axis.get_minorticklocs() @@ -496,32 +507,32 @@ def _to_label_array(arg, lon=True): Convert labels argument to length-5 boolean array. """ array = arg - which = 'lon' if lon else 'lat' + which = "lon" if lon else "lat" array = np.atleast_1d(array).tolist() if len(array) == 1 and array[0] is None: array = [None] * 5 elif all(isinstance(_, str) for _ in array): strings = array # iterate over list of strings array = [False] * 5 - opts = ('left', 'right', 'bottom', 'top', 'geo') + opts = ("left", "right", "bottom", "top", "geo") for string in strings: - if string == 'all': - string = 'lrbt' - elif string == 'both': - string = 'bt' if lon else 'lr' - elif string == 'neither': - string = '' + if string == "all": + string = "lrbt" + elif string == "both": + string = "bt" if lon else "lr" + elif string == "neither": + string = "" elif string in opts: string = string[0] - if set(string) - set('lrbtg'): + if set(string) - set("lrbtg"): raise ValueError( - f'Invalid {which}label string {string!r}. Must be one of ' - + ', '.join(map(repr, (*opts, 'neither', 'both', 'all'))) + f"Invalid {which}label string {string!r}. Must be one of " + + ", ".join(map(repr, (*opts, "neither", "both", "all"))) + " or a string of single-letter characters like 'lr'." ) for char in string: - array['lrbtg'.index(char)] = True - if rc['grid.geolabels'] and any(array): + array["lrbtg".index(char)] = True + if rc["grid.geolabels"] and any(array): array[4] = True # possibly toggle geo spine labels elif not any(isinstance(_, str) for _ in array): if len(array) == 1: @@ -529,35 +540,62 @@ def _to_label_array(arg, lon=True): if len(array) == 2: array = [False, False, *array] if lon else [*array, False, False] if len(array) == 4: - b = any(array) if rc['grid.geolabels'] else False + b = any(array) if rc["grid.geolabels"] else False array.append(b) # possibly toggle geo spine labels if len(array) != 5: - raise ValueError(f'Invald boolean label array length {len(array)}.') + raise ValueError(f"Invald boolean label array length {len(array)}.") array = list(map(bool, array)) else: - raise ValueError(f'Invalid {which}label spec: {arg}.') + raise ValueError(f"Invalid {which}label spec: {arg}.") return array @docstring._snippet_manager def format( - self, *, - extent=None, round=None, - lonlim=None, latlim=None, boundinglat=None, - longrid=None, latgrid=None, longridminor=None, latgridminor=None, - latmax=None, nsteps=None, - lonlocator=None, lonlines=None, - latlocator=None, latlines=None, - lonminorlocator=None, lonminorlines=None, - latminorlocator=None, latminorlines=None, - lonlocator_kw=None, lonlines_kw=None, - latlocator_kw=None, latlines_kw=None, - lonminorlocator_kw=None, lonminorlines_kw=None, - latminorlocator_kw=None, latminorlines_kw=None, - lonformatter=None, latformatter=None, - lonformatter_kw=None, latformatter_kw=None, - labels=None, latlabels=None, lonlabels=None, rotatelabels=None, - loninline=None, latinline=None, inlinelabels=None, dms=None, - labelpad=None, labelcolor=None, labelsize=None, labelweight=None, + self, + *, + extent=None, + round=None, + lonlim=None, + latlim=None, + boundinglat=None, + longrid=None, + latgrid=None, + longridminor=None, + latgridminor=None, + latmax=None, + nsteps=None, + lonlocator=None, + lonlines=None, + latlocator=None, + latlines=None, + lonminorlocator=None, + lonminorlines=None, + latminorlocator=None, + latminorlines=None, + lonlocator_kw=None, + lonlines_kw=None, + latlocator_kw=None, + latlines_kw=None, + lonminorlocator_kw=None, + lonminorlines_kw=None, + latminorlocator_kw=None, + latminorlines_kw=None, + lonformatter=None, + latformatter=None, + lonformatter_kw=None, + latformatter_kw=None, + labels=None, + latlabels=None, + lonlabels=None, + rotatelabels=None, + loninline=None, + latinline=None, + inlinelabels=None, + dms=None, + labelpad=None, + labelcolor=None, + labelsize=None, + labelweight=None, **kwargs, ): """ @@ -585,7 +623,7 @@ def format( # drawing gridlines before basemap map boundary will call set_axes_limits() # which initializes a boundary hidden from external access. So we must call # it here. Must do this between mpl.Axes.__init__() and base.Axes.format(). - if self._name == 'basemap' and self._map_boundary is None: + if self._name == "basemap" and self._map_boundary is None: if self.projection.projection in self._proj_non_rectangular: patch = self.projection.drawmapboundary(ax=self) self._map_boundary = patch @@ -597,22 +635,22 @@ def format( rc_kw, rc_mode = _pop_rc(kwargs) lonlabels = _not_none(lonlabels, labels) latlabels = _not_none(latlabels, labels) - if '0.18' <= _version_cartopy < '0.20': + if "0.18" <= _version_cartopy < "0.20": lonlabels = _not_none(lonlabels, loninline, inlinelabels) latlabels = _not_none(latlabels, latinline, inlinelabels) - labelcolor = _not_none(labelcolor, kwargs.get('color', None)) + labelcolor = _not_none(labelcolor, kwargs.get("color", None)) if labelcolor is not None: - rc_kw['grid.labelcolor'] = labelcolor + rc_kw["grid.labelcolor"] = labelcolor if labelsize is not None: - rc_kw['grid.labelsize'] = labelsize + rc_kw["grid.labelsize"] = labelsize if labelweight is not None: - rc_kw['grid.labelweight'] = labelweight + rc_kw["grid.labelweight"] = labelweight with rc.context(rc_kw, mode=rc_mode): # Apply extent mode first # NOTE: We deprecate autoextent on _CartopyAxes with _rename_kwargs which # does not translate boolean flag. So here apply translation. if extent is not None and not isinstance(extent, str): - extent = ('globe', 'auto')[int(bool(extent))] + extent = ("globe", "auto")[int(bool(extent))] self._update_boundary(round) self._update_extent_mode(extent, boundinglat) @@ -620,14 +658,14 @@ def format( # NOTE: Cartopy 0.18 and 0.19 inline labels require any of # top, bottom, left, or right to be toggled then ignores them. # Later versions of cartopy permit both or neither labels. - labels = _not_none(labels, rc.find('grid.labels', context=True)) + labels = _not_none(labels, rc.find("grid.labels", context=True)) lonlabels = _not_none(lonlabels, labels) latlabels = _not_none(latlabels, labels) lonarray = self._to_label_array(lonlabels, lon=True) latarray = self._to_label_array(latlabels, lon=False) # Update max latitude - latmax = _not_none(latmax, rc.find('grid.latmax', context=True)) + latmax = _not_none(latmax, rc.find("grid.latmax", context=True)) if latmax is not None: self._lataxis.set_latmax(latmax) @@ -636,13 +674,17 @@ def format( latlocator = _not_none(latlocator=latlocator, latlines=latlines) if lonlocator is not None: lonlocator_kw = _not_none( - lonlocator_kw=lonlocator_kw, lonlines_kw=lonlines_kw, default={}, + lonlocator_kw=lonlocator_kw, + lonlines_kw=lonlines_kw, + default={}, ) locator = constructor.Locator(lonlocator, **lonlocator_kw) self._lonaxis.set_major_locator(locator) if latlocator is not None: latlocator_kw = _not_none( - latlocator_kw=latlocator_kw, latlines_kw=latlines_kw, default={}, + latlocator_kw=latlocator_kw, + latlines_kw=latlines_kw, + default={}, ) locator = constructor.Locator(latlocator, **latlocator_kw) self._lataxis.set_major_locator(locator) @@ -672,12 +714,18 @@ def format( self._lataxis.set_minor_locator(locator) # Update formatters - loninline = _not_none(loninline, inlinelabels, rc.find('grid.inlinelabels', context=True)) # noqa: E501 - latinline = _not_none(latinline, inlinelabels, rc.find('grid.inlinelabels', context=True)) # noqa: E501 - rotatelabels = _not_none(rotatelabels, rc.find('grid.rotatelabels', context=True)) # noqa: E501 - labelpad = _not_none(labelpad, rc.find('grid.labelpad', context=True)) - dms = _not_none(dms, rc.find('grid.dmslabels', context=True)) - nsteps = _not_none(nsteps, rc.find('grid.nsteps', context=True)) + loninline = _not_none( + loninline, inlinelabels, rc.find("grid.inlinelabels", context=True) + ) # noqa: E501 + latinline = _not_none( + latinline, inlinelabels, rc.find("grid.inlinelabels", context=True) + ) # noqa: E501 + rotatelabels = _not_none( + rotatelabels, rc.find("grid.rotatelabels", context=True) + ) # noqa: E501 + labelpad = _not_none(labelpad, rc.find("grid.labelpad", context=True)) + dms = _not_none(dms, rc.find("grid.dmslabels", context=True)) + nsteps = _not_none(nsteps, rc.find("grid.nsteps", context=True)) if lonformatter is not None: lonformatter_kw = lonformatter_kw or {} formatter = constructor.Formatter(lonformatter, **lonformatter_kw) @@ -698,13 +746,20 @@ def format( self._update_extent(lonlim=lonlim, latlim=latlim, boundinglat=boundinglat) self._update_features() self._update_major_gridlines( - longrid=longrid, latgrid=latgrid, # gridline toggles - lonarray=lonarray, latarray=latarray, # label toggles - loninline=loninline, latinline=latinline, rotatelabels=rotatelabels, - labelpad=labelpad, nsteps=nsteps, + longrid=longrid, + latgrid=latgrid, # gridline toggles + lonarray=lonarray, + latarray=latarray, # label toggles + loninline=loninline, + latinline=latinline, + rotatelabels=rotatelabels, + labelpad=labelpad, + nsteps=nsteps, ) self._update_minor_gridlines( - longrid=longridminor, latgrid=latgridminor, nsteps=nsteps, + longrid=longridminor, + latgrid=latgridminor, + nsteps=nsteps, ) # Parent format method @@ -720,7 +775,7 @@ def gridlines_major(self): and `~mpl_toolkits.basemap.Basemap.drawparallels`. This can be used for customization and debugging. """ - if self._name == 'basemap': + if self._name == "basemap": return (self._lonlines_major, self._latlines_major) else: return self._gridlines_major @@ -735,7 +790,7 @@ def gridlines_minor(self): and `~mpl_toolkits.basemap.Basemap.drawparallels`. This can be used for customization and debugging. """ - if self._name == 'basemap': + if self._name == "basemap": return (self._lonlines_minor, self._latlines_minor) else: return self._gridlines_minor @@ -752,7 +807,7 @@ def projection(self): def projection(self, map_projection): cls = self._proj_class if not isinstance(map_projection, cls): - raise ValueError(f'Projection must be a {cls} instance.') + raise ValueError(f"Projection must be a {cls} instance.") self._map_projection = map_projection @@ -760,8 +815,9 @@ class _CartopyAxes(GeoAxes, _GeoAxes): """ Axes subclass for plotting cartopy projections. """ - _name = 'cartopy' - _name_aliases = ('geo', 'geographic') # default 'geographic' axes + + _name = "cartopy" + _name_aliases = ("geo", "geographic") # default 'geographic' axes _proj_class = Projection _proj_north = ( pproj.NorthPolarStereo, @@ -773,13 +829,13 @@ class _CartopyAxes(GeoAxes, _GeoAxes): pproj.SouthPolarStereo, pproj.SouthPolarGnomonic, pproj.SouthPolarAzimuthalEquidistant, - pproj.SouthPolarLambertAzimuthalEqualArea + pproj.SouthPolarLambertAzimuthalEqualArea, ) _proj_polar = _proj_north + _proj_south # NOTE: The rename argument wrapper belongs here instead of format() because # these arguments were previously only accepted during initialization. - @warnings._rename_kwargs('0.10', circular='round', autoextent='extent') + @warnings._rename_kwargs("0.10", circular="round", autoextent="extent") def __init__(self, *args, map_projection=None, **kwargs): """ Parameters @@ -792,6 +848,7 @@ def __init__(self, *args, map_projection=None, **kwargs): # Initialize axes. Note that critical attributes like outline_patch # needed by _format_apply are added before it is called. import cartopy # noqa: F401 verify package is available + self.projection = map_projection # verify polar = isinstance(self.projection, self._proj_polar) latmax = 80 if polar else 90 # default latmax @@ -801,9 +858,14 @@ def __init__(self, *args, map_projection=None, **kwargs): self._gridlines_minor = None self._lonaxis = _LonAxis(self) self._lataxis = _LatAxis(self, latmax=latmax) - super().__init__(*args, map_projection=self.projection, **kwargs) + # 'map_projection' argument is deprecated since cartopy 0.21 and + # replaced by 'projection'. + if _version_cartopy >= "0.21": + super().__init__(*args, projection=self.projection, **kwargs) + else: + super().__init__(*args, map_projection=self.projection, **kwargs) for axis in (self.xaxis, self.yaxis): - axis.set_tick_params(which='both', size=0) # prevent extra label offset + axis.set_tick_params(which="both", size=0) # prevent extra label offset def _apply_axis_sharing(self): # noqa: U100 """ @@ -835,31 +897,34 @@ def _get_lon0(self): """ Get the central longitude. Default is ``0``. """ - return self.projection.proj4_params.get('lon_0', 0) + return self.projection.proj4_params.get("lon_0", 0) def _init_gridlines(self): """ Create monkey patched "major" and "minor" gridliners managed by proplot. """ + # Cartopy < 0.18 monkey patch. Helps filter valid coordates to lon_0 +/- 180 def _axes_domain(self, *args, **kwargs): x_range, y_range = type(self)._axes_domain(self, *args, **kwargs) - if _version_cartopy < '0.18': - lon_0 = self.axes.projection.proj4_params.get('lon_0', 0) + if _version_cartopy < "0.18": + lon_0 = self.axes.projection.proj4_params.get("lon_0", 0) x_range = np.asarray(x_range) + lon_0 return x_range, y_range + # Cartopy >= 0.18 monkey patch. Fixes issue where cartopy draws an overlapping # dateline gridline (e.g. polar maps). See the nx -= 1 line in _draw_gridliner def _draw_gridliner(self, *args, **kwargs): # noqa: E306 result = type(self)._draw_gridliner(self, *args, **kwargs) - if _version_cartopy >= '0.18': + if _version_cartopy >= "0.18": lon_lim, _ = self._axes_domain() if abs(np.diff(lon_lim)) == abs(np.diff(self.crs.x_limits)): for collection in self.xline_artists: - if not getattr(collection, '_cartopy_fix', False): + if not getattr(collection, "_cartopy_fix", False): collection.get_paths().pop(-1) collection._cartopy_fix = True return result + # Return the gridliner with monkey patch gl = self.gridlines(crs=ccrs.PlateCarree()) gl._axes_domain = _axes_domain.__get__(gl) @@ -875,16 +940,16 @@ def _toggle_gridliner_labels( """ Toggle gridliner labels across different cartopy versions. """ - if _version_cartopy >= '0.18': - left_labels = 'left_labels' - right_labels = 'right_labels' - bottom_labels = 'bottom_labels' - top_labels = 'top_labels' + if _version_cartopy >= "0.18": + left_labels = "left_labels" + right_labels = "right_labels" + bottom_labels = "bottom_labels" + top_labels = "top_labels" else: # cartopy < 0.18 - left_labels = 'ylabels_left' - right_labels = 'ylabels_right' - bottom_labels = 'xlabels_bottom' - top_labels = 'xlabels_top' + left_labels = "ylabels_left" + right_labels = "ylabels_right" + bottom_labels = "xlabels_bottom" + top_labels = "xlabels_top" if left is not None: setattr(gl, left_labels, left) if right is not None: @@ -894,7 +959,7 @@ def _toggle_gridliner_labels( if top is not None: setattr(gl, top_labels, top) if geo is not None: # only cartopy 0.20 supported but harmless - setattr(gl, 'geo_labels', geo) + setattr(gl, "geo_labels", geo) def _update_background(self, **kwargs): """ @@ -906,11 +971,11 @@ def _update_background(self, **kwargs): # ignored and using spines vs. patch makes no difference. # NOTE: outline_patch is redundant, use background_patch instead kw_face, kw_edge = rc._get_background_props(native=False, **kwargs) - kw_face['linewidth'] = 0 - kw_edge['facecolor'] = 'none' - if _version_cartopy >= '0.18': + kw_face["linewidth"] = 0 + kw_edge["facecolor"] = "none" + if _version_cartopy >= "0.18": self.patch.update(kw_face) - self.spines['geo'].update(kw_edge) + self.spines["geo"].update(kw_edge) else: self.background_patch.update(kw_face) self.outline_patch.update(kw_edge) @@ -919,17 +984,17 @@ def _update_boundary(self, round=None): """ Update the map boundary path. """ - round = _not_none(round, rc.find('geo.round', context=True)) + round = _not_none(round, rc.find("geo.round", context=True)) if round is None or not isinstance(self.projection, self._proj_polar): pass elif round: self._is_round = True self.set_boundary(self._get_circle_path(), transform=self.transAxes) elif not round and self._is_round: - if hasattr(self, '_boundary'): + if hasattr(self, "_boundary"): self._boundary() else: - warnings._warn_proplot('Failed to reset round map boundary.') + warnings._warn_proplot("Failed to reset round map boundary.") def _update_extent_mode(self, extent=None, boundinglat=None): """ @@ -940,10 +1005,10 @@ def _update_extent_mode(self, extent=None, boundinglat=None): # NOTE: For some reason initial call to _set_view_intervals may change the # default boundary with extent='auto'. Try this in a robinson projection: # ax.contour(np.linspace(-90, 180, N), np.linspace(0, 90, N), np.zeros(N, N)) - extent = _not_none(extent, rc.find('geo.extent', context=True)) + extent = _not_none(extent, rc.find("geo.extent", context=True)) if extent is None: return - if extent not in ('globe', 'auto'): + if extent not in ("globe", "auto"): raise ValueError( f"Invalid extent mode {extent!r}. Must be 'auto' or 'globe'." ) @@ -959,7 +1024,7 @@ def _update_extent_mode(self, extent=None, boundinglat=None): default_boundinglat = 0 boundinglat = _not_none(boundinglat, default_boundinglat) self._update_extent(boundinglat=boundinglat) - if extent == 'auto': + if extent == "auto": # NOTE: This will work even if applied after plotting stuff # and fixing the limits. Very easy to toggle on and off. self.set_autoscalex_on(True) @@ -985,7 +1050,7 @@ def _update_extent(self, lonlim=None, latlim=None, boundinglat=None): if any(_ is not None for _ in (*lonlim, *latlim)): warnings._warn_proplot( f'{proj!r} extent is controlled by "boundinglat", ' - f'ignoring lonlim={lonlim!r} and latlim={latlim!r}.' + f"ignoring lonlim={lonlim!r} and latlim={latlim!r}." ) if boundinglat is not None and boundinglat != self._boundinglat: lat0 = 90 if north else -90 @@ -999,7 +1064,7 @@ def _update_extent(self, lonlim=None, latlim=None, boundinglat=None): if boundinglat is not None: warnings._warn_proplot( f'{proj!r} extent is controlled by "lonlim" and "latlim", ' - f'ignoring boundinglat={boundinglat!r}.' + f"ignoring boundinglat={boundinglat!r}." ) if any(_ is not None for _ in (*lonlim, *latlim)): lonlim = list(lonlim) @@ -1024,18 +1089,19 @@ def _update_features(self): # lo res versions. Use NaturalEarthFeature instead. # WARNING: Seems cartopy features cannot be updated! Updating _kwargs # attribute does *nothing*. - reso = rc['reso'] # resolution cannot be changed after feature created + reso = rc["reso"] # resolution cannot be changed after feature created try: reso = constructor.RESOS_CARTOPY[reso] except KeyError: raise ValueError( - f'Invalid resolution {reso!r}. Options are: ' - + ', '.join(map(repr, constructor.RESOS_CARTOPY)) + '.' + f"Invalid resolution {reso!r}. Options are: " + + ", ".join(map(repr, constructor.RESOS_CARTOPY)) + + "." ) for name, args in constructor.FEATURES_CARTOPY.items(): # Draw feature or toggle feature off b = rc.find(name, context=True) - attr = f'_{name}_feature' + attr = f"_{name}_feature" feat = getattr(self, attr, None) drawn = feat is not None # if exists, apply *updated* settings if b is not None: @@ -1046,26 +1112,35 @@ def _update_features(self): if not drawn: feat = cfeature.NaturalEarthFeature(*args, reso) feat = self.add_feature(feat) # convert to FeatureArtist + setattr(self, attr, feat) # Update artist attributes (FeatureArtist._kwargs used back to v0.5). # For 'lines', need to specify edgecolor and facecolor # See: https://github.com/SciTools/cartopy/issues/803 if feat is not None: kw = rc.category(name, context=drawn) - if name in ('coast', 'rivers', 'borders', 'innerborders'): - kw.update({'edgecolor': kw.pop('color'), 'facecolor': 'none'}) + if name in ("coast", "rivers", "borders", "innerborders"): + if "color" in kw: + kw.update({"edgecolor": kw.pop("color"), "facecolor": "none"}) else: - kw.update({'linewidth': 0}) - if 'zorder' in kw: + kw.update({"linewidth": 0}) + if "zorder" in kw: # NOTE: Necessary to update zorder directly because _kwargs # attributes are not applied until draw()... at which point # matplotlib is drawing in the order based on the *old* zorder. - feat.set_zorder(kw['zorder']) - if hasattr(feat, '_kwargs'): + feat.set_zorder(kw["zorder"]) + if hasattr(feat, "_kwargs"): feat._kwargs.update(kw) + if _version_cartopy >= "0.23": + feat.set(**feat._kwargs) def _update_gridlines( - self, gl, which='major', longrid=None, latgrid=None, nsteps=None, + self, + gl, + which="major", + longrid=None, + latgrid=None, + nsteps=None, ): """ Update gridliner object with axis locators, and toggle gridlines on and off. @@ -1084,27 +1159,32 @@ def _update_gridlines( if nsteps is not None: gl.n_steps = nsteps latmax = self._lataxis.get_latmax() - if _version_cartopy >= '0.19': + if _version_cartopy >= "0.19": gl.ylim = (-latmax, latmax) - longrid = rc._get_gridline_bool(longrid, axis='x', which=which, native=False) + longrid = rc._get_gridline_bool(longrid, axis="x", which=which, native=False) if longrid is not None: gl.xlines = longrid - latgrid = rc._get_gridline_bool(latgrid, axis='y', which=which, native=False) + latgrid = rc._get_gridline_bool(latgrid, axis="y", which=which, native=False) if latgrid is not None: gl.ylines = latgrid lonlines = self._get_lonticklocs(which=which) latlines = self._get_latticklocs(which=which) - if _version_cartopy >= '0.18': # see lukelbd/proplot#208 + if _version_cartopy >= "0.18": # see lukelbd/proplot#208 lonlines = (np.asarray(lonlines) + 180) % 360 - 180 # only for cartopy gl.xlocator = mticker.FixedLocator(lonlines) gl.ylocator = mticker.FixedLocator(latlines) def _update_major_gridlines( self, - longrid=None, latgrid=None, - lonarray=None, latarray=None, - loninline=None, latinline=None, labelpad=None, - rotatelabels=None, nsteps=None, + longrid=None, + latgrid=None, + lonarray=None, + latarray=None, + loninline=None, + latinline=None, + labelpad=None, + rotatelabels=None, + nsteps=None, ): """ Update major gridlines. @@ -1114,7 +1194,11 @@ def _update_major_gridlines( if gl is None: gl = self._gridlines_major = self._init_gridlines() self._update_gridlines( - gl, which='major', longrid=longrid, latgrid=latgrid, nsteps=nsteps, + gl, + which="major", + longrid=longrid, + latgrid=latgrid, + nsteps=nsteps, ) gl.xformatter = self._lonaxis.get_major_formatter() gl.yformatter = self._lataxis.get_major_formatter() @@ -1136,33 +1220,40 @@ def _update_major_gridlines( gl.rotate_labels = bool(rotatelabels) # ignored in cartopy < 0.18 if latinline is not None or loninline is not None: lon, lat = loninline, latinline - b = True if lon and lat else 'x' if lon else 'y' if lat else None + b = True if lon and lat else "x" if lon else "y" if lat else None gl.inline_labels = b # ignored in cartopy < 0.20 # Gridline label toggling # Issue warning instead of error! - if ( - _version_cartopy < '0.18' - and not isinstance(self.projection, (ccrs.Mercator, ccrs.PlateCarree)) + if _version_cartopy < "0.18" and not isinstance( + self.projection, (ccrs.Mercator, ccrs.PlateCarree) ): if any(latarray): warnings._warn_proplot( - 'Cannot add gridline labels to cartopy ' - f'{type(self.projection).__name__} projection.' + "Cannot add gridline labels to cartopy " + f"{type(self.projection).__name__} projection." ) latarray = [False] * 5 if any(lonarray): warnings._warn_proplot( - 'Cannot add gridline labels to cartopy ' - f'{type(self.projection).__name__} projection.' + "Cannot add gridline labels to cartopy " + f"{type(self.projection).__name__} projection." ) lonarray = [False] * 5 array = [ - True if lon and lat - else 'x' if lon - else 'y' if lat - else False if lon is not None or lon is not None - else None + ( + True + if lon and lat + else ( + "x" + if lon + else ( + "y" + if lat + else False if lon is not None or lon is not None else None + ) + ) + ) for lon, lat in zip(lonarray, latarray) ] self._toggle_gridliner_labels(gl, *array[:2], *array[2:4], array[4]) @@ -1175,7 +1266,11 @@ def _update_minor_gridlines(self, longrid=None, latgrid=None, nsteps=None): if gl is None: gl = self._gridlines_minor = self._init_gridlines() self._update_gridlines( - gl, which='minor', longrid=longrid, latgrid=latgrid, nsteps=nsteps, + gl, + which="minor", + longrid=longrid, + latgrid=latgrid, + nsteps=nsteps, ) def get_extent(self, crs=None): @@ -1200,11 +1295,10 @@ def get_tightbbox(self, renderer, *args, **kwargs): self.autoscale_view() # Adjust location - if _version_cartopy >= '0.18': + if _version_cartopy >= "0.18": self.patch._adjust_location() # this does the below steps - elif ( - getattr(self.background_patch, 'reclip', None) - and hasattr(self.background_patch, 'orig_path') + elif getattr(self.background_patch, "reclip", None) and hasattr( + self.background_patch, "orig_path" ): clipped_path = self.background_patch.orig_path.clip_to_bbox(self.viewLim) self.outline_patch._path = clipped_path @@ -1212,14 +1306,21 @@ def get_tightbbox(self, renderer, *args, **kwargs): # Apply aspect self.apply_aspect() - for gl in self._gridliners: - if _version_cartopy >= '0.18': + if _version_cartopy >= "0.23": + gridliners = [ + a for a in self.artists if isinstance(a, cgridliner.Gridliner) + ] + else: + gridliners = self._gridliners + + for gl in gridliners: + if _version_cartopy >= "0.18": gl._draw_gridliner(renderer=renderer) else: gl._draw_gridliner(background_patch=self.background_patch) # Remove gridliners - if _version_cartopy < '0.18': + if _version_cartopy < "0.18": self._gridliners = [] return super().get_tightbbox(renderer, *args, **kwargs) @@ -1241,10 +1342,10 @@ def set_extent(self, extent, crs=None): self._set_view_intervals(extent) with rc.context(mode=2): # do not reset gridline properties! if self._gridlines_major is not None: - self._update_gridlines(self._gridlines_major, which='major') + self._update_gridlines(self._gridlines_major, which="major") if self._gridlines_minor is not None: - self._update_gridlines(self._gridlines_minor, which='minor') - if _version_cartopy < '0.18': + self._update_gridlines(self._gridlines_minor, which="minor") + if _version_cartopy < "0.18": clipped_path = self.outline_patch.orig_path.clip_to_bbox(self.viewLim) self.outline_patch._path = clipped_path self.background_patch._path = clipped_path @@ -1261,16 +1362,24 @@ class _BasemapAxes(GeoAxes): """ Axes subclass for plotting basemap projections. """ - _name = 'basemap' + + _name = "basemap" _proj_class = Basemap - _proj_north = ('npaeqd', 'nplaea', 'npstere') - _proj_south = ('spaeqd', 'splaea', 'spstere') + _proj_north = ("npaeqd", "nplaea", "npstere") + _proj_south = ("spaeqd", "splaea", "spstere") _proj_polar = _proj_north + _proj_south _proj_non_rectangular = _proj_polar + ( # do not use axes spines as boundaries - 'ortho', 'geos', 'nsper', - 'moll', 'hammer', 'robin', - 'eck4', 'kav7', 'mbtfpq', - 'sinu', 'vandg', + "ortho", + "geos", + "nsper", + "moll", + "hammer", + "robin", + "eck4", + "kav7", + "mbtfpq", + "sinu", + "vandg", ) def __init__(self, *args, map_projection=None, **kwargs): @@ -1289,17 +1398,18 @@ def __init__(self, *args, map_projection=None, **kwargs): # twice with updated proj kwargs to modify map bounds after creation # and python immmediately crashes. Do not try again. import mpl_toolkits.basemap # noqa: F401 verify package is available + self.projection = copy.copy(map_projection) # verify lon0 = self._get_lon0() if self.projection.projection in self._proj_polar: latmax = 80 # default latmax for gridlines extent = [-180 + lon0, 180 + lon0] - bound = getattr(self.projection, 'boundinglat', 0) + bound = getattr(self.projection, "boundinglat", 0) north = self.projection.projection in self._proj_north extent.extend([bound, 90] if north else [-90, bound]) else: latmax = 90 - attrs = ('lonmin', 'lonmax', 'latmin', 'latmax') + attrs = ("lonmin", "lonmax", "latmin", "latmax") extent = [getattr(self.projection, attr, None) for attr in attrs] if any(_ is None for _ in extent): extent = [180 - lon0, 180 + lon0, -90, 90] # fallback @@ -1322,7 +1432,7 @@ def _get_lon0(self): """ Get the central longitude. """ - return getattr(self.projection, 'projparams', {}).get('lon_0', 0) + return getattr(self.projection, "projparams", {}).get("lon_0", 0) @staticmethod def _iter_gridlines(dict_): @@ -1343,14 +1453,14 @@ def _update_background(self, **kwargs): # WARNING: Map boundary must be drawn before all other tasks. See __init__. # WARNING: With clipping on boundary lines are clipped by the axes bbox. if self.projection.projection in self._proj_non_rectangular: - self.patch.set_facecolor('none') # make sure main patch is hidden + self.patch.set_facecolor("none") # make sure main patch is hidden kw_face, kw_edge = rc._get_background_props(native=False, **kwargs) - kw = {**kw_face, **kw_edge, 'rasterized': False, 'clip_on': False} + kw = {**kw_face, **kw_edge, "rasterized": False, "clip_on": False} self._map_boundary.update(kw) # Rectangular projections else: kw_face, kw_edge = rc._get_background_props(native=False, **kwargs) - self.patch.update({**kw_face, 'edgecolor': 'none'}) + self.patch.update({**kw_face, "edgecolor": "none"}) for spine in self.spines.values(): spine.update(kw_edge) @@ -1363,7 +1473,7 @@ def _update_boundary(self, round=None): return else: warnings._warn_proplot( - f'Got round={round!r}, but you cannot change the bounds of a polar ' + f"Got round={round!r}, but you cannot change the bounds of a polar " "basemap projection after creating it. Please pass 'round' to pplt.Proj " # noqa: E501 "instead (e.g. using the pplt.subplots() dictionary keyword 'proj_kw')." ) @@ -1375,14 +1485,14 @@ def _update_extent_mode(self, extent=None, boundinglat=None): # noqa: U100 # NOTE: Unlike the cartopy method we do not look up the rc setting here. if extent is None: return - if extent not in ('globe', 'auto'): + if extent not in ("globe", "auto"): raise ValueError( f"Invalid extent mode {extent!r}. Must be 'auto' or 'globe'." ) - if extent == 'auto': + if extent == "auto": warnings._warn_proplot( - f'Got extent={extent!r}, but you cannot use auto extent mode ' - 'in basemap projections. Please consider switching to cartopy.' + f"Got extent={extent!r}, but you cannot use auto extent mode " + "in basemap projections. Please consider switching to cartopy." ) def _update_extent(self, lonlim=None, latlim=None, boundinglat=None): @@ -1393,9 +1503,9 @@ def _update_extent(self, lonlim=None, latlim=None, boundinglat=None): latlim = _not_none(latlim, (None, None)) if boundinglat is not None or any(_ is not None for _ in (*lonlim, *latlim)): warnings._warn_proplot( - f'Got lonlim={lonlim!r}, latlim={latlim!r}, boundinglat={boundinglat!r}' + f"Got lonlim={lonlim!r}, latlim={latlim!r}, boundinglat={boundinglat!r}" ', but you cannot "zoom into" a basemap projection after creating it. ' - 'Please pass any of the following keyword arguments to pplt.Proj ' + "Please pass any of the following keyword arguments to pplt.Proj " "instead (e.g. using the pplt.subplots() dictionary keyword 'proj_kw'):" "'boundinglat', 'lonlim', 'latlim', 'llcrnrlon', 'llcrnrlat', " "'urcrnrlon', 'urcrnrlat', 'llcrnrx', 'llcrnry', 'urcrnrx', 'urcrnry', " @@ -1411,7 +1521,7 @@ def _update_features(self): for name, method in constructor.FEATURES_BASEMAP.items(): # Draw feature or toggle on and off b = rc.find(name, context=True) - attr = f'_{name}_feature' + attr = f"_{name}_feature" feat = getattr(self, attr, None) drawn = feat is not None # if exists, apply *updated* settings if b is not None: @@ -1433,27 +1543,32 @@ def _update_features(self): obj.update(kw) def _update_gridlines( - self, which='major', longrid=None, latgrid=None, lonarray=None, latarray=None, + self, + which="major", + longrid=None, + latgrid=None, + lonarray=None, + latarray=None, ): """ Apply changes to the basemap axes. """ latmax = self._lataxis.get_latmax() for axis, name, grid, array, method in zip( - ('x', 'y'), - ('lon', 'lat'), + ("x", "y"), + ("lon", "lat"), (longrid, latgrid), (lonarray, latarray), - ('drawmeridians', 'drawparallels'), + ("drawmeridians", "drawparallels"), ): # Correct lonarray and latarray label toggles by changing from lrbt to lrtb. # Then update the cahced toggle array. This lets us change gridline locs # while preserving the label toggle setting from a previous format() call. grid = rc._get_gridline_bool(grid, axis=axis, which=which, native=False) - axis = getattr(self, f'_{name}axis') + axis = getattr(self, f"_{name}axis") if len(array) == 5: # should be always array = array[:4] - bools = 4 * [False] if which == 'major' else getattr(self, f'_{name}array') + bools = 4 * [False] if which == "major" else getattr(self, f"_{name}array") array = [*array[:2], *array[2:4][::-1]] # flip lrbt to lrtb and skip geo for i, b in enumerate(array): if b is not None: @@ -1461,16 +1576,16 @@ def _update_gridlines( # Get gridlines # NOTE: This may re-apply existing gridlines. - lines = list(getattr(self, f'_get_{name}ticklocs')(which=which)) - if name == 'lon' and np.isclose(lines[0] + 360, lines[-1]): + lines = list(getattr(self, f"_get_{name}ticklocs")(which=which)) + if name == "lon" and np.isclose(lines[0] + 360, lines[-1]): lines = lines[:-1] # prevent double labels # Figure out whether we have to redraw meridians/parallels # NOTE: Always update minor gridlines if major locator also changed - attr = f'_{name}lines_{which}' + attr = f"_{name}lines_{which}" objs = getattr(self, attr) # dictionary of previous objects - attrs = ['isDefault_majloc'] # always check this one - attrs.append('isDefault_majfmt' if which == 'major' else 'isDefault_minloc') + attrs = ["isDefault_majloc"] # always check this one + attrs.append("isDefault_majfmt" if which == "major" else "isDefault_minloc") rebuild = lines and ( not objs or any(_ is not None for _ in array) # user-input or initial toggles @@ -1485,7 +1600,7 @@ def _update_gridlines( kwdraw = {} formatter = axis.get_major_formatter() if formatter is not None: # use functional formatter - kwdraw['fmt'] = formatter + kwdraw["fmt"] = formatter for obj in self._iter_gridlines(objs): obj.set_visible(False) objs = getattr(self.projection, method)( @@ -1510,16 +1625,26 @@ def _update_gridlines( def _update_major_gridlines( self, - longrid=None, latgrid=None, lonarray=None, latarray=None, - loninline=None, latinline=None, rotatelabels=None, labelpad=None, nsteps=None, + longrid=None, + latgrid=None, + lonarray=None, + latarray=None, + loninline=None, + latinline=None, + rotatelabels=None, + labelpad=None, + nsteps=None, ): """ Update major gridlines. """ loninline, latinline, labelpad, rotatelabels, nsteps # avoid U100 error self._update_gridlines( - which='major', - longrid=longrid, latgrid=latgrid, lonarray=lonarray, latarray=latarray, + which="major", + longrid=longrid, + latgrid=latgrid, + lonarray=lonarray, + latarray=latarray, ) def _update_minor_gridlines(self, longrid=None, latgrid=None, nsteps=None): @@ -1530,8 +1655,11 @@ def _update_minor_gridlines(self, longrid=None, latgrid=None, nsteps=None): nsteps # avoid U100 error array = [None] * 4 # NOTE: must be None not False (see _update_gridlines) self._update_gridlines( - which='minor', - longrid=longrid, latgrid=latgrid, lonarray=array, latarray=array, + which="minor", + longrid=longrid, + latgrid=latgrid, + lonarray=array, + latarray=array, ) # Set isDefault_majloc, etc. to True for both axes # NOTE: This cannot be done inside _update_gridlines or minor gridlines diff --git a/proplot/axes/plot.py b/proplot/axes/plot.py index 379641cce..8f14e8478 100644 --- a/proplot/axes/plot.py +++ b/proplot/axes/plot.py @@ -8,7 +8,8 @@ import itertools import re import sys -from numbers import Integral +from numbers import Integral, Number +from typing import Any, Iterable import matplotlib.artist as martist import matplotlib.axes as maxes @@ -47,7 +48,7 @@ except ModuleNotFoundError: PlateCarree = object -__all__ = ['PlotAxes'] +__all__ = ["PlotAxes"] # Constants @@ -102,12 +103,20 @@ {zvar} coordinates are `pint.Quantity`, pass the magnitude to the plotting command. A `pint.Quantity` embedded in an `xarray.DataArray` is also supported. """ -docstring._snippet_manager['plot.args_1d_y'] = _args_1d_docstring.format(x='x', y='y') -docstring._snippet_manager['plot.args_1d_x'] = _args_1d_docstring.format(x='y', y='x') -docstring._snippet_manager['plot.args_1d_multiy'] = _args_1d_multi_docstring.format(x='x', y='y') # noqa: E501 -docstring._snippet_manager['plot.args_1d_multix'] = _args_1d_multi_docstring.format(x='y', y='x') # noqa: E501 -docstring._snippet_manager['plot.args_2d'] = _args_2d_docstring.format(z='z', zvar='`z`') # noqa: E501 -docstring._snippet_manager['plot.args_2d_flow'] = _args_2d_docstring.format(z='u, v', zvar='`u` and `v`') # noqa: E501 +docstring._snippet_manager["plot.args_1d_y"] = _args_1d_docstring.format(x="x", y="y") +docstring._snippet_manager["plot.args_1d_x"] = _args_1d_docstring.format(x="y", y="x") +docstring._snippet_manager["plot.args_1d_multiy"] = _args_1d_multi_docstring.format( + x="x", y="y" +) # noqa: E501 +docstring._snippet_manager["plot.args_1d_multix"] = _args_1d_multi_docstring.format( + x="y", y="x" +) # noqa: E501 +docstring._snippet_manager["plot.args_2d"] = _args_2d_docstring.format( + z="z", zvar="`z`" +) # noqa: E501 +docstring._snippet_manager["plot.args_2d_flow"] = _args_2d_docstring.format( + z="u, v", zvar="`u` and `v`" +) # noqa: E501 # Shared docstrings @@ -147,8 +156,8 @@ the map edges. For example, if the central longitude is 90\N{DEGREE SIGN}, the data is shifted so that it spans -90\N{DEGREE SIGN} to 270\N{DEGREE SIGN}. """ -docstring._snippet_manager['plot.args_1d_shared'] = _args_1d_shared_docstring -docstring._snippet_manager['plot.args_2d_shared'] = _args_2d_shared_docstring +docstring._snippet_manager["plot.args_1d_shared"] = _args_1d_shared_docstring +docstring._snippet_manager["plot.args_2d_shared"] = _args_2d_shared_docstring # Auto colorbar and legend docstring @@ -171,7 +180,7 @@ legend_kw : dict-like, optional Extra keyword args for the call to `~proplot.axes.Axes.legend`. """ -docstring._snippet_manager['plot.guide'] = _guide_docstring +docstring._snippet_manager["plot.guide"] = _guide_docstring # Misc shared 1D plotting docstrings @@ -266,11 +275,11 @@ a *custom* label, use e.g. ``shadelabel='label'``. Otherwise, the shading is drawn underneath the line and/or marker in the legend entry. """ -docstring._snippet_manager['plot.inbounds'] = _inbounds_docstring -docstring._snippet_manager['plot.error_means_y'] = _error_means_docstring.format(y='y') -docstring._snippet_manager['plot.error_means_x'] = _error_means_docstring.format(y='x') -docstring._snippet_manager['plot.error_bars'] = _error_bars_docstring -docstring._snippet_manager['plot.error_shading'] = _error_shading_docstring +docstring._snippet_manager["plot.inbounds"] = _inbounds_docstring +docstring._snippet_manager["plot.error_means_y"] = _error_means_docstring.format(y="y") +docstring._snippet_manager["plot.error_means_x"] = _error_means_docstring.format(y="x") +docstring._snippet_manager["plot.error_bars"] = _error_bars_docstring +docstring._snippet_manager["plot.error_shading"] = _error_shading_docstring # Color docstrings @@ -326,8 +335,8 @@ The `diverging` option also applies `~proplot.colors.DivergingNorm` as the default continuous normalizer. """ -docstring._snippet_manager['plot.cycle'] = _cycle_docstring -docstring._snippet_manager['plot.cmap_norm'] = _cmap_norm_docstring +docstring._snippet_manager["plot.cycle"] = _cycle_docstring +docstring._snippet_manager["plot.cmap_norm"] = _cmap_norm_docstring # Levels docstrings @@ -389,9 +398,9 @@ If ``True``, ``0`` is removed from the level list. This is mainly useful for single-color `~matplotlib.axes.Axes.contour` plots. """ -docstring._snippet_manager['plot.vmin_vmax'] = _vmin_vmax_docstring -docstring._snippet_manager['plot.levels_manual'] = _manual_levels_docstring -docstring._snippet_manager['plot.levels_auto'] = _auto_levels_docstring +docstring._snippet_manager["plot.vmin_vmax"] = _vmin_vmax_docstring +docstring._snippet_manager["plot.levels_manual"] = _manual_levels_docstring +docstring._snippet_manager["plot.levels_auto"] = _auto_levels_docstring # Labels docstrings @@ -430,9 +439,9 @@ The maximum number of decimal places for number labels generated with the default formatter `~proplot.ticker.Simpleformatter`. """ -docstring._snippet_manager['plot.label'] = _label_docstring -docstring._snippet_manager['plot.labels_1d'] = _labels_1d_docstring -docstring._snippet_manager['plot.labels_2d'] = _labels_2d_docstring +docstring._snippet_manager["plot.label"] = _label_docstring +docstring._snippet_manager["plot.labels_1d"] = _labels_1d_docstring +docstring._snippet_manager["plot.labels_2d"] = _labels_2d_docstring # Negative-positive colors @@ -445,14 +454,14 @@ Colors to use for the negative and positive {objects}. Ignored if `negpos` is ``False``. """ -docstring._snippet_manager['plot.negpos_fill'] = _negpos_docstring.format( - objects='patches', neg='y2 < y1', pos='y2 >= y1' +docstring._snippet_manager["plot.negpos_fill"] = _negpos_docstring.format( + objects="patches", neg="y2 < y1", pos="y2 >= y1" ) -docstring._snippet_manager['plot.negpos_lines'] = _negpos_docstring.format( - objects='lines', neg='ymax < ymin', pos='ymax >= ymin' +docstring._snippet_manager["plot.negpos_lines"] = _negpos_docstring.format( + objects="lines", neg="ymax < ymin", pos="ymax >= ymin" ) -docstring._snippet_manager['plot.negpos_bar'] = _negpos_docstring.format( - objects='bars', neg='height < 0', pos='height >= 0' +docstring._snippet_manager["plot.negpos_bar"] = _negpos_docstring.format( + objects="bars", neg="height < 0", pos="height >= 0" ) @@ -484,8 +493,8 @@ PlotAxes.plotx matplotlib.axes.Axes.plot """ -docstring._snippet_manager['plot.plot'] = _plot_docstring.format(y='y') -docstring._snippet_manager['plot.plotx'] = _plot_docstring.format(y='x') +docstring._snippet_manager["plot.plot"] = _plot_docstring.format(y="y") +docstring._snippet_manager["plot.plotx"] = _plot_docstring.format(y="x") # Step docstring @@ -514,8 +523,8 @@ PlotAxes.stepx matplotlib.axes.Axes.step """ -docstring._snippet_manager['plot.step'] = _step_docstring.format(y='y') -docstring._snippet_manager['plot.stepx'] = _step_docstring.format(y='x') +docstring._snippet_manager["plot.step"] = _step_docstring.format(y="y") +docstring._snippet_manager["plot.stepx"] = _step_docstring.format(y="x") # Stem docstring @@ -535,8 +544,8 @@ **kwargs Passed to `~matplotlib.axes.Axes.stem`. """ -docstring._snippet_manager['plot.stem'] = _stem_docstring.format(y='x') -docstring._snippet_manager['plot.stemx'] = _stem_docstring.format(y='x') +docstring._snippet_manager["plot.stem"] = _stem_docstring.format(y="x") +docstring._snippet_manager["plot.stemx"] = _stem_docstring.format(y="x") # Lines docstrings @@ -569,11 +578,11 @@ matplotlib.axes.Axes.vlines matplotlib.axes.Axes.hlines """ -docstring._snippet_manager['plot.vlines'] = _lines_docstring.format( - y='y', prefix='v', orientation='vertical' +docstring._snippet_manager["plot.vlines"] = _lines_docstring.format( + y="y", prefix="v", orientation="vertical" ) -docstring._snippet_manager['plot.hlines'] = _lines_docstring.format( - y='x', prefix='h', orientation='horizontal' +docstring._snippet_manager["plot.hlines"] = _lines_docstring.format( + y="x", prefix="h", orientation="horizontal" ) @@ -623,7 +632,7 @@ PlotAxes.plotx matplotlib.collections.LineCollection """ -docstring._snippet_manager['plot.parametric'] = _parametric_docstring +docstring._snippet_manager["plot.parametric"] = _parametric_docstring # Scatter function docstring @@ -686,8 +695,8 @@ PlotAxes.scatterx matplotlib.axes.Axes.scatter """ -docstring._snippet_manager['plot.scatter'] = _scatter_docstring.format(y='y') -docstring._snippet_manager['plot.scatterx'] = _scatter_docstring.format(y='x') +docstring._snippet_manager["plot.scatter"] = _scatter_docstring.format(y="y") +docstring._snippet_manager["plot.scatterx"] = _scatter_docstring.format(y="x") # Bar function docstring @@ -733,11 +742,11 @@ matplotlib.axes.Axes.bar matplotlib.axes.Axes.barh """ -docstring._snippet_manager['plot.bar'] = _bar_docstring.format( - x='x', y='y', bottom='bottom', suffix='' +docstring._snippet_manager["plot.bar"] = _bar_docstring.format( + x="x", y="y", bottom="bottom", suffix="" ) -docstring._snippet_manager['plot.barh'] = _bar_docstring.format( - x='y', y='x', bottom='left', suffix='h' +docstring._snippet_manager["plot.barh"] = _bar_docstring.format( + x="y", y="x", bottom="left", suffix="h" ) @@ -778,11 +787,11 @@ matplotlib.axes.Axes.fill_between matplotlib.axes.Axes.fill_betweenx """ -docstring._snippet_manager['plot.fill_between'] = _fill_docstring.format( - x='x', y='y', suffix='' +docstring._snippet_manager["plot.fill_between"] = _fill_docstring.format( + x="x", y="y", suffix="" ) -docstring._snippet_manager['plot.fill_betweenx'] = _fill_docstring.format( - x='y', y='x', suffix='x' +docstring._snippet_manager["plot.fill_betweenx"] = _fill_docstring.format( + x="y", y="x", suffix="x" ) @@ -842,11 +851,11 @@ PlotAxes.boxploth matplotlib.axes.Axes.boxplot """ -docstring._snippet_manager['plot.boxplot'] = _boxplot_docstring.format( - y='y', orientation='vertical' +docstring._snippet_manager["plot.boxplot"] = _boxplot_docstring.format( + y="y", orientation="vertical" ) -docstring._snippet_manager['plot.boxploth'] = _boxplot_docstring.format( - y='x', orientation='horizontal' +docstring._snippet_manager["plot.boxploth"] = _boxplot_docstring.format( + y="x", orientation="horizontal" ) @@ -882,11 +891,11 @@ PlotAxes.violinploth matplotlib.axes.Axes.violinplot """ -docstring._snippet_manager['plot.violinplot'] = _violinplot_docstring.format( - y='y', orientation='vertical' +docstring._snippet_manager["plot.violinplot"] = _violinplot_docstring.format( + y="y", orientation="vertical" ) -docstring._snippet_manager['plot.violinploth'] = _violinplot_docstring.format( - y='x', orientation='horizontal' +docstring._snippet_manager["plot.violinploth"] = _violinplot_docstring.format( + y="x", orientation="horizontal" ) @@ -936,12 +945,12 @@ The weights associated with each point. If string this can be retrieved from `data` (see below). """ -docstring._snippet_manager['plot.weights'] = _weights_docstring -docstring._snippet_manager['plot.hist'] = _hist_docstring.format( - y='x', orientation='vertical' +docstring._snippet_manager["plot.weights"] = _weights_docstring +docstring._snippet_manager["plot.hist"] = _hist_docstring.format( + y="x", orientation="vertical" ) -docstring._snippet_manager['plot.histh'] = _hist_docstring.format( - y='x', orientation='horizontal' +docstring._snippet_manager["plot.histh"] = _hist_docstring.format( + y="x", orientation="horizontal" ) @@ -977,11 +986,11 @@ bins : int or 2-tuple of int, or array-like or 2-tuple of array-like, optional The bin count or exact bin edges for each dimension or both dimensions. """.rstrip() -docstring._snippet_manager['plot.hist2d'] = _hist2d_docstring.format( - command='hist2d', descrip='standard 2D histogram', bins=_bins_docstring +docstring._snippet_manager["plot.hist2d"] = _hist2d_docstring.format( + command="hist2d", descrip="standard 2D histogram", bins=_bins_docstring ) -docstring._snippet_manager['plot.hexbin'] = _hist2d_docstring.format( - command='hexbin', descrip='2D hexagonally binned histogram', bins='' +docstring._snippet_manager["plot.hexbin"] = _hist2d_docstring.format( + command="hexbin", descrip="2D hexagonally binned histogram", bins="" ) @@ -1007,7 +1016,7 @@ -------- matplotlib.axes.Axes.pie """ -docstring._snippet_manager['plot.pie'] = _pie_docstring +docstring._snippet_manager["plot.pie"] = _pie_docstring # Contour docstrings @@ -1040,17 +1049,21 @@ PlotAxes.tricontourf matplotlib.axes.Axes.{command} """ -docstring._snippet_manager['plot.contour'] = _contour_docstring.format( - descrip='contour lines', command='contour', edgefix='' +docstring._snippet_manager["plot.contour"] = _contour_docstring.format( + descrip="contour lines", command="contour", edgefix="" ) -docstring._snippet_manager['plot.contourf'] = _contour_docstring.format( - descrip='filled contours', command='contourf', edgefix='%(axes.edgefix)s\n', +docstring._snippet_manager["plot.contourf"] = _contour_docstring.format( + descrip="filled contours", + command="contourf", + edgefix="%(axes.edgefix)s\n", ) -docstring._snippet_manager['plot.tricontour'] = _contour_docstring.format( - descrip='contour lines on a triangular grid', command='tricontour', edgefix='' +docstring._snippet_manager["plot.tricontour"] = _contour_docstring.format( + descrip="contour lines on a triangular grid", command="tricontour", edgefix="" ) -docstring._snippet_manager['plot.tricontourf'] = _contour_docstring.format( - descrip='filled contours on a triangular grid', command='tricontourf', edgefix='\n%(axes.edgefix)s' # noqa: E501 +docstring._snippet_manager["plot.tricontourf"] = _contour_docstring.format( + descrip="filled contours on a triangular grid", + command="tricontourf", + edgefix="\n%(axes.edgefix)s", # noqa: E501 ) @@ -1102,20 +1115,20 @@ * ``'auto'``: Allows the data aspect ratio to change depending on the layout. In general this results in non-square grid boxes. """.rstrip() -docstring._snippet_manager['plot.pcolor'] = _pcolor_docstring.format( - descrip='irregular grid boxes', command='pcolor', aspect='' +docstring._snippet_manager["plot.pcolor"] = _pcolor_docstring.format( + descrip="irregular grid boxes", command="pcolor", aspect="" ) -docstring._snippet_manager['plot.pcolormesh'] = _pcolor_docstring.format( - descrip='regular grid boxes', command='pcolormesh', aspect='' +docstring._snippet_manager["plot.pcolormesh"] = _pcolor_docstring.format( + descrip="regular grid boxes", command="pcolormesh", aspect="" ) -docstring._snippet_manager['plot.pcolorfast'] = _pcolor_docstring.format( - descrip='grid boxes quickly', command='pcolorfast', aspect='' +docstring._snippet_manager["plot.pcolorfast"] = _pcolor_docstring.format( + descrip="grid boxes quickly", command="pcolorfast", aspect="" ) -docstring._snippet_manager['plot.tripcolor'] = _pcolor_docstring.format( - descrip='triangular grid boxes', command='tripcolor', aspect='' +docstring._snippet_manager["plot.tripcolor"] = _pcolor_docstring.format( + descrip="triangular grid boxes", command="tripcolor", aspect="" ) -docstring._snippet_manager['plot.heatmap'] = _pcolor_docstring.format( - descrip=_heatmap_descrip, command='pcolormesh', aspect=_heatmap_aspect +docstring._snippet_manager["plot.heatmap"] = _pcolor_docstring.format( + descrip=_heatmap_descrip, command="pcolormesh", aspect=_heatmap_aspect ) @@ -1144,14 +1157,14 @@ proplot.axes.PlotAxes matplotlib.axes.Axes.{command} """ -docstring._snippet_manager['plot.imshow'] = _show_docstring.format( - descrip='an image', command='imshow' +docstring._snippet_manager["plot.imshow"] = _show_docstring.format( + descrip="an image", command="imshow" ) -docstring._snippet_manager['plot.matshow'] = _show_docstring.format( - descrip='a matrix', command='matshow' +docstring._snippet_manager["plot.matshow"] = _show_docstring.format( + descrip="a matrix", command="matshow" ) -docstring._snippet_manager['plot.spy'] = _show_docstring.format( - descrip='a sparcity pattern', command='spy' +docstring._snippet_manager["plot.spy"] = _show_docstring.format( + descrip="a sparcity pattern", command="spy" ) @@ -1186,14 +1199,14 @@ PlotAxes.streamplot matplotlib.axes.Axes.{command} """ -docstring._snippet_manager['plot.barbs'] = _flow_docstring.format( - descrip='wind barbs', command='barbs' +docstring._snippet_manager["plot.barbs"] = _flow_docstring.format( + descrip="wind barbs", command="barbs" ) -docstring._snippet_manager['plot.quiver'] = _flow_docstring.format( - descrip='quiver arrows', command='quiver' +docstring._snippet_manager["plot.quiver"] = _flow_docstring.format( + descrip="quiver arrows", command="quiver" ) -docstring._snippet_manager['plot.stream'] = _flow_docstring.format( - descrip='streamlines', command='streamplot' +docstring._snippet_manager["plot.stream"] = _flow_docstring.format( + descrip="streamlines", command="streamplot" ) @@ -1205,7 +1218,7 @@ def _get_vert(vert=None, orientation=None, **kwargs): if vert is not None: return kwargs, vert elif orientation is not None: - return kwargs, orientation != 'horizontal' # should already be validated + return kwargs, orientation != "horizontal" # should already be validated else: return kwargs, True # fallback @@ -1221,18 +1234,18 @@ def _parse_vert( # the plot, scatter, area, or bar orientation users should use the differently # named functions. Internally, however, they use these keyword args. if default_vert is not None: - kwargs['vert'] = _not_none( + kwargs["vert"] = _not_none( vert=vert, - orientation=None if orientation is None else orientation == 'vertical', + orientation=None if orientation is None else orientation == "vertical", default=default_vert, ) if default_orientation is not None: - kwargs['orientation'] = _not_none( + kwargs["orientation"] = _not_none( orientation=orientation, - vert=None if vert is None else 'vertical' if vert else 'horizontal', + vert=None if vert is None else "vertical" if vert else "horizontal", default=default_orientation, ) - if kwargs.get('orientation', None) not in (None, 'horizontal', 'vertical'): + if kwargs.get("orientation", None) not in (None, "horizontal", "vertical"): raise ValueError("Orientation must be either 'horizontal' or 'vertical'.") return kwargs @@ -1244,13 +1257,13 @@ def _inside_seaborn_call(): """ frame = sys._getframe() absolute_names = ( - 'seaborn.distributions', - 'seaborn.categorical', - 'seaborn.relational', - 'seaborn.regression', + "seaborn.distributions", + "seaborn.categorical", + "seaborn.relational", + "seaborn.regression", ) while frame is not None: - if frame.f_globals.get('__name__', '') in absolute_names: + if frame.f_globals.get("__name__", "") in absolute_names: return True frame = frame.f_back return False @@ -1261,6 +1274,7 @@ class PlotAxes(base.Axes): The second lowest-level `~matplotlib.axes.Axes` subclass used by proplot. Implements all plotting overrides. """ + def __init__(self, *args, **kwargs): """ Parameters @@ -1285,54 +1299,69 @@ def _call_native(self, name, *args, **kwargs): # NOTE: Previously allowed internal matplotlib plotting function calls to run # through proplot overrides then avoided awkward conflicts in piecemeal fashion. # Now prevent internal calls from running through overrides using preprocessor - kwargs.pop('distribution', None) # remove stat distributions + kwargs.pop("distribution", None) # remove stat distributions with context._state_context(self, _internal_call=True): - if self._name == 'basemap': + if self._name == "basemap": obj = getattr(self.projection, name)(*args, ax=self, **kwargs) else: obj = getattr(super(), name)(*args, **kwargs) return obj def _call_negpos( - self, name, x, *ys, negcolor=None, poscolor=None, colorkey='facecolor', - use_where=False, use_zero=False, **kwargs + self, + name, + x, + *ys, + negcolor=None, + poscolor=None, + colorkey="facecolor", + use_where=False, + use_zero=False, + **kwargs, ): """ Call the plotting method separately for "negative" and "positive" data. """ if use_where: - kwargs.setdefault('interpolate', True) # see fill_between docs - for key in ('color', 'colors', 'facecolor', 'facecolors', 'where'): + kwargs.setdefault("interpolate", True) # see fill_between docs + for key in ("color", "colors", "facecolor", "facecolors", "where"): value = kwargs.pop(key, None) if value is not None: warnings._warn_proplot( - f'{name}() argument {key}={value!r} is incompatible with negpos=True. Ignoring.' # noqa: E501 + f"{name}() argument {key}={value!r} is incompatible with negpos=True. Ignoring." # noqa: E501 ) # Negative component yneg = list(ys) # copy if use_zero: # filter bar heights yneg[0] = inputs._safe_mask(ys[0] < 0, ys[0]) elif use_where: # apply fill_between mask - kwargs['where'] = ys[1] < ys[0] + kwargs["where"] = ys[1] < ys[0] else: yneg = inputs._safe_mask(ys[1] < ys[0], *ys) - kwargs[colorkey] = _not_none(negcolor, rc['negcolor']) + kwargs[colorkey] = _not_none(negcolor, rc["negcolor"]) negobj = self._call_native(name, x, *yneg, **kwargs) # Positive component ypos = list(ys) # copy if use_zero: # filter bar heights ypos[0] = inputs._safe_mask(ys[0] >= 0, ys[0]) elif use_where: # apply fill_between mask - kwargs['where'] = ys[1] >= ys[0] + kwargs["where"] = ys[1] >= ys[0] else: ypos = inputs._safe_mask(ys[1] >= ys[0], *ys) - kwargs[colorkey] = _not_none(poscolor, rc['poscolor']) + kwargs[colorkey] = _not_none(poscolor, rc["poscolor"]) posobj = self._call_native(name, x, *ypos, **kwargs) return cbook.silent_list(type(negobj).__name__, (negobj, posobj)) def _add_auto_labels( - self, obj, cobj=None, labels=False, labels_kw=None, - fmt=None, formatter=None, formatter_kw=None, precision=None, + self, + obj, + cobj=None, + labels=False, + labels_kw=None, + fmt=None, + formatter=None, + formatter_kw=None, + precision=None, ): """ Add number labels. Default formatter is `~proplot.ticker.SimpleFormatter` @@ -1344,28 +1373,38 @@ def _add_auto_labels( labels_kw = labels_kw or {} formatter_kw = formatter_kw or {} formatter = _not_none( - fmt_labels_kw=labels_kw.pop('fmt', None), - formatter_labels_kw=labels_kw.pop('formatter', None), + fmt_labels_kw=labels_kw.pop("fmt", None), + formatter_labels_kw=labels_kw.pop("formatter", None), fmt=fmt, formatter=formatter, - default='simple' + default="simple", ) precision = _not_none( - formatter_kw_precision=formatter_kw.pop('precision', None), + formatter_kw_precision=formatter_kw.pop("precision", None), precision=precision, default=3, # should be lower than the default intended for tick labels ) - formatter = constructor.Formatter(formatter, precision=precision, **formatter_kw) # noqa: E501 + formatter = constructor.Formatter( + formatter, precision=precision, **formatter_kw + ) # noqa: E501 if isinstance(obj, mcontour.ContourSet): self._add_contour_labels(obj, cobj, formatter, **labels_kw) elif isinstance(obj, mcollections.Collection): self._add_collection_labels(obj, formatter, **labels_kw) else: - raise RuntimeError(f'Not possible to add labels to object {obj!r}.') + raise RuntimeError(f"Not possible to add labels to object {obj!r}.") def _add_collection_labels( - self, obj, fmt, *, c=None, color=None, colors=None, - size=None, fontsize=None, **kwargs + self, + obj, + fmt, + *, + c=None, + color=None, + colors=None, + size=None, + fontsize=None, + **kwargs, ): """ Add labels to pcolor boxes with support for shade-dependent text colors. @@ -1376,9 +1415,9 @@ def _add_collection_labels( # issue where edge colors surround NaNs. Should maybe move this somewhere else. obj.update_scalarmappable() # update 'edgecolors' list color = _not_none(c=c, color=color, colors=colors) - fontsize = _not_none(size=size, fontsize=fontsize, default=rc['font.smallsize']) - kwargs.setdefault('ha', 'center') - kwargs.setdefault('va', 'center') + fontsize = _not_none(size=size, fontsize=fontsize, default=rc["font.smallsize"]) + kwargs.setdefault("ha", "center") + kwargs.setdefault("va", "center") # Apply colors and hide edge colors for empty grids labs = [] @@ -1390,15 +1429,15 @@ def _add_collection_labels( for i, (path, value) in enumerate(zip(paths, array)): # Round to the number corresponding to the *color* rather than # the exact data value. Similar to contour label numbering. - if value is ma.masked or not np.isfinite(value): + if value is ma.masked or not np.any(np.isfinite(value) == False): edgecolors[i, :] = 0 continue if isinstance(obj.norm, pcolors.DiscreteNorm): value = obj.norm._norm.inverse(obj.norm(value)) icolor = color if color is None: - _, _, lum = utils.to_xyz(obj.cmap(obj.norm(value)), 'hcl') - icolor = 'w' if lum < 50 else 'k' + _, _, lum = utils.to_xyz(obj.cmap(obj.norm(value)), "hcl") + icolor = "w" if lum < 50 else "k" bbox = path.get_extents() x = (bbox.xmin + bbox.xmax) / 2 y = (bbox.ymin + bbox.ymax) / 2 @@ -1409,8 +1448,18 @@ def _add_collection_labels( return labs def _add_contour_labels( - self, obj, cobj, fmt, *, c=None, color=None, colors=None, - size=None, fontsize=None, inline_spacing=None, **kwargs + self, + obj, + cobj, + fmt, + *, + c=None, + color=None, + colors=None, + size=None, + fontsize=None, + inline_spacing=None, + **kwargs, ): """ Add labels to contours with support for shade-dependent filled contour labels. @@ -1420,14 +1469,14 @@ def _add_contour_labels( # Parse input args zorder = max((h.get_zorder() for h in obj.collections), default=3) zorder = max(3, zorder + 1) - kwargs.setdefault('zorder', zorder) + kwargs.setdefault("zorder", zorder) colors = _not_none(c=c, color=color, colors=colors) - fontsize = _not_none(size=size, fontsize=fontsize, default=rc['font.smallsize']) + fontsize = _not_none(size=size, fontsize=fontsize, default=rc["font.smallsize"]) inline_spacing = _not_none(inline_spacing, 2.5) # Separate clabel args from text Artist args text_kw = {} - clabel_keys = ('levels', 'inline', 'manual', 'rightside_up', 'use_clabeltext') + clabel_keys = ("levels", "inline", "manual", "rightside_up", "use_clabeltext") for key in tuple(kwargs): # allow dict to change size if key not in clabel_keys: text_kw[key] = kwargs.pop(key) @@ -1438,12 +1487,15 @@ def _add_contour_labels( colors = [] for level in obj.levels: _, _, lum = utils.to_xyz(obj.cmap(obj.norm(level))) - colors.append('w' if lum < 50 else 'k') + colors.append("w" if lum < 50 else "k") # Draw the labels labs = cobj.clabel( - fmt=fmt, colors=colors, fontsize=fontsize, - inline_spacing=inline_spacing, **kwargs + fmt=fmt, + colors=colors, + fontsize=fontsize, + inline_spacing=inline_spacing, + **kwargs, ) if labs is not None: # returns None if no contours for lab in labs: @@ -1452,13 +1504,30 @@ def _add_contour_labels( return labs def _add_error_bars( - self, x, y, *_, distribution=None, - default_barstds=False, default_boxstds=False, - default_barpctiles=False, default_boxpctiles=False, default_marker=False, - bars=None, boxes=None, - barstd=None, barstds=None, barpctile=None, barpctiles=None, bardata=None, - boxstd=None, boxstds=None, boxpctile=None, boxpctiles=None, boxdata=None, - capsize=None, **kwargs, + self, + x, + y, + *_, + distribution=None, + default_barstds=False, + default_boxstds=False, + default_barpctiles=False, + default_boxpctiles=False, + default_marker=False, + bars=None, + boxes=None, + barstd=None, + barstds=None, + barpctile=None, + barpctiles=None, + bardata=None, + boxstd=None, + boxstds=None, + boxpctile=None, + boxpctiles=None, + boxdata=None, + capsize=None, + **kwargs, ): """ Add up to 2 error indicators: thick "boxes" and thin "bars". The ``default`` @@ -1474,8 +1543,10 @@ def _add_error_bars( barpctiles = _not_none(barpctile=barpctile, barpctiles=barpctiles) boxpctiles = _not_none(boxpctile=boxpctile, boxpctiles=boxpctiles) if distribution is not None and not any( - typ + mode in key for key in kwargs - for typ in ('shade', 'fade') for mode in ('', 'std', 'pctile', 'data') + typ + mode in key + for key in kwargs + for typ in ("shade", "fade") + for mode in ("", "std", "pctile", "data") ): # ugly kludge to check for shading if all(_ is None for _ in (bardata, barstds, barpctiles)): barstds, barpctiles = default_barstds, default_barpctiles @@ -1489,72 +1560,99 @@ def _add_error_bars( ) # Error bar properties - edgecolor = kwargs.get('edgecolor', rc['boxplot.whiskerprops.color']) - barprops = _pop_props(kwargs, 'line', ignore='marker', prefix='bar') - barprops['capsize'] = _not_none(capsize, rc['errorbar.capsize']) - barprops['linestyle'] = 'none' - barprops.setdefault('color', edgecolor) - barprops.setdefault('zorder', 2.5) - barprops.setdefault('linewidth', rc['boxplot.whiskerprops.linewidth']) + edgecolor = kwargs.get("edgecolor", rc["boxplot.whiskerprops.color"]) + barprops = _pop_props(kwargs, "line", ignore="marker", prefix="bar") + barprops["capsize"] = _not_none(capsize, rc["errorbar.capsize"]) + barprops["linestyle"] = "none" + barprops.setdefault("color", edgecolor) + barprops.setdefault("zorder", 2.5) + barprops.setdefault("linewidth", rc["boxplot.whiskerprops.linewidth"]) # Error box properties # NOTE: Includes 'markerfacecolor' and 'markeredgecolor' props - boxprops = _pop_props(kwargs, 'line', prefix='box') - boxprops['capsize'] = 0 - boxprops['linestyle'] = 'none' - boxprops.setdefault('color', barprops['color']) - boxprops.setdefault('zorder', barprops['zorder']) - boxprops.setdefault('linewidth', 4 * barprops['linewidth']) + boxprops = _pop_props(kwargs, "line", prefix="box") + boxprops["capsize"] = 0 + boxprops["linestyle"] = "none" + boxprops.setdefault("color", barprops["color"]) + boxprops.setdefault("zorder", barprops["zorder"]) + boxprops.setdefault("linewidth", 4 * barprops["linewidth"]) # Box marker properties - boxmarker = {key: boxprops.pop(key) for key in tuple(boxprops) if 'marker' in key} # noqa: E501 - boxmarker['c'] = _not_none(boxmarker.pop('markerfacecolor', None), 'white') - boxmarker['s'] = _not_none(boxmarker.pop('markersize', None), boxprops['linewidth'] ** 0.5) # noqa: E501 - boxmarker['zorder'] = boxprops['zorder'] - boxmarker['edgecolor'] = boxmarker.pop('markeredgecolor', None) - boxmarker['linewidth'] = boxmarker.pop('markerlinewidth', None) - if boxmarker.get('marker') is True: - boxmarker['marker'] = 'o' + boxmarker = { + key: boxprops.pop(key) for key in tuple(boxprops) if "marker" in key + } # noqa: E501 + boxmarker["c"] = _not_none(boxmarker.pop("markerfacecolor", None), "white") + boxmarker["s"] = _not_none( + boxmarker.pop("markersize", None), boxprops["linewidth"] ** 0.5 + ) # noqa: E501 + boxmarker["zorder"] = boxprops["zorder"] + boxmarker["edgecolor"] = boxmarker.pop("markeredgecolor", None) + boxmarker["linewidth"] = boxmarker.pop("markerlinewidth", None) + if boxmarker.get("marker") is True: + boxmarker["marker"] = "o" elif default_marker: - boxmarker.setdefault('marker', 'o') + boxmarker.setdefault("marker", "o") # Draw thin or thick error bars from distributions or explicit errdata # NOTE: Now impossible to make thin bar width different from cap width! # NOTE: Boxes must go after so scatter point can go on top - sy = 'y' if vert else 'x' # yerr + sy = "y" if vert else "x" # yerr ex, ey = (x, y) if vert else (y, x) eobjs = [] if showbars: # noqa: E501 edata, _ = inputs._dist_range( - y, distribution, - stds=barstds, pctiles=barpctiles, errdata=bardata, - stds_default=(-3, 3), pctiles_default=(0, 100), + y, + distribution, + stds=barstds, + pctiles=barpctiles, + errdata=bardata, + stds_default=(-3, 3), + pctiles_default=(0, 100), ) if edata is not None: - obj = self.errorbar(ex, ey, **barprops, **{sy + 'err': edata}) + obj = self.errorbar(ex, ey, **barprops, **{sy + "err": edata}) eobjs.append(obj) if showboxes: # noqa: E501 edata, _ = inputs._dist_range( - y, distribution, - stds=boxstds, pctiles=boxpctiles, errdata=boxdata, - stds_default=(-1, 1), pctiles_default=(25, 75), + y, + distribution, + stds=boxstds, + pctiles=boxpctiles, + errdata=boxdata, + stds_default=(-1, 1), + pctiles_default=(25, 75), ) if edata is not None: - obj = self.errorbar(ex, ey, **boxprops, **{sy + 'err': edata}) - if boxmarker.get('marker', None): + obj = self.errorbar(ex, ey, **boxprops, **{sy + "err": edata}) + if boxmarker.get("marker", None): self.scatter(ex, ey, **boxmarker) eobjs.append(obj) - kwargs['distribution'] = distribution + kwargs["distribution"] = distribution return (*eobjs, kwargs) def _add_error_shading( - self, x, y, *_, distribution=None, color_key='color', - shade=None, shadestd=None, shadestds=None, - shadepctile=None, shadepctiles=None, shadedata=None, - fade=None, fadestd=None, fadestds=None, - fadepctile=None, fadepctiles=None, fadedata=None, - shadelabel=False, fadelabel=False, **kwargs + self, + x, + y, + *_, + distribution=None, + color_key="color", + shade=None, + shadestd=None, + shadestds=None, + shadepctile=None, + shadepctiles=None, + shadedata=None, + fade=None, + fadestd=None, + fadestds=None, + fadepctile=None, + fadepctiles=None, + fadedata=None, + shadelabel=False, + fadelabel=False, + **kwargs, ): """ Add up to 2 error indicators: more opaque "shading" and less opaque "fading". @@ -1569,60 +1667,71 @@ def _add_error_shading( for _ in (shadestds, shadepctiles, shadedata) ) drawfade = any( - _ is not None and _ is not False - for _ in (fadestds, fadepctiles, fadedata) + _ is not None and _ is not False for _ in (fadestds, fadepctiles, fadedata) ) # Shading properties - shadeprops = _pop_props(kwargs, 'patch', prefix='shade') - shadeprops.setdefault('alpha', 0.4) - shadeprops.setdefault('zorder', 1.5) - shadeprops.setdefault('linewidth', rc['patch.linewidth']) - shadeprops.setdefault('edgecolor', 'none') + shadeprops = _pop_props(kwargs, "patch", prefix="shade") + shadeprops.setdefault("alpha", 0.4) + shadeprops.setdefault("zorder", 1.5) + shadeprops.setdefault("linewidth", rc["patch.linewidth"]) + shadeprops.setdefault("edgecolor", "none") # Fading properties - fadeprops = _pop_props(kwargs, 'patch', prefix='fade') - fadeprops.setdefault('zorder', shadeprops['zorder']) - fadeprops.setdefault('alpha', 0.5 * shadeprops['alpha']) - fadeprops.setdefault('linewidth', shadeprops['linewidth']) - fadeprops.setdefault('edgecolor', 'none') + fadeprops = _pop_props(kwargs, "patch", prefix="fade") + fadeprops.setdefault("zorder", shadeprops["zorder"]) + fadeprops.setdefault("alpha", 0.5 * shadeprops["alpha"]) + fadeprops.setdefault("linewidth", shadeprops["linewidth"]) + fadeprops.setdefault("edgecolor", "none") # Get default color then apply to outgoing keyword args so # that plotting function will not advance to next cycler color. # TODO: More robust treatment of 'color' vs. 'facecolor' if ( - drawshade and shadeprops.get('facecolor', None) is None - or drawfade and fadeprops.get('facecolor', None) is None + drawshade + and shadeprops.get("facecolor", None) is None + or drawfade + and fadeprops.get("facecolor", None) is None ): color = kwargs.get(color_key, None) if color is None: # add to outgoing color = kwargs[color_key] = self._get_lines.get_next_color() - shadeprops.setdefault('facecolor', color) - fadeprops.setdefault('facecolor', color) + shadeprops.setdefault("facecolor", color) + fadeprops.setdefault("facecolor", color) # Draw dark and light shading from distributions or explicit errdata eobjs = [] fill = self.fill_between if vert else self.fill_betweenx if drawfade: edata, label = inputs._dist_range( - y, distribution, - stds=fadestds, pctiles=fadepctiles, errdata=fadedata, - stds_default=(-3, 3), pctiles_default=(0, 100), - label=fadelabel, absolute=True, + y, + distribution, + stds=fadestds, + pctiles=fadepctiles, + errdata=fadedata, + stds_default=(-3, 3), + pctiles_default=(0, 100), + label=fadelabel, + absolute=True, ) if edata is not None: eobj = fill(x, *edata, label=label, **fadeprops) eobjs.append(eobj) if drawshade: edata, label = inputs._dist_range( - y, distribution, - stds=shadestds, pctiles=shadepctiles, errdata=shadedata, - stds_default=(-2, 2), pctiles_default=(10, 90), - label=shadelabel, absolute=True, + y, + distribution, + stds=shadestds, + pctiles=shadepctiles, + errdata=shadedata, + stds_default=(-2, 2), + pctiles_default=(10, 90), + label=shadelabel, + absolute=True, ) if edata is not None: eobj = fill(x, *edata, label=label, **shadeprops) eobjs.append(eobj) - kwargs['distribution'] = distribution + kwargs["distribution"] = distribution return (*eobjs, kwargs) def _fix_contour_edges(self, method, *args, **kwargs): @@ -1634,11 +1743,11 @@ def _fix_contour_edges(self, method, *args, **kwargs): # auto-labels. Filled contours create strange artifacts. # NOTE: Make the default 'line width' identical to one used for pcolor plots # rather than rc['contour.linewidth']. See mpl pcolor() source code - if not any(key in kwargs for key in ('linewidths', 'linestyles', 'edgecolors')): - kwargs['linewidths'] = 0 # for clabel - kwargs.setdefault('linewidths', EDGEWIDTH) - kwargs.pop('cmap', None) - kwargs['colors'] = kwargs.pop('edgecolors', 'k') + if not any(key in kwargs for key in ("linewidths", "linestyles", "edgecolors")): + kwargs["linewidths"] = 0 # for clabel + kwargs.setdefault("linewidths", EDGEWIDTH) + kwargs.pop("cmap", None) + kwargs["colors"] = kwargs.pop("edgecolors", "k") return self._call_native(method, *args, **kwargs) def _fix_sticky_edges(self, objs, axis, *args, only=None): @@ -1653,7 +1762,7 @@ def _fix_sticky_edges(self, objs, axis, *args, only=None): for obj in guides._iter_iterables(objs): if only and not isinstance(obj, only): continue # e.g. ignore error bars - convert = getattr(self, 'convert_' + axis + 'units') + convert = getattr(self, "convert_" + axis + "units") edges = getattr(obj.sticky_edges, axis) edges.extend(convert((min_, max_))) @@ -1672,15 +1781,15 @@ def _fix_patch_edges(obj, edgefix=None, **kwargs): linewidth = EDGEWIDTH if edgefix is True else 0 if edgefix is False else edgefix if not linewidth: return - keys = ('linewidth', 'linestyle', 'edgecolor') # patches and collections - if any(key + suffix in kwargs for key in keys for suffix in ('', 's')): + keys = ("linewidth", "linestyle", "edgecolor") # patches and collections + if any(key + suffix in kwargs for key in keys for suffix in ("", "s")): return rasterized = obj.get_rasterized() if isinstance(obj, martist.Artist) else False if rasterized: return # Skip when cmap has transparency - if hasattr(obj, 'get_alpha'): # collections and contour sets use singular + if hasattr(obj, "get_alpha"): # collections and contour sets use singular alpha = obj.get_alpha() if alpha is not None and alpha < 1: return @@ -1695,13 +1804,17 @@ def _fix_patch_edges(obj, edgefix=None, **kwargs): # NOTE: This also covers TriContourSet returned by tricontour if isinstance(obj, mcontour.ContourSet): if obj.filled: + obj.set_linestyle("-") + obj.set_linewidth(linewidth) + obj.set_edgecolor("face") + for contour in obj.collections: - contour.set_linestyle('-') + contour.set_linestyle("-") contour.set_linewidth(linewidth) - contour.set_edgecolor('face') + contour.set_edgecolor("face") elif isinstance(obj, mcollections.Collection): # e.g. QuadMesh, PolyCollection obj.set_linewidth(linewidth) - obj.set_edgecolor('face') + obj.set_edgecolor("face") elif isinstance(obj, mpatches.Patch): # e.g. Rectangle obj.set_linewidth(linewidth) obj.set_edgecolor(obj.get_facecolor()) @@ -1709,7 +1822,7 @@ def _fix_patch_edges(obj, edgefix=None, **kwargs): for element in obj: PlotAxes._fix_patch_edges(element, edgefix=edgefix) else: - warnings._warn_proplot(f'Unexpected obj {obj} passed to _fix_patch_edges.') + warnings._warn_proplot(f"Unexpected obj {obj} passed to _fix_patch_edges.") @contextlib.contextmanager def _keep_grid_bools(self): @@ -1721,15 +1834,13 @@ def _keep_grid_bools(self): # Axes3D which PlotAxes does not subclass. Safe to use xaxis and yaxis. bools = [] for axis, which in itertools.product( - (self.xaxis, self.yaxis), ('major', 'minor') + (self.xaxis, self.yaxis), ("major", "minor") ): - kw = getattr(axis, f'_{which}_tick_kw', {}) - bools.append(kw.get('gridOn', None)) - kw['gridOn'] = False # prevent deprecation warning + kw = getattr(axis, f"_{which}_tick_kw", {}) + bools.append(kw.get("gridOn", None)) + kw["gridOn"] = False # prevent deprecation warning yield - for b, (axis, which) in zip( - bools, itertools.product('xy', ('major', 'minor')) - ): + for b, (axis, which) in zip(bools, itertools.product("xy", ("major", "minor"))): if b is not None: self.grid(b, axis=axis, which=which) @@ -1740,7 +1851,7 @@ def _inbounds_extent(self, *, inbounds=None, **kwargs): ``_inbounds_xylim`` gets ``None`` it will silently exit. """ extents = None - inbounds = _not_none(inbounds, rc['axes.inbounds']) + inbounds = _not_none(inbounds, rc["axes.inbounds"]) if inbounds: extents = list(self.dataLim.extents) # ensure modifiable return kwargs, extents @@ -1755,10 +1866,10 @@ def _inbounds_vlim(self, x, y, z, *, to_centers=False): # keep this in a try-except clause for now. However *internally* we should # not reach this block unless everything is an array so raise that error. xmask = ymask = None - if self._name != 'cartesian': + if self._name != "cartesian": return z # TODO: support geographic projections when input is PlateCarree() - if not all(getattr(a, 'ndim', None) in (1, 2) for a in (x, y, z)): - raise ValueError('Invalid input coordinates. Must be 1D or 2D arrays.') + if not all(getattr(a, "ndim", None) in (1, 2) for a in (x, y, z)): + raise ValueError("Invalid input coordinates. Must be 1D or 2D arrays.") try: # Get centers and masks if to_centers and z.ndim == 2: @@ -1771,7 +1882,11 @@ def _inbounds_vlim(self, x, y, z, *, to_centers=False): ymask = (y >= min(ylim)) & (y <= max(ylim)) # Get subsample if xmask is not None and ymask is not None: - z = z[np.ix_(ymask, xmask)] if z.ndim == 2 and xmask.ndim == 1 else z[ymask & xmask] # noqa: E501 + z = ( + z[np.ix_(ymask, xmask)] + if z.ndim == 2 and xmask.ndim == 1 + else z[ymask & xmask] + ) # noqa: E501 elif xmask is not None: z = z[:, xmask] if z.ndim == 2 and xmask.ndim == 1 else z[xmask] elif ymask is not None: @@ -1779,8 +1894,8 @@ def _inbounds_vlim(self, x, y, z, *, to_centers=False): return z except Exception as err: warnings._warn_proplot( - 'Failed to restrict automatic colormap normalization ' - f'to in-bounds data only. Error message: {err}' + "Failed to restrict automatic colormap normalization " + f"to in-bounds data only. Error message: {err}" ) return z @@ -1795,7 +1910,7 @@ def _inbounds_xylim(self, extents, x, y, **kwargs): # but since proplot standardizes inputs we can easily use them for dataLim. if extents is None: return - if self._name != 'cartesian': + if self._name != "cartesian": return if not x.size or not y.size: return @@ -1815,7 +1930,7 @@ def _inbounds_xylim(self, extents, x, y, **kwargs): trans.y0 = extents[1] = min(convert(ymin), extents[1]) if ymax is not None: trans.y1 = extents[3] = max(convert(ymax), extents[3]) - getattr(self, '_request_autoscale_view', self.autoscale_view)() + getattr(self, "_request_autoscale_view", self.autoscale_view)() if autox and not autoy and y.shape == x.shape: # Reset the x data limits ymin, ymax = sorted(self.get_ylim()) @@ -1826,11 +1941,11 @@ def _inbounds_xylim(self, extents, x, y, **kwargs): trans.x0 = extents[0] = min(convert(xmin), extents[0]) if xmax is not None: trans.x1 = extents[2] = max(convert(xmax), extents[2]) - getattr(self, '_request_autoscale_view', self.autoscale_view)() + getattr(self, "_request_autoscale_view", self.autoscale_view)() except Exception as err: warnings._warn_proplot( - 'Failed to restrict automatic y (x) axis limit algorithm to ' - f'data within locked x (y) limits only. Error message: {err}' + "Failed to restrict automatic y (x) axis limit algorithm to " + f"data within locked x (y) limits only. Error message: {err}" ) def _parse_1d_args(self, x, *ys, **kwargs): @@ -1847,25 +1962,40 @@ def _parse_1d_args(self, x, *ys, **kwargs): elif ys[0] is None: ys = (np.array([0.0]), ys[1]) # user input keyword 'y2' but no y1 if any(y is None for y in ys): - raise ValueError('Missing required data array argument.') + raise ValueError("Missing required data array argument.") ys = tuple(map(inputs._to_duck_array, ys)) if x is not None: x = inputs._to_duck_array(x) x, *ys, kwargs = self._parse_1d_format(x, *ys, zerox=zerox, **kwargs) # Geographic corrections - if self._name == 'cartopy' and isinstance(kwargs.get('transform'), PlateCarree): # noqa: E501 + if self._name == "cartopy" and isinstance( + kwargs.get("transform"), PlateCarree + ): # noqa: E501 x, *ys = inputs._geo_cartopy_1d(x, *ys) - elif self._name == 'basemap' and kwargs.get('latlon', None): + elif self._name == "basemap" and kwargs.get("latlon", None): xmin, xmax = self._lonaxis.get_view_interval() x, *ys = inputs._geo_basemap_1d(x, *ys, xmin=xmin, xmax=xmax) return (x, *ys, kwargs) def _parse_1d_format( - self, x, *ys, zerox=False, autox=True, autoy=True, autoformat=None, - autoreverse=True, autolabels=True, autovalues=False, autoguide=True, - label=None, labels=None, value=None, values=None, **kwargs + self, + x, + *ys, + zerox=False, + autox=True, + autoy=True, + autoformat=None, + autoreverse=True, + autolabels=True, + autovalues=False, + autoguide=True, + label=None, + labels=None, + value=None, + values=None, + **kwargs, ): """ Try to retrieve default coordinates from array-like objects and apply default @@ -1874,15 +2004,15 @@ def _parse_1d_format( # Parse input y = max(ys, key=lambda y: y.size) # find a non-scalar y for inferring metadata autox = autox and not zerox # so far just relevant for hist() - autoformat = _not_none(autoformat, rc['autoformat']) + autoformat = _not_none(autoformat, rc["autoformat"]) kwargs, vert = _get_vert(**kwargs) labels = _not_none( label=label, labels=labels, value=value, values=values, - legend_kw_labels=kwargs.get('legend_kw', {}).pop('labels', None), - colorbar_kw_values=kwargs.get('colorbar_kw', {}).pop('values', None), + legend_kw_labels=kwargs.get("legend_kw", {}).pop("labels", None), + colorbar_kw_values=kwargs.get("colorbar_kw", {}).pop("values", None), ) # Retrieve the x coords @@ -1890,8 +2020,8 @@ def _parse_1d_format( # where we use 'means' or 'medians', columns coords (axis 1) are 'x' coords. # Otherwise, columns represent e.g. lines and row coords (axis 0) are 'x' # coords. Exception is passing "ragged arrays" to boxplot and violinplot. - dists = any(kwargs.get(s) for s in ('mean', 'means', 'median', 'medians')) - raggd = any(getattr(y, 'dtype', None) == 'object' for y in ys) + dists = any(kwargs.get(s) for s in ("mean", "means", "median", "medians")) + raggd = any(getattr(y, "dtype", None) == "object" for y in ys) xaxis = 0 if raggd else 1 if dists or not autoy else 0 if autox and x is None: x = inputs._meta_labels(y, axis=xaxis) # use the first one @@ -1915,35 +2045,35 @@ def _parse_1d_format( # Apply the labels or values if labels is not None: if autovalues: - kwargs['values'] = inputs._to_numpy_array(labels) + kwargs["values"] = inputs._to_numpy_array(labels) elif autolabels: - kwargs['labels'] = inputs._to_numpy_array(labels) + kwargs["labels"] = inputs._to_numpy_array(labels) # Apply title for legend or colorbar that uses the labels or values if autoguide and autoformat: title = inputs._meta_title(labels) if title: # safely update legend_kw and colorbar_kw - guides._add_guide_kw('legend', kwargs, title=title) - guides._add_guide_kw('colorbar', kwargs, title=title) + guides._add_guide_kw("legend", kwargs, title=title) + guides._add_guide_kw("colorbar", kwargs, title=title) # Apply the basic x and y settings - autox = autox and self._name == 'cartesian' - autoy = autoy and self._name == 'cartesian' - sx, sy = 'xy' if vert else 'yx' + autox = autox and self._name == "cartesian" + autoy = autoy and self._name == "cartesian" + sx, sy = "xy" if vert else "yx" kw_format = {} if autox and autoformat: # 'x' axis title = inputs._meta_title(x) if title: - axis = getattr(self, sx + 'axis') + axis = getattr(self, sx + "axis") if axis.isDefault_label: - kw_format[sx + 'label'] = title + kw_format[sx + "label"] = title if autoy and autoformat: # 'y' axis sy = sx if zerox else sy # hist() 'y' values are along 'x' axis title = inputs._meta_title(y) if title: - axis = getattr(self, sy + 'axis') + axis = getattr(self, sy + "axis") if axis.isDefault_label: - kw_format[sy + 'label'] = title + kw_format[sy + "label"] = title # Convert string-type coordinates to indices # NOTE: This should even allow qualitative string input to hist() @@ -1952,8 +2082,8 @@ def _parse_1d_format( if autoy: *ys, kw_format = inputs._meta_coords(*ys, which=sy, **kw_format) if autox and autoreverse and inputs._is_descending(x): - if getattr(self, f'get_autoscale{sx}_on')(): - kw_format[sx + 'reverse'] = True + if getattr(self, f"get_autoscale{sx}_on")(): + kw_format[sx + "reverse"] = True # Finally apply formatting and strip metadata # WARNING: Most methods that accept 2D arrays use columns of data, but when @@ -1967,8 +2097,16 @@ def _parse_1d_format( return (x, *ys, kwargs) def _parse_2d_args( - self, x, y, *zs, globe=False, edges=False, allow1d=False, - transpose=None, order=None, **kwargs + self, + x, + y, + *zs, + globe=False, + edges=False, + allow1d=False, + transpose=None, + order=None, + **kwargs, ): """ Interpret positional arguments for all 2D plotting commands. @@ -1976,19 +2114,19 @@ def _parse_2d_args( # Standardize values # NOTE: Functions pass two 'zs' at most right now if all(z is None for z in zs): - x, y, zs = None, None, (x, y)[:len(zs)] + x, y, zs = None, None, (x, y)[: len(zs)] if any(z is None for z in zs): - raise ValueError('Missing required data array argument(s).') + raise ValueError("Missing required data array argument(s).") zs = tuple(inputs._to_duck_array(z, strip_units=True) for z in zs) if x is not None: x = inputs._to_duck_array(x) if y is not None: y = inputs._to_duck_array(y) if order is not None: - if not isinstance(order, str) or order not in 'CF': + if not isinstance(order, str) or order not in "CF": raise ValueError(f"Invalid order={order!r}. Options are 'C' or 'F'.") transpose = _not_none( - transpose=transpose, transpose_order=bool('CF'.index(order)) + transpose=transpose, transpose_order=bool("CF".index(order)) ) if transpose: zs = tuple(z.T for z in zs) @@ -2006,11 +2144,15 @@ def _parse_2d_args( # Geographic corrections if allow1d: pass - elif self._name == 'cartopy' and isinstance(kwargs.get('transform'), PlateCarree): # noqa: E501 + elif self._name == "cartopy" and isinstance( + kwargs.get("transform"), PlateCarree + ): # noqa: E501 x, y, *zs = inputs._geo_cartopy_2d(x, y, *zs, globe=globe) - elif self._name == 'basemap' and kwargs.get('latlon', None): + elif self._name == "basemap" and kwargs.get("latlon", None): xmin, xmax = self._lonaxis.get_view_interval() - x, y, *zs = inputs._geo_basemap_2d(x, y, *zs, xmin=xmin, xmax=xmax, globe=globe) # noqa: E501 + x, y, *zs = inputs._geo_basemap_2d( + x, y, *zs, xmin=xmin, xmax=xmax, globe=globe + ) # noqa: E501 x, y = np.meshgrid(x, y) # WARNING: required always return (x, y, *zs, kwargs) @@ -2023,7 +2165,7 @@ def _parse_2d_format( formatting. Also apply optional transpose and update the keyword arguments. """ # Retrieve coordinates - autoformat = _not_none(autoformat, rc['autoformat']) + autoformat = _not_none(autoformat, rc["autoformat"]) if x is None and y is None: z = zs[0] if z.ndim == 1: @@ -2034,25 +2176,25 @@ def _parse_2d_format( y = inputs._meta_labels(z, axis=0) # Apply labels and XY axis settings - if self._name == 'cartesian': + if self._name == "cartesian": # Apply labels # NOTE: Do not overwrite existing labels! kw_format = {} if autoformat: - for s, d in zip('xy', (x, y)): + for s, d in zip("xy", (x, y)): title = inputs._meta_title(d) if title: - axis = getattr(self, s + 'axis') + axis = getattr(self, s + "axis") if axis.isDefault_label: - kw_format[s + 'label'] = title + kw_format[s + "label"] = title # Handle string-type coordinates - x, kw_format = inputs._meta_coords(x, which='x', **kw_format) - y, kw_format = inputs._meta_coords(y, which='y', **kw_format) - for s, d in zip('xy', (x, y)): + x, kw_format = inputs._meta_coords(x, which="x", **kw_format) + y, kw_format = inputs._meta_coords(y, which="y", **kw_format) + for s, d in zip("xy", (x, y)): if autoreverse and inputs._is_descending(d): - if getattr(self, f'get_autoscale{s}_on')(): - kw_format[s + 'reverse'] = True + if getattr(self, f"get_autoscale{s}_on")(): + kw_format[s + "reverse"] = True # Apply formatting if kw_format: @@ -2062,8 +2204,8 @@ def _parse_2d_format( if autoguide and autoformat: title = inputs._meta_title(zs[0]) if title: # safely update legend_kw and colorbar_kw - guides._add_guide_kw('legend', kwargs, title=title) - guides._add_guide_kw('colorbar', kwargs, title=title) + guides._add_guide_kw("legend", kwargs, title=title) + guides._add_guide_kw("colorbar", kwargs, title=title) # Finally strip metadata x = inputs._to_numpy_array(x) @@ -2087,24 +2229,42 @@ def _parse_color(self, x, y, c, *, apply_cycle=True, infer_rgb=False, **kwargs): kwargs = self._parse_cycle(**kwargs) else: c = np.atleast_1d(c) # should only have effect on 'scatter' input - if infer_rgb and (inputs._is_categorical(c) or c.ndim == 2 and c.shape[1] in (3, 4)): # noqa: E501 + if infer_rgb and ( + inputs._is_categorical(c) or c.ndim == 2 and c.shape[1] in (3, 4) + ): # noqa: E501 c = list(map(pcolors.to_hex, c)) # avoid iterating over columns else: - kwargs = self._parse_cmap(x, y, c, plot_lines=True, default_discrete=False, **kwargs) # noqa: E501 + kwargs = self._parse_cmap( + x, y, c, plot_lines=True, default_discrete=False, **kwargs + ) # noqa: E501 parsers = (self._parse_cycle,) pop = _pop_params(kwargs, *parsers, ignore_internal=True) if pop: - warnings._warn_proplot(f'Ignoring unused keyword arg(s): {pop}') + warnings._warn_proplot(f"Ignoring unused keyword arg(s): {pop}") return (c, kwargs) - @warnings._rename_kwargs('0.6.0', centers='values') + @warnings._rename_kwargs("0.6.0", centers="values") def _parse_cmap( - self, *args, - cmap=None, cmap_kw=None, c=None, color=None, colors=None, - norm=None, norm_kw=None, extend=None, vmin=None, vmax=None, discrete=None, - default_cmap=None, default_discrete=True, skip_autolev=False, - min_levels=None, plot_lines=False, plot_contours=False, - **kwargs + self, + *args, + cmap=None, + cmap_kw=None, + c=None, + color=None, + colors=None, + norm=None, + norm_kw=None, + extend=None, + vmin=None, + vmax=None, + discrete=None, + default_cmap=None, + default_discrete=True, + skip_autolev=False, + min_levels=None, + plot_lines=False, + plot_contours=False, + **kwargs, ): """ Parse colormap and normalizer arguments. @@ -2139,15 +2299,18 @@ def _parse_cmap( # Parse keyword args cmap_kw = cmap_kw or {} norm_kw = norm_kw or {} - vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop('vmin', None)) - vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop('vmax', None)) - extend = _not_none(extend, 'neither') + vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop("vmin", None)) + vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop("vmax", None)) + extend = _not_none(extend, "neither") colors = _not_none(c=c, color=color, colors=colors) # in case untranslated - modes = {key: kwargs.pop(key, None) for key in ('sequential', 'diverging', 'cyclic', 'qualitative')} # noqa: E501 + modes = { + key: kwargs.pop(key, None) + for key in ("sequential", "diverging", "cyclic", "qualitative") + } # noqa: E501 trues = {key: b for key, b in modes.items() if b} if len(trues) > 1: # noqa: E501 warnings._warn_proplot( - f'Conflicting colormap arguments: {trues!r}. Using the first one.' + f"Conflicting colormap arguments: {trues!r}. Using the first one." ) for key in tuple(trues)[1:]: del trues[key] @@ -2162,53 +2325,53 @@ def _parse_cmap( # makes single-level single-color contour plots, and since _parse_level_num is # only generates approximate level counts, the idea failed anyway. Users should # pass their own levels to avoid truncation/cycling in these very special cases. - autodiverging = rc['cmap.autodiverging'] + autodiverging = rc["cmap.autodiverging"] if colors is not None: if cmap is not None: warnings._warn_proplot( - f'you specified both cmap={cmap!s} and the qualitative-colormap ' + f"you specified both cmap={cmap!s} and the qualitative-colormap " f"colors={colors!r}. Ignoring 'colors'. If you meant to specify " - f'the edge color please use e.g. edgecolor={colors!r} instead.' + f"the edge color please use e.g. edgecolor={colors!r} instead." ) else: if mcolors.is_color_like(colors): colors = [colors] # RGB[A] tuple possibly cmap = colors = np.atleast_1d(colors) - cmap_kw['listmode'] = 'discrete' + cmap_kw["listmode"] = "discrete" if cmap is not None: if plot_lines: - cmap_kw['default_luminance'] = constructor.DEFAULT_CYCLE_LUMINANCE + cmap_kw["default_luminance"] = constructor.DEFAULT_CYCLE_LUMINANCE cmap = constructor.Colormap(cmap, **cmap_kw) - name = re.sub(r'\A_*(.*?)(?:_r|_s|_copy)*\Z', r'\1', cmap.name.lower()) + name = re.sub(r"\A_*(.*?)(?:_r|_s|_copy)*\Z", r"\1", cmap.name.lower()) if not any(name in opts for opts in pcolors.CMAPS_DIVERGING.items()): autodiverging = False # avoid auto-truncation of sequential colormaps # Force default options in special cases # NOTE: Delay application of 'sequential', 'diverging', 'cyclic', 'qualitative' # until after level generation so 'diverging' can be automatically applied. - if 'cyclic' in trues or getattr(cmap, '_cyclic', None): - if extend is not None and extend != 'neither': + if "cyclic" in trues or getattr(cmap, "_cyclic", None): + if extend is not None and extend != "neither": warnings._warn_proplot( f"Cyclic colormaps require extend='neither'. Ignoring extend={extend!r}" # noqa: E501 ) - extend = 'neither' - if 'qualitative' in trues or isinstance(cmap, pcolors.DiscreteColormap): + extend = "neither" + if "qualitative" in trues or isinstance(cmap, pcolors.DiscreteColormap): if discrete is not None and not discrete: # noqa: E501 warnings._warn_proplot( - 'Qualitative colormaps require discrete=True. Ignoring discrete=False.' # noqa: E501 + "Qualitative colormaps require discrete=True. Ignoring discrete=False." # noqa: E501 ) discrete = True if plot_contours: if discrete is not None and not discrete: warnings._warn_proplot( - 'Contoured plots require discrete=True. Ignoring discrete=False.' + "Contoured plots require discrete=True. Ignoring discrete=False." ) discrete = True - keys = ('levels', 'values', 'locator', 'negative', 'positive', 'symmetric') + keys = ("levels", "values", "locator", "negative", "positive", "symmetric") if any(key in kwargs for key in keys): # override discrete = _not_none(discrete, True) else: # use global boolean rc['cmap.discrete'] or command-specific default - discrete = _not_none(discrete, rc['cmap.discrete'], default_discrete) + discrete = _not_none(discrete, rc["cmap.discrete"], default_discrete) # Determine the appropriate 'vmin', 'vmax', and/or 'levels' # NOTE: Unlike xarray, but like matplotlib, vmin and vmax only approximately @@ -2224,34 +2387,41 @@ def _parse_cmap( isdiverging = True if discrete: levels, vmin, vmax, norm, norm_kw, kwargs = self._parse_level_vals( - *args, vmin=vmin, vmax=vmax, norm=norm, norm_kw=norm_kw, extend=extend, - min_levels=min_levels, skip_autolev=skip_autolev, **kwargs + *args, + vmin=vmin, + vmax=vmax, + norm=norm, + norm_kw=norm_kw, + extend=extend, + min_levels=min_levels, + skip_autolev=skip_autolev, + **kwargs, ) if autodiverging and levels is not None: _, counts = np.unique(np.sign(levels), return_counts=True) if counts[counts > 1].size > 1: isdiverging = True - if not trues and isdiverging and modes['diverging'] is None: - trues['diverging'] = modes['diverging'] = True + if not trues and isdiverging and modes["diverging"] is None: + trues["diverging"] = modes["diverging"] = True # Create the continuous normalizer. - norm = _not_none(norm, 'div' if 'diverging' in trues else 'linear') + norm = _not_none(norm, "div" if "diverging" in trues else "linear") if isinstance(norm, mcolors.Normalize): norm.vmin, norm.vmax = vmin, vmax else: norm = constructor.Norm(norm, vmin=vmin, vmax=vmax, **norm_kw) isdiverging = autodiverging and isinstance(norm, pcolors.DivergingNorm) - if not trues and isdiverging and modes['diverging'] is None: - trues['diverging'] = modes['diverging'] = True + if not trues and isdiverging and modes["diverging"] is None: + trues["diverging"] = modes["diverging"] = True # Create the final colormap if cmap is None: if default_cmap is not None: # used internally cmap = default_cmap elif trues: - cmap = rc['cmap.' + tuple(trues)[0]] + cmap = rc["cmap." + tuple(trues)[0]] else: - cmap = rc['image.cmap'] + cmap = rc["image.cmap"] cmap = constructor.Colormap(cmap, **cmap_kw) # Create the discrete normalizer @@ -2261,25 +2431,31 @@ def _parse_cmap( levels, norm, cmap, extend=extend, min_levels=min_levels, **kwargs ) params = _pop_params(kwargs, *self._level_parsers, ignore_internal=True) - if 'N' in params: # use this for lookup table N instead of levels N - cmap = cmap.copy(N=params.pop('N')) + if "N" in params: # use this for lookup table N instead of levels N + cmap = cmap.copy(N=params.pop("N")) if params: - warnings._warn_proplot(f'Ignoring unused keyword args(s): {params}') + warnings._warn_proplot(f"Ignoring unused keyword args(s): {params}") # Update outgoing args # NOTE: ContourSet natively stores 'extend' on the result but for other # classes we need to hide it on the object. - kwargs.update({'cmap': cmap, 'norm': norm}) + kwargs.update({"cmap": cmap, "norm": norm}) if plot_contours: - kwargs.update({'levels': levels, 'extend': extend}) + kwargs.update({"levels": levels, "extend": extend}) else: - guides._add_guide_kw('colorbar', kwargs, extend=extend) + guides._add_guide_kw("colorbar", kwargs, extend=extend) return kwargs def _parse_cycle( - self, ncycle=None, *, cycle=None, cycle_kw=None, - cycle_manually=None, return_cycle=False, **kwargs + self, + ncycle=None, + *, + cycle=None, + cycle_kw=None, + cycle_manually=None, + return_cycle=False, + **kwargs, ): """ Parse property cycle-related arguments. @@ -2305,18 +2481,18 @@ def _parse_cycle( if cycle is not None or cycle_kw: cycle_kw = cycle_kw or {} if ncycle != 1: # ignore for column-by-column plotting commands - cycle_kw.setdefault('N', ncycle) # if None then filled in Colormap() - if isinstance(cycle, str) and cycle.lower() == 'none': + cycle_kw.setdefault("N", ncycle) # if None then filled in Colormap() + if isinstance(cycle, str) and cycle.lower() == "none": cycle = False if not cycle: args = () elif cycle is True: # consistency with 'False' ('reactivate' the cycler) - args = (rc['axes.prop_cycle'],) + args = (rc["axes.prop_cycle"],) else: args = (cycle,) cycle = constructor.Cycle(*args, **cycle_kw) with warnings.catch_warnings(): # hide 'elementwise-comparison failed' - warnings.simplefilter('ignore', FutureWarning) + warnings.simplefilter("ignore", FutureWarning) if return_cycle: pass elif cycle != self._active_cycle: @@ -2327,16 +2503,21 @@ def _parse_cycle( cycle_manually = cycle_manually or {} parser = self._get_lines # the _process_plot_var_args instance props = {} # which keys to apply from property cycler + # BREAKING in mpl3.9.1 parse has cycle items and no longer posseses _prop_keys for prop, key in cycle_manually.items(): - if kwargs.get(key, None) is None and prop in parser._prop_keys: + if kwargs.get(key, None) is None and any( + prop in item for item in parser._cycler_items + ): props[prop] = key if props: - dict_ = next(parser.prop_cycler) - for prop, key in props.items(): - value = dict_[prop] - if key == 'c': # special case: scatter() color must be converted to hex - value = pcolors.to_hex(value) - kwargs[key] = value + for dict_ in parser._cycler_items: + for prop, key in props.items(): + value = dict_[prop] + if ( + key == "c" + ): # special case: scatter() color must be converted to hex + value = pcolors.to_hex(value) + kwargs[key] = value if return_cycle: return cycle, kwargs # needed for stem() to apply in a context() @@ -2344,9 +2525,17 @@ def _parse_cycle( return kwargs def _parse_level_lim( - self, *args, - vmin=None, vmax=None, robust=None, inbounds=None, - negative=None, positive=None, symmetric=None, to_centers=False, **kwargs + self, + *args, + vmin=None, + vmax=None, + robust=None, + inbounds=None, + negative=None, + positive=None, + symmetric=None, + to_centers=False, + **kwargs, ): """ Return a suitable vmin and vmax based on the input data. @@ -2380,8 +2569,8 @@ def _parse_level_lim( return vmin, vmax, kwargs # Parse input args - inbounds = _not_none(inbounds, rc['cmap.inbounds']) - robust = _not_none(robust, rc['cmap.robust'], False) + inbounds = _not_none(inbounds, rc["cmap.inbounds"]) + robust = _not_none(robust, rc["cmap.robust"], False) robust = 96 if robust is True else 100 if robust is False else robust robust = np.atleast_1d(robust) if robust.size == 1: @@ -2389,7 +2578,9 @@ def _parse_level_lim( elif robust.size == 2: pmin, pmax = robust.flat # pull out of array else: - raise ValueError(f'Unexpected robust={robust!r}. Must be bool, float, or 2-tuple.') # noqa: E501 + raise ValueError( + f"Unexpected robust={robust!r}. Must be bool, float, or 2-tuple." + ) # noqa: E501 # Get sample data # NOTE: Critical to use _to_numpy_array here because some @@ -2429,16 +2620,16 @@ def _parse_level_lim( vmax = 0 else: warnings._warn_proplot( - f'Incompatible arguments vmax={vmax!r} and negative=True. ' - 'Ignoring the latter.' + f"Incompatible arguments vmax={vmax!r} and negative=True. " + "Ignoring the latter." ) if positive: if automin: vmin = 0 else: warnings._warn_proplot( - f'Incompatible arguments vmin={vmin!r} and positive=True. ' - 'Ignoring the latter.' + f"Incompatible arguments vmin={vmin!r} and positive=True. " + "Ignoring the latter." ) if symmetric: if automin and not automax: @@ -2449,15 +2640,25 @@ def _parse_level_lim( vmin, vmax = -np.max(np.abs((vmin, vmax))), np.max(np.abs((vmin, vmax))) else: warnings._warn_proplot( - f'Incompatible arguments vmin={vmin!r}, vmax={vmax!r}, and ' - 'symmetric=True. Ignoring the latter.' + f"Incompatible arguments vmin={vmin!r}, vmax={vmax!r}, and " + "symmetric=True. Ignoring the latter." ) return vmin, vmax, kwargs def _parse_level_num( - self, *args, levels=None, locator=None, locator_kw=None, vmin=None, vmax=None, - norm=None, norm_kw=None, extend=None, symmetric=None, **kwargs + self, + *args, + levels=None, + locator=None, + locator_kw=None, + vmin=None, + vmax=None, + norm=None, + norm_kw=None, + extend=None, + symmetric=None, + **kwargs, ): """ Return a suitable level list given the input data, normalizer, @@ -2493,31 +2694,33 @@ def _parse_level_num( # zero level but may trim many of these below. norm_kw = norm_kw or {} locator_kw = locator_kw or {} - extend = _not_none(extend, 'neither') - levels = _not_none(levels, rc['cmap.levels']) - vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop('vmin', None)) - vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop('vmax', None)) - norm = constructor.Norm(norm or 'linear', **norm_kw) + extend = _not_none(extend, "neither") + levels = _not_none(levels, rc["cmap.levels"]) + vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop("vmin", None)) + vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop("vmax", None)) + norm = constructor.Norm(norm or "linear", **norm_kw) symmetric = _not_none( symmetric=symmetric, - locator_kw_symmetric=locator_kw.pop('symmetric', None), + locator_kw_symmetric=locator_kw.pop("symmetric", None), default=False, ) # Get default locator from input norm # NOTE: This normalizer is only temporary for inferring level locs - norm = constructor.Norm(norm or 'linear', **norm_kw) + norm = constructor.Norm(norm or "linear", **norm_kw) if locator is not None: locator = constructor.Locator(locator, **locator_kw) elif isinstance(norm, mcolors.LogNorm): locator = mticker.LogLocator(**locator_kw) elif isinstance(norm, mcolors.SymLogNorm): - for key, default in (('base', 10), ('linthresh', 1)): - val = _not_none(getattr(norm, key, None), getattr(norm, '_' + key, None), default) # noqa: E501 + for key, default in (("base", 10), ("linthresh", 1)): + val = _not_none( + getattr(norm, key, None), getattr(norm, "_" + key, None), default + ) # noqa: E501 locator_kw.setdefault(key, val) locator = mticker.SymmetricalLogLocator(**locator_kw) else: - locator_kw['symmetric'] = symmetric + locator_kw["symmetric"] = symmetric locator = mticker.MaxNLocator(levels, min_n_ticks=1, **locator_kw) # Get default level locations @@ -2538,15 +2741,15 @@ def _parse_level_num( # NOTE: This part is mostly copied from matplotlib _autolev if not symmetric: i0, i1 = 0, len(levels) # defaults - under, = np.where(levels < vmin) + (under,) = np.where(levels < vmin) if len(under): i0 = under[-1] - if not automin or extend in ('min', 'both'): + if not automin or extend in ("min", "both"): i0 += 1 # permit out-of-bounds data - over, = np.where(levels > vmax) + (over,) = np.where(levels > vmax) if len(over): i1 = over[0] + 1 if len(over) else len(levels) - if not automax or extend in ('max', 'both'): + if not automax or extend in ("max", "both"): i1 -= 1 # permit out-of-bounds data if i1 - i0 < 3: i0, i1 = 0, len(levels) # revert @@ -2568,9 +2771,20 @@ def _parse_level_num( return levels, kwargs def _parse_level_vals( - self, *args, N=None, levels=None, values=None, extend=None, - positive=False, negative=False, nozero=False, norm=None, norm_kw=None, - skip_autolev=False, min_levels=None, **kwargs, + self, + *args, + N=None, + levels=None, + values=None, + extend=None, + positive=False, + negative=False, + nozero=False, + norm=None, + norm_kw=None, + skip_autolev=False, + min_levels=None, + **kwargs, ): """ Return levels resulting from a wide variety of keyword options. @@ -2602,6 +2816,7 @@ def _parse_level_vals( **kwargs Unused arguments. """ + # Helper function that restricts levels # NOTE: This should have no effect if levels were generated automatically. # However want to apply these to manual-input levels as well. @@ -2609,7 +2824,7 @@ def _restrict_levels(levels): kw = {} levels = np.asarray(levels) if len(levels) > 2: - kw['atol'] = 1e-5 * np.min(np.diff(levels)) + kw["atol"] = 1e-5 * np.min(np.diff(levels)) if nozero: levels = levels[~np.isclose(levels, 0, **kw)] if positive: @@ -2626,26 +2841,26 @@ def _sanitize_levels(key, array, minsize): elif isinstance(array, Integral): pass elif array is not None: - raise ValueError(f'Invalid {key}={array}. Must be list or integer.') + raise ValueError(f"Invalid {key}={array}. Must be list or integer.") if isinstance(norm, (mcolors.BoundaryNorm, pcolors.SegmentedNorm)): if isinstance(array, Integral): warnings._warn_proplot( - f'Ignoring {key}={array}. Using norm={norm!r} {key} instead.' + f"Ignoring {key}={array}. Using norm={norm!r} {key} instead." ) - array = norm.boundaries if key == 'levels' else None + array = norm.boundaries if key == "levels" else None return array # Parse input arguments and resolve incompatibilities vmin = vmax = None - levels = _not_none(N=N, levels=levels, norm_kw_levs=norm_kw.pop('levels', None)) + levels = _not_none(N=N, levels=levels, norm_kw_levs=norm_kw.pop("levels", None)) if positive and negative: warnings._warn_proplot( - 'Incompatible args positive=True and negative=True. Using former.' + "Incompatible args positive=True and negative=True. Using former." ) negative = False if levels is not None and values is not None: warnings._warn_proplot( - f'Incompatible args levels={levels!r} and values={values!r}. Using former.' # noqa: E501 + f"Incompatible args levels={levels!r} and values={values!r}. Using former." # noqa: E501 ) values = None @@ -2656,14 +2871,14 @@ def _sanitize_levels(key, array, minsize): levels = values + 1 values = None if values is None: - levels = _sanitize_levels('levels', levels, _not_none(min_levels, 2)) - levels = _not_none(levels, rc['cmap.levels']) + levels = _sanitize_levels("levels", levels, _not_none(min_levels, 2)) + levels = _not_none(levels, rc["cmap.levels"]) else: - values = _sanitize_levels('values', values, 1) - kwargs['discrete_ticks'] = values # passed to _parse_level_norm + values = _sanitize_levels("values", values, 1) + kwargs["discrete_ticks"] = values # passed to _parse_level_norm if len(values) == 1: levels = [values[0] - 1, values[0] + 1] # weird but why not - elif norm is not None and norm not in ('segments', 'segmented'): + elif norm is not None and norm not in ("segments", "segmented"): # Generate levels by finding in-between points in the # normalized numeric space, e.g. LogNorm space. norm_kw = norm_kw or {} @@ -2690,11 +2905,17 @@ def _sanitize_levels(key, array, minsize): if np.iterable(levels): pop = _pop_params(kwargs, self._parse_level_num, ignore_internal=True) if pop: - warnings._warn_proplot(f'Ignoring unused keyword arg(s): {pop}') + warnings._warn_proplot(f"Ignoring unused keyword arg(s): {pop}") elif not skip_autolev: levels, kwargs = self._parse_level_num( - *args, levels=levels, norm=norm, norm_kw=norm_kw, extend=extend, - negative=negative, positive=positive, **kwargs + *args, + levels=levels, + norm=norm, + norm_kw=norm_kw, + extend=extend, + negative=negative, + positive=positive, + **kwargs, ) else: levels = values = None @@ -2713,16 +2934,23 @@ def _sanitize_levels(key, array, minsize): else: # use minimum and maximum vmin, vmax = np.min(levels), np.max(levels) if not np.allclose(levels[1] - levels[0], np.diff(levels)): - norm = _not_none(norm, 'segmented') - if norm in ('segments', 'segmented'): - norm_kw['levels'] = levels + norm = _not_none(norm, "segmented") + if norm in ("segments", "segmented"): + norm_kw["levels"] = levels return levels, vmin, vmax, norm, norm_kw, kwargs @staticmethod def _parse_level_norm( - levels, norm, cmap, *, extend=None, min_levels=None, - discrete_ticks=None, discrete_labels=None, **kwargs + levels, + norm, + cmap, + *, + extend=None, + min_levels=None, + discrete_ticks=None, + discrete_labels=None, + **kwargs, ): """ Create a `~proplot.colors.DiscreteNorm` or `~proplot.colors.BoundaryNorm` @@ -2757,30 +2985,30 @@ def _parse_level_norm( # Reverse the colormap if input levels or values were descending # See _parse_level_vals for details min_levels = _not_none(min_levels, 2) # 1 for contour plots - unique = extend = _not_none(extend, 'neither') + unique = extend = _not_none(extend, "neither") under = cmap._rgba_under over = cmap._rgba_over - cyclic = getattr(cmap, '_cyclic', None) + cyclic = getattr(cmap, "_cyclic", None) qualitative = isinstance(cmap, pcolors.DiscreteColormap) # see _parse_cmap if len(levels) < min_levels: raise ValueError( - f'Invalid levels={levels!r}. Must be at least length {min_levels}.' + f"Invalid levels={levels!r}. Must be at least length {min_levels}." ) # Ensure end colors are unique by scaling colors as if extend='both' # NOTE: Inside _parse_cmap should have enforced extend='neither' if cyclic: step = 0.5 # try to allocate space for unique end colors - unique = 'both' + unique = "both" # Ensure color list length matches level list length using rotation # NOTE: No harm if not enough colors, we just end up with the same # color for out-of-bounds extensions. This is a gentle failure elif qualitative: step = 0.5 # try to sample the central index for safety - unique = 'both' - auto_under = under is None and extend in ('min', 'both') - auto_over = over is None and extend in ('max', 'both') + unique = "both" + auto_under = under is None and extend in ("min", "both") + auto_over = over is None and extend in ("max", "both") ncolors = len(levels) - min_levels + 1 + auto_under + auto_over colors = list(itertools.islice(itertools.cycle(cmap.colors), ncolors)) if auto_under and len(colors) > 1: @@ -2798,24 +3026,28 @@ def _parse_level_norm( else: step = 1.0 if over is not None and under is not None: - unique = 'neither' + unique = "neither" elif over is not None: # turn off over-bounds unique bin - if extend == 'both': - unique = 'min' - elif extend == 'max': - unique = 'neither' + if extend == "both": + unique = "min" + elif extend == "max": + unique = "neither" elif under is not None: # turn off under-bounds unique bin - if extend == 'both': - unique = 'min' - elif extend == 'max': - unique = 'neither' + if extend == "both": + unique = "min" + elif extend == "max": + unique = "neither" # Generate DiscreteNorm and update "child" norm with vmin and vmax from # levels. This lets the colorbar set tick locations properly! if not isinstance(norm, mcolors.BoundaryNorm) and len(levels) > 1: norm = pcolors.DiscreteNorm( - levels, norm=norm, unique=unique, step=step, - ticks=discrete_ticks, labels=discrete_labels, + levels, + norm=norm, + unique=unique, + step=step, + ticks=discrete_ticks, + labels=discrete_labels, ) return norm, cmap, kwargs @@ -2827,7 +3059,7 @@ def _apply_plot(self, *pairs, vert=True, **kwargs): # Plot the lines objs, xsides = [], [] kws = kwargs.copy() - kws.update(_pop_props(kws, 'line')) + kws.update(_pop_props(kws, "line")) kws, extents = self._inbounds_extent(**kws) for xs, ys, fmt in self._iter_arg_pairs(*pairs): xs, ys, kw = self._parse_1d_args(xs, ys, vert=vert, **kws) @@ -2835,7 +3067,9 @@ def _apply_plot(self, *pairs, vert=True, **kwargs): guide_kw = _pop_params(kw, self._update_guide) # after standardize for _, n, x, y, kw in self._iter_arg_cols(xs, ys, **kw): kw = self._parse_cycle(n, **kw) - *eb, kw = self._add_error_bars(x, y, vert=vert, default_barstds=True, **kw) # noqa: E501 + *eb, kw = self._add_error_bars( + x, y, vert=vert, default_barstds=True, **kw + ) # noqa: E501 *es, kw = self._add_error_shading(x, y, vert=vert, **kw) xsides.append(x) if not vert: @@ -2843,14 +3077,14 @@ def _apply_plot(self, *pairs, vert=True, **kwargs): a = [x, y] if fmt is not None: # x1, y1, fmt1, x2, y2, fm2... style input a.append(fmt) - obj, = self._call_native('plot', *a, **kw) + (obj,) = self._call_native("plot", *a, **kw) self._inbounds_xylim(extents, x, y) objs.append((*eb, *es, obj) if eb or es else obj) # Add sticky edges - self._fix_sticky_edges(objs, 'x' if vert else 'y', *xsides, only=mlines.Line2D) + self._fix_sticky_edges(objs, "x" if vert else "y", *xsides, only=mlines.Line2D) self._update_guide(objs, **guide_kw) - return cbook.silent_list('Line2D', objs) # always return list + return cbook.silent_list("Line2D", objs) # always return list @docstring._snippet_manager def line(self, *args, **kwargs): @@ -2866,7 +3100,7 @@ def linex(self, *args, **kwargs): """ return self.plotx(*args, **kwargs) - @inputs._preprocess_or_redirect('x', 'y', allow_extra=True) + @inputs._preprocess_or_redirect("x", "y", allow_extra=True) @docstring._concatenate_inherited @docstring._snippet_manager def plot(self, *args, **kwargs): @@ -2876,7 +3110,7 @@ def plot(self, *args, **kwargs): kwargs = _parse_vert(default_vert=True, **kwargs) return self._apply_plot(*args, **kwargs) - @inputs._preprocess_or_redirect('y', 'x', allow_extra=True) + @inputs._preprocess_or_redirect("y", "x", allow_extra=True) @docstring._snippet_manager def plotx(self, *args, **kwargs): """ @@ -2885,7 +3119,7 @@ def plotx(self, *args, **kwargs): kwargs = _parse_vert(default_vert=False, **kwargs) return self._apply_plot(*args, **kwargs) - def _apply_step(self, *pairs, vert=True, where='pre', **kwargs): + def _apply_step(self, *pairs, vert=True, where="pre", **kwargs): """ Plot the steps. """ @@ -2894,30 +3128,30 @@ def _apply_step(self, *pairs, vert=True, where='pre', **kwargs): # approach... but instead repeat _apply_plot internals here so we can # disable error indications that make no sense for 'step' plots. kws = kwargs.copy() - opts = ('pre', 'post', 'mid') + opts = ("pre", "post", "mid") if where not in opts: - raise ValueError(f'Invalid where={where!r}. Options are {opts!r}.') - kws.update(_pop_props(kws, 'line')) - kws.setdefault('drawstyle', 'steps-' + where) + raise ValueError(f"Invalid where={where!r}. Options are {opts!r}.") + kws.update(_pop_props(kws, "line")) + kws.setdefault("drawstyle", "steps-" + where) kws, extents = self._inbounds_extent(**kws) objs = [] for xs, ys, fmt in self._iter_arg_pairs(*pairs): xs, ys, kw = self._parse_1d_args(xs, ys, vert=vert, **kws) guide_kw = _pop_params(kw, self._update_guide) # after standardize if fmt is not None: - kw['fmt'] = fmt + kw["fmt"] = fmt for _, n, x, y, *a, kw in self._iter_arg_cols(xs, ys, **kw): kw = self._parse_cycle(n, **kw) if not vert: x, y = y, x - obj, = self._call_native('step', x, y, *a, **kw) + (obj,) = self._call_native("step", x, y, *a, **kw) self._inbounds_xylim(extents, x, y) objs.append(obj) self._update_guide(objs, **guide_kw) - return cbook.silent_list('Line2D', objs) # always return list + return cbook.silent_list("Line2D", objs) # always return list - @inputs._preprocess_or_redirect('x', 'y', allow_extra=True) + @inputs._preprocess_or_redirect("x", "y", allow_extra=True) @docstring._concatenate_inherited @docstring._snippet_manager def step(self, *args, **kwargs): @@ -2927,7 +3161,7 @@ def step(self, *args, **kwargs): kwargs = _parse_vert(default_vert=True, **kwargs) return self._apply_step(*args, **kwargs) - @inputs._preprocess_or_redirect('y', 'x', allow_extra=True) + @inputs._preprocess_or_redirect("y", "x", allow_extra=True) @docstring._snippet_manager def stepx(self, *args, **kwargs): """ @@ -2937,8 +3171,15 @@ def stepx(self, *args, **kwargs): return self._apply_step(*args, **kwargs) def _apply_stem( - self, x, y, *, - linefmt=None, markerfmt=None, basefmt=None, orientation=None, **kwargs + self, + x, + y, + *, + linefmt=None, + markerfmt=None, + basefmt=None, + orientation=None, + **kwargs, ): """ Plot stem lines and markers. @@ -2956,16 +3197,16 @@ def _apply_stem( # color the stems. To make this more robust we temporarily replace the cycler. # Bizarrely stem() only reads from the global cycler() so have to update it. fmts = (linefmt, basefmt, markerfmt) - orientation = _not_none(orientation, 'vertical') - if not any(isinstance(fmt, str) and re.match(r'\AC[0-9]', fmt) for fmt in fmts): - cycle = constructor.Cycle((rc['negcolor'], rc['poscolor']), name='_no_name') - kw.setdefault('cycle', cycle) - kw['basefmt'] = _not_none(basefmt, 'C1-') # red base - kw['linefmt'] = linefmt = _not_none(linefmt, 'C0-') # blue stems - kw['markerfmt'] = _not_none(markerfmt, linefmt[:-1] + 'o') # blue marker + orientation = _not_none(orientation, "vertical") + if not any(isinstance(fmt, str) and re.match(r"\AC[0-9]", fmt) for fmt in fmts): + cycle = constructor.Cycle((rc["negcolor"], rc["poscolor"]), name="_no_name") + kw.setdefault("cycle", cycle) + kw["basefmt"] = _not_none(basefmt, "C1-") # red base + kw["linefmt"] = linefmt = _not_none(linefmt, "C0-") # blue stems + kw["markerfmt"] = _not_none(markerfmt, linefmt[:-1] + "o") # blue marker sig = inspect.signature(maxes.Axes.stem) - if 'use_line_collection' in sig.parameters: - kw.setdefault('use_line_collection', True) + if "use_line_collection" in sig.parameters: + kw.setdefault("use_line_collection", True) # Call function then restore property cycle # WARNING: Horizontal stem plots are only supported in recent versions of @@ -2973,35 +3214,35 @@ def _apply_stem( ctx = {} cycle, kw = self._parse_cycle(return_cycle=True, **kw) # allow re-application if cycle is not None: - ctx['axes.prop_cycle'] = cycle - if orientation == 'horizontal': # may raise error - kw['orientation'] = orientation + ctx["axes.prop_cycle"] = cycle + if orientation == "horizontal": # may raise error + kw["orientation"] = orientation with rc.context(ctx): - obj = self._call_native('stem', x, y, **kw) + obj = self._call_native("stem", x, y, **kw) self._inbounds_xylim(extents, x, y, orientation=orientation) self._update_guide(obj, **guide_kw) return obj - @inputs._preprocess_or_redirect('x', 'y') + @inputs._preprocess_or_redirect("x", "y") @docstring._concatenate_inherited @docstring._snippet_manager def stem(self, *args, **kwargs): """ %(plot.stem)s """ - kwargs = _parse_vert(default_orientation='vertical', **kwargs) + kwargs = _parse_vert(default_orientation="vertical", **kwargs) return self._apply_stem(*args, **kwargs) - @inputs._preprocess_or_redirect('x', 'y') + @inputs._preprocess_or_redirect("x", "y") @docstring._snippet_manager def stemx(self, *args, **kwargs): """ %(plot.stemx)s """ - kwargs = _parse_vert(default_orientation='horizontal', **kwargs) + kwargs = _parse_vert(default_orientation="horizontal", **kwargs) return self._apply_stem(*args, **kwargs) - @inputs._preprocess_or_redirect('x', 'y', ('c', 'color', 'colors', 'values')) + @inputs._preprocess_or_redirect("x", "y", ("c", "color", "colors", "values")) @docstring._snippet_manager def parametric(self, x, y, c, *, interp=0, scalex=True, scaley=True, **kwargs): """ @@ -3013,13 +3254,13 @@ def parametric(self, x, y, c, *, interp=0, scalex=True, scaley=True, **kwargs): # NOTE: We want to be able to think of 'c' as a scatter color array and # as a colormap color list. Try to support that here. kw = kwargs.copy() - kw.update(_pop_props(kw, 'collection')) + kw.update(_pop_props(kw, "collection")) kw, extents = self._inbounds_extent(**kw) - label = _not_none(**{key: kw.pop(key, None) for key in ('label', 'value')}) + label = _not_none(**{key: kw.pop(key, None) for key in ("label", "value")}) x, y, kw = self._parse_1d_args( x, y, values=c, autovalues=True, autoreverse=False, **kw ) - c = kw.pop('values', None) # permits auto-inferring values + c = kw.pop("values", None) # permits auto-inferring values c = np.arange(y.size) if c is None else inputs._to_numpy_array(c) if ( c.size in (3, 4) @@ -3027,17 +3268,17 @@ def parametric(self, x, y, c, *, interp=0, scalex=True, scaley=True, **kwargs): and mcolors.is_color_like(tuple(c.flat)) or all(map(mcolors.is_color_like, c)) ): - c, kw['colors'] = np.arange(c.shape[0]), c # convert color specs + c, kw["colors"] = np.arange(c.shape[0]), c # convert color specs # Interpret color values # NOTE: This permits string label input for 'values' - c, guide_kw = inputs._meta_coords(c, which='') # convert string labels + c, guide_kw = inputs._meta_coords(c, which="") # convert string labels if c.size == 1 and y.size != 1: c = np.arange(y.size) # convert dummy label for single color if guide_kw: - guides._add_guide_kw('colorbar', kw, **guide_kw) + guides._add_guide_kw("colorbar", kw, **guide_kw) else: - guides._add_guide_kw('colorbar', kw, locator=c) + guides._add_guide_kw("colorbar", kw, locator=c) # Interpolate values to allow for smooth gradations between values or just # to color siwtchover halfway between points (interp True, False respectively) @@ -3067,42 +3308,56 @@ def parametric(self, x, y, c, *, interp=0, scalex=True, scaley=True, **kwargs): coords = np.array(coords) # Get the colormap accounting for 'discrete' mode - discrete = kw.get('discrete', None) + discrete = kw.get("discrete", None) if discrete is not None and not discrete: a = (x, y, c) # pick levels from vmin and vmax, possibly limiting range else: - a, kw['values'] = (), c + a, kw["values"] = (), c kw = self._parse_cmap(*a, plot_lines=True, **kw) - cmap, norm = kw.pop('cmap'), kw.pop('norm') + cmap, norm = kw.pop("cmap"), kw.pop("norm") # Add collection with some custom attributes # NOTE: Modern API uses self._request_autoscale_view but this is # backwards compatible to earliest matplotlib versions. guide_kw = _pop_params(kw, self._update_guide) obj = mcollections.LineCollection( - coords, cmap=cmap, norm=norm, label=label, - linestyles='-', capstyle='butt', joinstyle='miter', + coords, + cmap=cmap, + norm=norm, + label=label, + linestyles="-", + capstyle="butt", + joinstyle="miter", ) obj.set_array(c) # the ScalarMappable method - obj.update({key: value for key, value in kw.items() if key not in ('color',)}) + obj.update({key: value for key, value in kw.items() if key not in ("color",)}) self.add_collection(obj) # also adjusts label self.autoscale_view(scalex=scalex, scaley=scaley) self._update_guide(obj, **guide_kw) return obj def _apply_lines( - self, xs, ys1, ys2, colors, *, - vert=True, stack=None, stacked=None, negpos=False, **kwargs + self, + xs, + ys1, + ys2, + colors, + *, + vert=True, + stack=None, + stacked=None, + negpos=False, + **kwargs, ): """ Plot vertical or hotizontal lines at each point. """ # Parse input arguments kw = kwargs.copy() - name = 'vlines' if vert else 'hlines' + name = "vlines" if vert else "hlines" if colors is not None: - kw['colors'] = colors - kw.update(_pop_props(kw, 'collection')) + kw["colors"] = colors + kw.update(_pop_props(kw, "collection")) kw, extents = self._inbounds_extent(**kw) stack = _not_none(stack=stack, stacked=stacked) xs, ys1, ys2, kw = self._parse_1d_args(xs, ys1, ys2, vert=vert, **kw) @@ -3120,7 +3375,7 @@ def _apply_lines( y2 = y2 + y0 y0 = y0 + y2 - y1 # irrelevant that we added y0 to both if negpos: - obj = self._call_negpos(name, x, y1, y2, colorkey='colors', **kw) + obj = self._call_negpos(name, x, y1, y2, colorkey="colors", **kw) else: obj = self._call_native(name, x, y1, y2, **kw) for y in (y1, y2): @@ -3130,15 +3385,12 @@ def _apply_lines( objs.append(obj) # Draw guide and add sticky edges - self._fix_sticky_edges(objs, 'y' if vert else 'x', *sides) + self._fix_sticky_edges(objs, "y" if vert else "x", *sides) self._update_guide(objs, **guide_kw) - return ( - objs[0] if len(objs) == 1 - else cbook.silent_list('LineCollection', objs) - ) + return objs[0] if len(objs) == 1 else cbook.silent_list("LineCollection", objs) # WARNING: breaking change from native 'ymin' and 'ymax' - @inputs._preprocess_or_redirect('x', 'y1', 'y2', ('c', 'color', 'colors')) + @inputs._preprocess_or_redirect("x", "y1", "y2", ("c", "color", "colors")) @docstring._snippet_manager def vlines(self, *args, **kwargs): """ @@ -3148,7 +3400,7 @@ def vlines(self, *args, **kwargs): return self._apply_lines(*args, **kwargs) # WARNING: breaking change from native 'xmin' and 'xmax' - @inputs._preprocess_or_redirect('y', 'x1', 'x2', ('c', 'color', 'colors')) + @inputs._preprocess_or_redirect("y", "x1", "x2", ("c", "color", "colors")) @docstring._snippet_manager def hlines(self, *args, **kwargs): """ @@ -3169,7 +3421,7 @@ def _parse_markersize( absolute_size = s.size == 1 or _inside_seaborn_call() if not absolute_size or smin is not None or smax is not None: smin = _not_none(smin, 1) - smax = _not_none(smax, rc['lines.markersize'] ** (1, 2)[area_size]) + smax = _not_none(smax, rc["lines.markersize"] ** (1, 2)[area_size]) dmin, dmax = inputs._safe_range(s) # data value range if dmin is not None and dmax is not None and dmin != dmax: s = smin + (smax - smin) * (s - dmin) / (dmax - dmin) @@ -3186,60 +3438,68 @@ def _apply_scatter(self, xs, ys, ss, cc, *, vert=True, **kwargs): # scatter() plots. It only ever inherits color from that. We instead use # _get_lines to help overarching goal of unifying plot() and scatter(). cycle_manually = { - 'alpha': 'alpha', 'color': 'c', - 'markerfacecolor': 'c', 'markeredgecolor': 'edgecolors', - 'marker': 'marker', 'markersize': 's', 'markeredgewidth': 'linewidths', - 'linestyle': 'linestyles', 'linewidth': 'linewidths', + "alpha": "alpha", + "color": "c", + "markerfacecolor": "c", + "markeredgecolor": "edgecolors", + "marker": "marker", + "markersize": "s", + "markeredgewidth": "linewidths", + "linestyle": "linestyles", + "linewidth": "linewidths", } - # Iterate over the columns - # NOTE: Use 'inbounds' for both cmap and axes 'inbounds' restriction kw = kwargs.copy() - inbounds = kw.pop('inbounds', None) - kw.update(_pop_props(kw, 'collection')) + inbounds = kw.pop("inbounds", None) + kw.update(_pop_props(kw, "collection")) kw, extents = self._inbounds_extent(inbounds=inbounds, **kw) xs, ys, kw = self._parse_1d_args(xs, ys, vert=vert, autoreverse=False, **kw) ys, kw = inputs._dist_reduce(ys, **kw) ss, kw = self._parse_markersize(ss, **kw) # parse 's' - infer_rgb = True - if cc is not None and not isinstance(cc, str): - test = np.atleast_1d(cc) # for testing only - if ( - any(_.ndim == 2 and _.shape[1] in (3, 4) for _ in (xs, ys)) - and test.ndim == 2 and test.shape[1] in (3, 4) - ): - infer_rgb = False - cc, kw = self._parse_color( - xs, ys, cc, inbounds=inbounds, apply_cycle=False, infer_rgb=infer_rgb, **kw - ) + + # Move _parse_cycle before _parse_color + kw = self._parse_cycle(xs.shape[1] if xs.ndim > 1 else 1, **kw) + + # Only parse color if explicitly provided + if cc is not None: + infer_rgb = True + if not isinstance(cc, str): + test = np.atleast_1d(cc) + if ( + any(_.ndim == 2 and _.shape[1] in (3, 4) for _ in (xs, ys)) + and test.ndim == 2 + and test.shape[1] in (3, 4) + ): + infer_rgb = False + cc, kw = self._parse_color(xs, ys, cc, inbounds=inbounds, apply_cycle=False, infer_rgb=infer_rgb, **kw) + guide_kw = _pop_params(kw, self._update_guide) objs = [] for _, n, x, y, s, c, kw in self._iter_arg_cols(xs, ys, ss, cc, **kw): - kw['s'], kw['c'] = s, c # make _parse_cycle() detect these - kw = self._parse_cycle(n, cycle_manually=cycle_manually, **kw) + # Don't set 'c' explicitly unless it was provided + kw["s"] = s + if c is not None: + kw["c"] = c *eb, kw = self._add_error_bars(x, y, vert=vert, default_barstds=True, **kw) - *es, kw = self._add_error_shading(x, y, vert=vert, color_key='c', **kw) + *es, kw = self._add_error_shading(x, y, vert=vert, color_key="c", **kw) if not vert: x, y = y, x - obj = self._call_native('scatter', x, y, **kw) + obj = self._call_native("scatter", x, y, **kw) self._inbounds_xylim(extents, x, y) objs.append((*eb, *es, obj) if eb or es else obj) self._update_guide(objs, queue_colorbar=False, **guide_kw) - return ( - objs[0] if len(objs) == 1 - else cbook.silent_list('PathCollection', objs) - ) + return objs[0] if len(objs) == 1 else cbook.silent_list("PathCollection", objs) # NOTE: Matplotlib internally applies scatter 'c' arguments as the # 'facecolors' argument to PathCollection. So perfectly reasonable to # point both 'color' and 'facecolor' arguments to the 'c' keyword here. @inputs._preprocess_or_redirect( - 'x', - 'y', - _get_aliases('collection', 'sizes'), - _get_aliases('collection', 'colors', 'facecolors'), - keywords=_get_aliases('collection', 'linewidths', 'edgecolors') + "x", + "y", + _get_aliases("collection", "sizes"), + _get_aliases("collection", "colors", "facecolors"), + keywords=_get_aliases("collection", "linewidths", "edgecolors"), ) @docstring._concatenate_inherited @docstring._snippet_manager @@ -3251,11 +3511,11 @@ def scatter(self, *args, **kwargs): return self._apply_scatter(*args, **kwargs) @inputs._preprocess_or_redirect( - 'y', - 'x', - _get_aliases('collection', 'sizes'), - _get_aliases('collection', 'colors', 'facecolors'), - keywords=_get_aliases('collection', 'linewidths', 'edgecolors') + "y", + "x", + _get_aliases("collection", "sizes"), + _get_aliases("collection", "colors", "facecolors"), + keywords=_get_aliases("collection", "linewidths", "edgecolors"), ) @docstring._snippet_manager def scatterx(self, *args, **kwargs): @@ -3266,17 +3526,26 @@ def scatterx(self, *args, **kwargs): return self._apply_scatter(*args, **kwargs) def _apply_fill( - self, xs, ys1, ys2, where, *, - vert=True, negpos=None, stack=None, stacked=None, **kwargs + self, + xs, + ys1, + ys2, + where, + *, + vert=True, + negpos=None, + stack=None, + stacked=None, + **kwargs, ): """ Apply area shading. """ # Parse input arguments kw = kwargs.copy() - kw.update(_pop_props(kw, 'patch')) + kw.update(_pop_props(kw, "patch")) kw, extents = self._inbounds_extent(**kw) - name = 'fill_between' if vert else 'fill_betweenx' + name = "fill_between" if vert else "fill_betweenx" stack = _not_none(stack=stack, stacked=stacked) xs, ys1, ys2, kw = self._parse_1d_args(xs, ys1, ys2, vert=vert, **kw) edgefix_kw = _pop_params(kw, self._fix_patch_edges) @@ -3305,12 +3574,9 @@ def _apply_fill( # Draw guide and add sticky edges self._update_guide(objs, **guide_kw) - for axis, sides in zip('xy' if vert else 'yx', (xsides, ysides)): + for axis, sides in zip("xy" if vert else "yx", (xsides, ysides)): self._fix_sticky_edges(objs, axis, *sides) - return ( - objs[0] if len(objs) == 1 - else cbook.silent_list('PolyCollection', objs) - ) + return objs[0] if len(objs) == 1 else cbook.silent_list("PolyCollection", objs) @docstring._snippet_manager def area(self, *args, **kwargs): @@ -3326,7 +3592,7 @@ def areax(self, *args, **kwargs): """ return self.fill_betweenx(*args, **kwargs) - @inputs._preprocess_or_redirect('x', 'y1', 'y2', 'where') + @inputs._preprocess_or_redirect("x", "y1", "y2", "where") @docstring._concatenate_inherited @docstring._snippet_manager def fill_between(self, *args, **kwargs): @@ -3336,7 +3602,7 @@ def fill_between(self, *args, **kwargs): kwargs = _parse_vert(default_vert=True, **kwargs) return self._apply_fill(*args, **kwargs) - @inputs._preprocess_or_redirect('y', 'x1', 'x2', 'where') + @inputs._preprocess_or_redirect("y", "x1", "x2", "where") @docstring._concatenate_inherited @docstring._snippet_manager def fill_betweenx(self, *args, **kwargs): @@ -3361,17 +3627,27 @@ def _convert_bar_width(x, width=1): x_step = x_test[1:] - x_test[:-1] x_step = np.concatenate((x_step, x_step[-1:])) elif x_test.dtype == np.datetime64: - x_step = np.timedelta64(1, 'D') + x_step = np.timedelta64(1, "D") else: x_step = np.array(0.5) if np.issubdtype(x_test.dtype, np.datetime64): # Avoid integer timedelta truncation - x_step = x_step.astype('timedelta64[ns]') + x_step = x_step.astype("timedelta64[ns]") return width * x_step def _apply_bar( - self, xs, hs, ws, bs, *, absolute_width=None, - stack=None, stacked=None, negpos=False, orientation='vertical', **kwargs + self, + xs, + hs, + ws, + bs, + *, + absolute_width=None, + stack=None, + stacked=None, + negpos=False, + orientation="vertical", + **kwargs, ): """ Apply bar or barh command. Support default "minima" at zero. @@ -3379,7 +3655,7 @@ def _apply_bar( # Parse args kw = kwargs.copy() kw, extents = self._inbounds_extent(**kw) - name = 'barh' if orientation == 'horizontal' else 'bar' + name = "barh" if orientation == "horizontal" else "bar" stack = _not_none(stack=stack, stacked=stacked) xs, hs, kw = self._parse_1d_args(xs, hs, orientation=orientation, **kw) edgefix_kw = _pop_params(kw, self._fix_patch_edges) @@ -3389,9 +3665,18 @@ def _apply_bar( # Call func after converting bar width b0 = 0 objs = [] - kw.update(_pop_props(kw, 'patch')) + kw.update(_pop_props(kw, "patch")) hs, kw = inputs._dist_reduce(hs, **kw) guide_kw = _pop_params(kw, self._update_guide) + alphas = kw.pop("alpha", None) + if alphas is None: + alphas = xs.size * [None] + elif isinstance(alphas, Number): + alphas = xs.size * [alphas] + elif len(alphas) != xs.size: + raise ValueError( + f"Received {len(alphas)} values for alpha but needed {xs.size}" + ) for i, n, x, h, w, b, kw in self._iter_arg_cols(xs, hs, ws, bs, **kw): kw = self._parse_cycle(n, **kw) # Adjust x or y coordinates for grouped and stacked bars @@ -3407,7 +3692,9 @@ def _apply_bar( o = 0.5 * (n - 1) # center coordinate x = x + w * (i - o) # += may cause integer/float casting issue # Draw simple bars - *eb, kw = self._add_error_bars(x, b + h, default_barstds=True, orientation=orientation, **kw) # noqa: E501 + *eb, kw = self._add_error_bars( + x, b + h, default_barstds=True, orientation=orientation, **kw + ) # noqa: E501 if negpos: obj = self._call_negpos(name, x, h, w, b, use_zero=True, **kw) else: @@ -3415,39 +3702,40 @@ def _apply_bar( self._fix_patch_edges(obj, **edgefix_kw, **kw) for y in (b, b + h): self._inbounds_xylim(extents, x, y, orientation=orientation) + + if alphas[i] is not None: + for child in obj.get_children(): + child.set_alpha(alphas[i]) objs.append((*eb, obj) if eb else obj) self._update_guide(objs, **guide_kw) - return ( - objs[0] if len(objs) == 1 - else cbook.silent_list('BarContainer', objs) - ) + return objs[0] if len(objs) == 1 else cbook.silent_list("BarContainer", objs) - @inputs._preprocess_or_redirect('x', 'height', 'width', 'bottom') + @inputs._preprocess_or_redirect("x", "height", "width", "bottom") @docstring._concatenate_inherited @docstring._snippet_manager def bar(self, *args, **kwargs): """ %(plot.bar)s """ - kwargs = _parse_vert(default_orientation='vertical', **kwargs) + kwargs = _parse_vert(default_orientation="vertical", **kwargs) return self._apply_bar(*args, **kwargs) # WARNING: Swap 'height' and 'width' here so that they are always relative # to the 'tall' axis. This lets people always pass 'width' as keyword - @inputs._preprocess_or_redirect('y', 'height', 'width', 'left') + @inputs._preprocess_or_redirect("y", "height", "width", "left") @docstring._concatenate_inherited @docstring._snippet_manager def barh(self, *args, **kwargs): """ %(plot.barh)s """ - kwargs = _parse_vert(default_orientation='horizontal', **kwargs) + kwargs = _parse_vert(default_orientation="horizontal", **kwargs) return self._apply_bar(*args, **kwargs) # WARNING: 'labels' and 'colors' no longer passed through `data` (seems like # extremely niche usage... `data` variables should be data-like) - @inputs._preprocess_or_redirect('x', 'explode') + @inputs._preprocess_or_redirect("x", "explode") @docstring._concatenate_inherited @docstring._snippet_manager def pie(self, x, explode, *, labelpad=None, labeldistance=None, **kwargs): @@ -3456,15 +3744,15 @@ def pie(self, x, explode, *, labelpad=None, labeldistance=None, **kwargs): """ kw = kwargs.copy() pad = _not_none(labeldistance=labeldistance, labelpad=labelpad, default=1.15) - wedge_kw = kw.pop('wedgeprops', None) or {} - wedge_kw.update(_pop_props(kw, 'patch')) + wedge_kw = kw.pop("wedgeprops", None) or {} + wedge_kw.update(_pop_props(kw, "patch")) edgefix_kw = _pop_params(kw, self._fix_patch_edges) _, x, kw = self._parse_1d_args( x, autox=False, autoy=False, autoreverse=False, **kw ) kw = self._parse_cycle(x.size, **kw) objs = self._call_native( - 'pie', x, explode, labeldistance=pad, wedgeprops=wedge_kw, **kw + "pie", x, explode, labeldistance=pad, wedgeprops=wedge_kw, **kw ) objs = tuple(cbook.silent_list(type(seq[0]).__name__, seq) for seq in objs) self._fix_patch_edges(objs[0], **edgefix_kw, **wedge_kw) @@ -3477,57 +3765,75 @@ def _parse_box_violin(fillcolor, fillalpha, edgecolor, **kw): """ if isinstance(fillcolor, list): warnings._warn_proplot( - 'Passing lists to fillcolor was deprecated in v0.9. Please use ' - f'the property cycler with e.g. cycle={fillcolor!r} instead.' + "Passing lists to fillcolor was deprecated in v0.9. Please use " + f"the property cycler with e.g. cycle={fillcolor!r} instead." ) - kw['cycle'] = _not_none(cycle=kw.get('cycle', None), fillcolor=fillcolor) + kw["cycle"] = _not_none(cycle=kw.get("cycle", None), fillcolor=fillcolor) fillcolor = None if isinstance(fillalpha, list): warnings._warn_proplot( - 'Passing lists to fillalpha was removed in v0.9. Please specify ' - 'different opacities using the property cycle colors instead.' + "Passing lists to fillalpha was removed in v0.9. Please specify " + "different opacities using the property cycle colors instead." ) fillalpha = fillalpha[0] # too complicated to try to apply this if isinstance(edgecolor, list): warnings._warn_proplot( - 'Passing lists of edgecolors was removed in v0.9. Please call the ' - 'plotting command multiple times with different edge colors instead.' + "Passing lists of edgecolors was removed in v0.9. Please call the " + "plotting command multiple times with different edge colors instead." ) edgecolor = edgecolor[0] return fillcolor, fillalpha, edgecolor, kw def _apply_boxplot( - self, x, y, *, mean=None, means=None, vert=True, - fill=None, filled=None, marker=None, markersize=None, **kwargs + self, + x, + y, + *, + mean=None, + means=None, + vert=True, + fill=None, + filled=None, + marker=None, + markersize=None, + **kwargs, ): """ Apply the box plot. """ # Global and fill properties kw = kwargs.copy() - kw.update(_pop_props(kw, 'patch')) + kw.update(_pop_props(kw, "patch")) fill = _not_none(fill=fill, filled=filled) - means = _not_none(mean=mean, means=means, showmeans=kw.get('showmeans')) - linewidth = kw.pop('linewidth', rc['patch.linewidth']) - edgecolor = kw.pop('edgecolor', 'black') - fillcolor = kw.pop('facecolor', None) - fillalpha = kw.pop('alpha', None) + means = _not_none(mean=mean, means=means, showmeans=kw.get("showmeans")) + linewidth = kw.pop("linewidth", rc["patch.linewidth"]) + edgecolor = kw.pop("edgecolor", "black") + fillcolor = kw.pop("facecolor", None) + fillalpha = kw.pop("alpha", None) fillcolor, fillalpha, edgecolor, kw = self._parse_box_violin( fillcolor, fillalpha, edgecolor, **kw ) if fill is None: fill = fillcolor is not None or fillalpha is not None - fill = fill or kw.get('cycle') is not None + fill = fill or kw.get("cycle") is not None # Parse non-color properties # NOTE: Output dict keys are plural but we use singular for keyword args props = {} - for key in ('boxes', 'whiskers', 'caps', 'fliers', 'medians', 'means'): - prefix = key.rstrip('es') # singular form - props[key] = iprops = _pop_props(kw, 'line', prefix=prefix) - iprops.setdefault('color', edgecolor) - iprops.setdefault('linewidth', linewidth) - iprops.setdefault('markeredgecolor', edgecolor) + for key in ( + "boxes", + "whiskers", + "caps", + "fliers", + "medians", + "means", + "hatches", + ): + prefix = key.rstrip("es") # singular form + props[key] = iprops = _pop_props(kw, "line", prefix=prefix) + iprops.setdefault("color", edgecolor) + iprops.setdefault("linewidth", linewidth) + iprops.setdefault("markeredgecolor", edgecolor) # Parse color properties x, y, kw = self._parse_1d_args( @@ -3541,11 +3847,16 @@ def _apply_boxplot( fillcolor = [fillcolor] * x.size # Plot boxes - kw.setdefault('positions', x) + kw.setdefault("positions", x) if means: - kw['showmeans'] = kw['meanline'] = True + kw["showmeans"] = kw["meanline"] = True y = inputs._dist_clean(y) - artists = self._call_native('boxplot', y, vert=vert, **kw) + # Add hatch to props as boxplot does not have a hatch but Rectangle does + hatch = kw.pop("hatch", None) + if hatch is None: + hatch = [None for _ in range(x.size)] + + artists = self._call_native("boxplot", y, vert=vert, **kw) artists = artists or {} # necessary? artists = { key: cbook.silent_list(type(objs[0]).__name__, objs) if objs else objs @@ -3553,6 +3864,7 @@ def _apply_boxplot( } # Modify artist settings + for key, aprops in props.items(): if key not in artists: # possible if not rendered continue @@ -3562,7 +3874,7 @@ def _apply_boxplot( # TODO: Test this thoroughly! iprops = { key: ( - value[i // 2 if key in ('caps', 'whiskers') else i] + value[i // 2 if key in ("caps", "whiskers") else i] if isinstance(value, (list, np.ndarray)) else value ) @@ -3570,18 +3882,21 @@ def _apply_boxplot( } obj.update(iprops) # "Filled" boxplot by adding patch beneath line path - if key == 'boxes' and ( - fillcolor[i] is not None or fillalpha is not None + if key == "boxes" and ( + fillcolor[i] is not None + or fillalpha is not None + or hatch[i] is not None ): patch = mpatches.PathPatch( obj.get_path(), linewidth=0.0, facecolor=fillcolor[i], alpha=fillalpha, + hatch=hatch[i], ) self.add_artist(patch) # Outlier markers - if key == 'fliers': + if key == "fliers": if marker is not None: obj.set_marker(marker) if markersize is not None: @@ -3603,7 +3918,7 @@ def boxh(self, *args, **kwargs): """ return self.boxploth(*args, **kwargs) - @inputs._preprocess_or_redirect('positions', 'y') + @inputs._preprocess_or_redirect("positions", "y") @docstring._concatenate_inherited @docstring._snippet_manager def boxplot(self, *args, **kwargs): @@ -3613,7 +3928,7 @@ def boxplot(self, *args, **kwargs): kwargs = _parse_vert(default_vert=True, **kwargs) return self._apply_boxplot(*args, **kwargs) - @inputs._preprocess_or_redirect('positions', 'x') + @inputs._preprocess_or_redirect("positions", "x") @docstring._snippet_manager def boxploth(self, *args, **kwargs): """ @@ -3623,26 +3938,36 @@ def boxploth(self, *args, **kwargs): return self._apply_boxplot(*args, **kwargs) def _apply_violinplot( - self, x, y, vert=True, mean=None, means=None, median=None, medians=None, - showmeans=None, showmedians=None, showextrema=None, **kwargs + self, + x, + y, + vert=True, + mean=None, + means=None, + median=None, + medians=None, + showmeans=None, + showmedians=None, + showextrema=None, + **kwargs, ): """ Apply the violinplot. """ # Parse keyword args kw = kwargs.copy() - kw.update(_pop_props(kw, 'patch')) - kw.setdefault('capsize', 0) # caps are redundant for violin plots + kw.update(_pop_props(kw, "patch")) + kw.setdefault("capsize", 0) # caps are redundant for violin plots means = _not_none(mean=mean, means=means, showmeans=showmeans) medians = _not_none(median=median, medians=medians, showmedians=showmedians) if showextrema: - kw['default_barpctiles'] = True + kw["default_barpctiles"] = True if not means and not medians: medians = _not_none(medians, True) - linewidth = kw.pop('linewidth', None) - edgecolor = kw.pop('edgecolor', 'black') - fillcolor = kw.pop('facecolor', None) - fillalpha = kw.pop('alpha', None) + linewidth = kw.pop("linewidth", None) + edgecolor = kw.pop("edgecolor", "black") + fillcolor = kw.pop("facecolor", None) + fillalpha = kw.pop("alpha", None) fillcolor, fillalpha, edgecolor, kw = self._parse_box_violin( fillcolor, fillalpha, edgecolor, **kw ) @@ -3660,19 +3985,38 @@ def _apply_violinplot( # Plot violins y, kw = inputs._dist_reduce(y, means=means, medians=medians, **kw) - *eb, kw = self._add_error_bars(x, y, vert=vert, default_boxstds=True, default_marker=True, **kw) # noqa: E501 - kw.pop('labels', None) # already applied in _parse_1d_args - kw.setdefault('positions', x) # coordinates passed as keyword - y = _not_none(kw.pop('distribution'), y) # i.e. was reduced + *eb, kw = self._add_error_bars( + x, y, vert=vert, default_boxstds=True, default_marker=True, **kw + ) # noqa: E501 + kw.pop("labels", None) # already applied in _parse_1d_args + kw.setdefault("positions", x) # coordinates passed as keyword + y = _not_none(kw.pop("distribution"), y) # i.e. was reduced y = inputs._dist_clean(y) + + hatches = None + if "hatch" in kw: + hatches = kw.pop("hatch", None) + if "hatches" in kw: + hatches = kw.pop("hatches", None) + + if hatches is None: + hatches = len(y) * [None] + elif len(hatches) != len(y): + raise ValueError(f"Retrieved {len(hatches)} hatches but need {len(y)}") + artists = self._call_native( - 'violinplot', y, vert=vert, - showmeans=False, showmedians=False, showextrema=False, **kw + "violinplot", + y, + vert=vert, + showmeans=False, + showmedians=False, + showextrema=False, + **kw, ) # Modify body settings artists = artists or {} # necessary? - bodies = artists.pop('bodies', ()) # should be no other entries + bodies = artists.pop("bodies", ()) # should be no other entries if bodies: bodies = cbook.silent_list(type(bodies[0]).__name__, bodies) for i, body in enumerate(bodies): @@ -3685,6 +4029,8 @@ def _apply_violinplot( body.set_edgecolor(edgecolor) if linewidth is not None: body.set_linewidths(linewidth) + if hatches[i] is not None: + body.set_hatch(hatches[i]) return (bodies, *eb) if eb else bodies @docstring._snippet_manager @@ -3694,7 +4040,7 @@ def violin(self, *args, **kwargs): """ # WARNING: This disables use of 'violin' by users but # probably very few people use this anyway. - if getattr(self, '_internal_call', None): + if getattr(self, "_internal_call", None): return super().violin(*args, **kwargs) else: return self.violinplot(*args, **kwargs) @@ -3706,7 +4052,7 @@ def violinh(self, *args, **kwargs): """ return self.violinploth(*args, **kwargs) - @inputs._preprocess_or_redirect('positions', 'y') + @inputs._preprocess_or_redirect("positions", "y") @docstring._concatenate_inherited @docstring._snippet_manager def violinplot(self, *args, **kwargs): @@ -3716,7 +4062,7 @@ def violinplot(self, *args, **kwargs): kwargs = _parse_vert(default_vert=True, **kwargs) return self._apply_violinplot(*args, **kwargs) - @inputs._preprocess_or_redirect('positions', 'x') + @inputs._preprocess_or_redirect("positions", "x") @docstring._snippet_manager def violinploth(self, *args, **kwargs): """ @@ -3726,9 +4072,19 @@ def violinploth(self, *args, **kwargs): return self._apply_violinplot(*args, **kwargs) def _apply_hist( - self, xs, bins, *, - width=None, rwidth=None, stack=None, stacked=None, fill=None, filled=None, - histtype=None, orientation='vertical', **kwargs + self, + xs, + bins, + *, + width=None, + rwidth=None, + stack=None, + stacked=None, + fill=None, + filled=None, + histtype=None, + orientation="vertical", + **kwargs, ): """ Apply the histogram. @@ -3743,54 +4099,54 @@ def _apply_hist( fill = _not_none(fill=fill, filled=filled) stack = _not_none(stack=stack, stacked=stacked) if fill is not None: - histtype = _not_none(histtype, 'stepfilled' if fill else 'step') + histtype = _not_none(histtype, "stepfilled" if fill else "step") if stack is not None: - histtype = _not_none(histtype, 'barstacked' if stack else 'bar') - kw['bins'] = bins - kw['label'] = kw.pop('labels', None) # multiple labels are natively supported - kw['rwidth'] = _not_none(width=width, rwidth=rwidth) # latter is native - kw['histtype'] = histtype = _not_none(histtype, 'bar') - kw.update(_pop_props(kw, 'patch')) + histtype = _not_none(histtype, "barstacked" if stack else "bar") + kw["bins"] = bins + kw["label"] = kw.pop("labels", None) # multiple labels are natively supported + kw["rwidth"] = _not_none(width=width, rwidth=rwidth) # latter is native + kw["histtype"] = histtype = _not_none(histtype, "bar") + kw.update(_pop_props(kw, "patch")) edgefix_kw = _pop_params(kw, self._fix_patch_edges) guide_kw = _pop_params(kw, self._update_guide) n = xs.shape[1] if xs.ndim > 1 else 1 kw = self._parse_cycle(n, **kw) - obj = self._call_native('hist', xs, orientation=orientation, **kw) - if histtype.startswith('bar'): + obj = self._call_native("hist", xs, orientation=orientation, **kw) + if histtype.startswith("bar"): self._fix_patch_edges(obj[2], **edgefix_kw, **kw) # Revert to mpl < 3.3 behavior where silent_list was always returned for # non-bar-type histograms. Because consistency. res = obj[2] if type(res) is list: # 'step' histtype plots - res = cbook.silent_list('Polygon', res) + res = cbook.silent_list("Polygon", res) obj = (*obj[:2], res) else: for i, sub in enumerate(res): if type(sub) is list: - res[i] = cbook.silent_list('Polygon', sub) + res[i] = cbook.silent_list("Polygon", sub) self._update_guide(res, **guide_kw) return obj - @inputs._preprocess_or_redirect('x', 'bins', keywords='weights') + @inputs._preprocess_or_redirect("x", "bins", keywords="weights") @docstring._concatenate_inherited @docstring._snippet_manager def hist(self, *args, **kwargs): """ %(plot.hist)s """ - kwargs = _parse_vert(default_orientation='vertical', **kwargs) + kwargs = _parse_vert(default_orientation="vertical", **kwargs) return self._apply_hist(*args, **kwargs) - @inputs._preprocess_or_redirect('y', 'bins', keywords='weights') + @inputs._preprocess_or_redirect("y", "bins", keywords="weights") @docstring._snippet_manager def histh(self, *args, **kwargs): """ %(plot.histh)s """ - kwargs = _parse_vert(default_orientation='horizontal', **kwargs) + kwargs = _parse_vert(default_orientation="horizontal", **kwargs) return self._apply_hist(*args, **kwargs) - @inputs._preprocess_or_redirect('x', 'y', 'bins', keywords='weights') + @inputs._preprocess_or_redirect("x", "y", "bins", keywords="weights") @docstring._concatenate_inherited @docstring._snippet_manager def hist2d(self, x, y, bins, **kwargs): @@ -3799,11 +4155,11 @@ def hist2d(self, x, y, bins, **kwargs): """ # Rely on the pcolormesh() override for this. if bins is not None: - kwargs['bins'] = bins + kwargs["bins"] = bins return super().hist2d(x, y, autoreverse=False, default_discrete=False, **kwargs) # WARNING: breaking change from native 'C' - @inputs._preprocess_or_redirect('x', 'y', 'weights') + @inputs._preprocess_or_redirect("x", "y", "weights") @docstring._concatenate_inherited @docstring._snippet_manager def hexbin(self, x, y, weights, **kwargs): @@ -3814,22 +4170,20 @@ def hexbin(self, x, y, weights, **kwargs): # estimated. Inside _parse_level_vals if no manual levels were provided then # _parse_level_num is skipped and args like levels=10 or locator=5 are ignored kw = kwargs.copy() - x, y, kw = self._parse_1d_args( - x, y, autoreverse=False, autovalues=True, **kw - ) - kw.update(_pop_props(kw, 'collection')) # takes LineCollection props + x, y, kw = self._parse_1d_args(x, y, autoreverse=False, autovalues=True, **kw) + kw.update(_pop_props(kw, "collection")) # takes LineCollection props kw = self._parse_cmap(x, y, y, skip_autolev=True, default_discrete=False, **kw) - norm = kw.get('norm', None) + norm = kw.get("norm", None) if norm is not None and not isinstance(norm, pcolors.DiscreteNorm): norm.vmin = norm.vmax = None # remove nonsense values labels_kw = _pop_params(kw, self._add_auto_labels) guide_kw = _pop_params(kw, self._update_guide) - m = self._call_native('hexbin', x, y, weights, **kw) + m = self._call_native("hexbin", x, y, weights, **kw) self._add_auto_labels(m, **labels_kw) self._update_guide(m, queue_colorbar=False, **guide_kw) return m - @inputs._preprocess_or_redirect('x', 'y', 'z') + @inputs._preprocess_or_redirect("x", "y", "z") @docstring._concatenate_inherited @docstring._snippet_manager def contour(self, x, y, z, **kwargs): @@ -3837,20 +4191,20 @@ def contour(self, x, y, z, **kwargs): %(plot.contour)s """ x, y, z, kw = self._parse_2d_args(x, y, z, **kwargs) - kw.update(_pop_props(kw, 'collection')) + kw.update(_pop_props(kw, "collection")) kw = self._parse_cmap( x, y, z, min_levels=1, plot_lines=True, plot_contours=True, **kw ) labels_kw = _pop_params(kw, self._add_auto_labels) guide_kw = _pop_params(kw, self._update_guide) - label = kw.pop('label', None) - m = self._call_native('contour', x, y, z, **kw) + label = kw.pop("label", None) + m = self._call_native("contour", x, y, z, **kw) m._legend_label = label self._add_auto_labels(m, **labels_kw) self._update_guide(m, queue_colorbar=False, **guide_kw) return m - @inputs._preprocess_or_redirect('x', 'y', 'z') + @inputs._preprocess_or_redirect("x", "y", "z") @docstring._concatenate_inherited @docstring._snippet_manager def contourf(self, x, y, z, **kwargs): @@ -3858,23 +4212,23 @@ def contourf(self, x, y, z, **kwargs): %(plot.contourf)s """ x, y, z, kw = self._parse_2d_args(x, y, z, **kwargs) - kw.update(_pop_props(kw, 'collection')) + kw.update(_pop_props(kw, "collection")) kw = self._parse_cmap(x, y, z, plot_contours=True, **kw) - contour_kw = _pop_kwargs(kw, 'edgecolors', 'linewidths', 'linestyles') + contour_kw = _pop_kwargs(kw, "edgecolors", "linewidths", "linestyles") edgefix_kw = _pop_params(kw, self._fix_patch_edges) labels_kw = _pop_params(kw, self._add_auto_labels) guide_kw = _pop_params(kw, self._update_guide) - label = kw.pop('label', None) - m = cm = self._call_native('contourf', x, y, z, **kw) + label = kw.pop("label", None) + m = cm = self._call_native("contourf", x, y, z, **kw) m._legend_label = label self._fix_patch_edges(m, **edgefix_kw, **contour_kw) # no-op if not contour_kw if contour_kw or labels_kw: - cm = self._fix_contour_edges('contour', x, y, z, **kw, **contour_kw) + cm = self._fix_contour_edges("contour", x, y, z, **kw, **contour_kw) self._add_auto_labels(m, cm, **labels_kw) self._update_guide(m, queue_colorbar=False, **guide_kw) return m - @inputs._preprocess_or_redirect('x', 'y', 'z') + @inputs._preprocess_or_redirect("x", "y", "z") @docstring._concatenate_inherited @docstring._snippet_manager def pcolor(self, x, y, z, **kwargs): @@ -3882,19 +4236,19 @@ def pcolor(self, x, y, z, **kwargs): %(plot.pcolor)s """ x, y, z, kw = self._parse_2d_args(x, y, z, edges=True, **kwargs) - kw.update(_pop_props(kw, 'collection')) + kw.update(_pop_props(kw, "collection")) kw = self._parse_cmap(x, y, z, to_centers=True, **kw) edgefix_kw = _pop_params(kw, self._fix_patch_edges) labels_kw = _pop_params(kw, self._add_auto_labels) guide_kw = _pop_params(kw, self._update_guide) with self._keep_grid_bools(): - m = self._call_native('pcolor', x, y, z, **kw) + m = self._call_native("pcolor", x, y, z, **kw) self._fix_patch_edges(m, **edgefix_kw, **kw) self._add_auto_labels(m, **labels_kw) self._update_guide(m, queue_colorbar=False, **guide_kw) return m - @inputs._preprocess_or_redirect('x', 'y', 'z') + @inputs._preprocess_or_redirect("x", "y", "z") @docstring._concatenate_inherited @docstring._snippet_manager def pcolormesh(self, x, y, z, **kwargs): @@ -3902,19 +4256,19 @@ def pcolormesh(self, x, y, z, **kwargs): %(plot.pcolormesh)s """ x, y, z, kw = self._parse_2d_args(x, y, z, edges=True, **kwargs) - kw.update(_pop_props(kw, 'collection')) + kw.update(_pop_props(kw, "collection")) kw = self._parse_cmap(x, y, z, to_centers=True, **kw) edgefix_kw = _pop_params(kw, self._fix_patch_edges) labels_kw = _pop_params(kw, self._add_auto_labels) guide_kw = _pop_params(kw, self._update_guide) with self._keep_grid_bools(): - m = self._call_native('pcolormesh', x, y, z, **kw) + m = self._call_native("pcolormesh", x, y, z, **kw) self._fix_patch_edges(m, **edgefix_kw, **kw) self._add_auto_labels(m, **labels_kw) self._update_guide(m, queue_colorbar=False, **guide_kw) return m - @inputs._preprocess_or_redirect('x', 'y', 'z') + @inputs._preprocess_or_redirect("x", "y", "z") @docstring._concatenate_inherited @docstring._snippet_manager def pcolorfast(self, x, y, z, **kwargs): @@ -3922,21 +4276,21 @@ def pcolorfast(self, x, y, z, **kwargs): %(plot.pcolorfast)s """ x, y, z, kw = self._parse_2d_args(x, y, z, edges=True, **kwargs) - kw.update(_pop_props(kw, 'collection')) + kw.update(_pop_props(kw, "collection")) kw = self._parse_cmap(x, y, z, to_centers=True, **kw) edgefix_kw = _pop_params(kw, self._fix_patch_edges) labels_kw = _pop_params(kw, self._add_auto_labels) guide_kw = _pop_params(kw, self._update_guide) with self._keep_grid_bools(): - m = self._call_native('pcolorfast', x, y, z, **kw) + m = self._call_native("pcolorfast", x, y, z, **kw) if not isinstance(m, mimage.AxesImage): # NOTE: PcolorImage is derivative self._fix_patch_edges(m, **edgefix_kw, **kw) self._add_auto_labels(m, **labels_kw) elif edgefix_kw or labels_kw: kw = {**edgefix_kw, **labels_kw} warnings._warn_proplot( - f'Ignoring unused keyword argument(s): {kw}. These only work with ' - 'QuadMesh, not AxesImage. Consider using pcolor() or pcolormesh().' + f"Ignoring unused keyword argument(s): {kw}. These only work with " + "QuadMesh, not AxesImage. Consider using pcolor() or pcolormesh()." ) self._update_guide(m, queue_colorbar=False, **guide_kw) return m @@ -3947,70 +4301,74 @@ def heatmap(self, *args, aspect=None, **kwargs): %(plot.heatmap)s """ obj = self.pcolormesh(*args, default_discrete=False, **kwargs) - aspect = _not_none(aspect, rc['image.aspect']) - if self._name != 'cartesian': + aspect = _not_none(aspect, rc["image.aspect"]) + if self._name != "cartesian": warnings._warn_proplot( - 'The heatmap() command is meant for CartesianAxes ' - 'only. Please use pcolor() or pcolormesh() instead.' + "The heatmap() command is meant for CartesianAxes " + "only. Please use pcolor() or pcolormesh() instead." ) return obj - coords = getattr(obj, '_coordinates', None) + coords = getattr(obj, "_coordinates", None) xlocator = ylocator = None if coords is not None: coords = 0.5 * (coords[1:, ...] + coords[:-1, ...]) coords = 0.5 * (coords[:, 1:, :] + coords[:, :-1, :]) xlocator, ylocator = coords[0, :, 0], coords[:, 0, 1] - kw = {'aspect': aspect, 'xgrid': False, 'ygrid': False} + kw = {"aspect": aspect, "xgrid": False, "ygrid": False} if xlocator is not None and self.xaxis.isDefault_majloc: - kw['xlocator'] = xlocator + kw["xlocator"] = xlocator if ylocator is not None and self.yaxis.isDefault_majloc: - kw['ylocator'] = ylocator + kw["ylocator"] = ylocator if self.xaxis.isDefault_minloc: - kw['xtickminor'] = False + kw["xtickminor"] = False if self.yaxis.isDefault_minloc: - kw['ytickminor'] = False + kw["ytickminor"] = False self.format(**kw) return obj - @inputs._preprocess_or_redirect('x', 'y', 'u', 'v', ('c', 'color', 'colors')) + @inputs._preprocess_or_redirect("x", "y", "u", "v", ("c", "color", "colors")) @docstring._concatenate_inherited @docstring._snippet_manager def barbs(self, x, y, u, v, c, **kwargs): """ %(plot.barbs)s """ - x, y, u, v, kw = self._parse_2d_args(x, y, u, v, allow1d=True, autoguide=False, **kwargs) # noqa: E501 - kw.update(_pop_props(kw, 'line')) # applied to barbs + x, y, u, v, kw = self._parse_2d_args( + x, y, u, v, allow1d=True, autoguide=False, **kwargs + ) # noqa: E501 + kw.update(_pop_props(kw, "line")) # applied to barbs c, kw = self._parse_color(x, y, c, **kw) if mcolors.is_color_like(c): - kw['barbcolor'], c = c, None + kw["barbcolor"], c = c, None a = [x, y, u, v] if c is not None: a.append(c) - kw.pop('colorbar_kw', None) # added by _parse_cmap - m = self._call_native('barbs', *a, **kw) + kw.pop("colorbar_kw", None) # added by _parse_cmap + m = self._call_native("barbs", *a, **kw) return m - @inputs._preprocess_or_redirect('x', 'y', 'u', 'v', ('c', 'color', 'colors')) + @inputs._preprocess_or_redirect("x", "y", "u", "v", ("c", "color", "colors")) @docstring._concatenate_inherited @docstring._snippet_manager def quiver(self, x, y, u, v, c, **kwargs): """ %(plot.quiver)s """ - x, y, u, v, kw = self._parse_2d_args(x, y, u, v, allow1d=True, autoguide=False, **kwargs) # noqa: E501 - kw.update(_pop_props(kw, 'line')) # applied to arrow outline + x, y, u, v, kw = self._parse_2d_args( + x, y, u, v, allow1d=True, autoguide=False, **kwargs + ) # noqa: E501 + kw.update(_pop_props(kw, "line")) # applied to arrow outline c, kw = self._parse_color(x, y, c, **kw) color = None if mcolors.is_color_like(c): color, c = c, None if color is not None: - kw['color'] = color + kw["color"] = color a = [x, y, u, v] if c is not None: a.append(c) - kw.pop('colorbar_kw', None) # added by _parse_cmap - m = self._call_native('quiver', *a, **kw) + kw.pop("colorbar_kw", None) # added by _parse_cmap + m = self._call_native("quiver", *a, **kw) return m @docstring._snippet_manager @@ -4022,7 +4380,7 @@ def stream(self, *args, **kwargs): # WARNING: breaking change from native streamplot() fifth positional arg 'density' @inputs._preprocess_or_redirect( - 'x', 'y', 'u', 'v', ('c', 'color', 'colors'), keywords='start_points' + "x", "y", "u", "v", ("c", "color", "colors"), keywords="start_points" ) @docstring._concatenate_inherited @docstring._snippet_manager @@ -4031,19 +4389,19 @@ def streamplot(self, x, y, u, v, c, **kwargs): %(plot.stream)s """ x, y, u, v, kw = self._parse_2d_args(x, y, u, v, **kwargs) - kw.update(_pop_props(kw, 'line')) # applied to lines + kw.update(_pop_props(kw, "line")) # applied to lines c, kw = self._parse_color(x, y, c, **kw) if c is None: # throws an error if color not provided c = pcolors.to_hex(self._get_lines.get_next_color()) - kw['color'] = c # always pass this + kw["color"] = c # always pass this guide_kw = _pop_params(kw, self._update_guide) - label = kw.pop('label', None) - m = self._call_native('streamplot', x, y, u, v, **kw) + label = kw.pop("label", None) + m = self._call_native("streamplot", x, y, u, v, **kw) m.lines.set_label(label) # the collection label self._update_guide(m.lines, queue_colorbar=False, **guide_kw) # use lines return m - @inputs._preprocess_or_redirect('x', 'y', 'z') + @inputs._preprocess_or_redirect("x", "y", "z") @docstring._concatenate_inherited @docstring._snippet_manager def tricontour(self, x, y, z, **kwargs): @@ -4052,21 +4410,21 @@ def tricontour(self, x, y, z, **kwargs): """ kw = kwargs.copy() if x is None or y is None or z is None: - raise ValueError('Three input arguments are required.') - kw.update(_pop_props(kw, 'collection')) + raise ValueError("Three input arguments are required.") + kw.update(_pop_props(kw, "collection")) kw = self._parse_cmap( x, y, z, min_levels=1, plot_lines=True, plot_contours=True, **kw ) labels_kw = _pop_params(kw, self._add_auto_labels) guide_kw = _pop_params(kw, self._update_guide) - label = kw.pop('label', None) - m = self._call_native('tricontour', x, y, z, **kw) + label = kw.pop("label", None) + m = self._call_native("tricontour", x, y, z, **kw) m._legend_label = label self._add_auto_labels(m, **labels_kw) self._update_guide(m, queue_colorbar=False, **guide_kw) return m - @inputs._preprocess_or_redirect('x', 'y', 'z') + @inputs._preprocess_or_redirect("x", "y", "z") @docstring._concatenate_inherited @docstring._snippet_manager def tricontourf(self, x, y, z, **kwargs): @@ -4075,24 +4433,24 @@ def tricontourf(self, x, y, z, **kwargs): """ kw = kwargs.copy() if x is None or y is None or z is None: - raise ValueError('Three input arguments are required.') - kw.update(_pop_props(kw, 'collection')) - contour_kw = _pop_kwargs(kw, 'edgecolors', 'linewidths', 'linestyles') + raise ValueError("Three input arguments are required.") + kw.update(_pop_props(kw, "collection")) + contour_kw = _pop_kwargs(kw, "edgecolors", "linewidths", "linestyles") kw = self._parse_cmap(x, y, z, plot_contours=True, **kw) edgefix_kw = _pop_params(kw, self._fix_patch_edges) labels_kw = _pop_params(kw, self._add_auto_labels) guide_kw = _pop_params(kw, self._update_guide) - label = kw.pop('label', None) - m = cm = self._call_native('tricontourf', x, y, z, **kw) + label = kw.pop("label", None) + m = cm = self._call_native("tricontourf", x, y, z, **kw) m._legend_label = label self._fix_patch_edges(m, **edgefix_kw, **contour_kw) # no-op if not contour_kw if contour_kw or labels_kw: - cm = self._fix_contour_edges('tricontour', x, y, z, **kw, **contour_kw) + cm = self._fix_contour_edges("tricontour", x, y, z, **kw, **contour_kw) self._add_auto_labels(m, cm, **labels_kw) self._update_guide(m, queue_colorbar=False, **guide_kw) return m - @inputs._preprocess_or_redirect('x', 'y', 'z') + @inputs._preprocess_or_redirect("x", "y", "z") @docstring._concatenate_inherited @docstring._snippet_manager def tripcolor(self, x, y, z, **kwargs): @@ -4101,21 +4459,21 @@ def tripcolor(self, x, y, z, **kwargs): """ kw = kwargs.copy() if x is None or y is None or z is None: - raise ValueError('Three input arguments are required.') - kw.update(_pop_props(kw, 'collection')) + raise ValueError("Three input arguments are required.") + kw.update(_pop_props(kw, "collection")) kw = self._parse_cmap(x, y, z, **kw) edgefix_kw = _pop_params(kw, self._fix_patch_edges) labels_kw = _pop_params(kw, self._add_auto_labels) guide_kw = _pop_params(kw, self._update_guide) with self._keep_grid_bools(): - m = self._call_native('tripcolor', x, y, z, **kw) + m = self._call_native("tripcolor", x, y, z, **kw) self._fix_patch_edges(m, **edgefix_kw, **kw) self._add_auto_labels(m, **labels_kw) self._update_guide(m, queue_colorbar=False, **guide_kw) return m # WARNING: breaking change from native 'X' - @inputs._preprocess_or_redirect('z') + @inputs._preprocess_or_redirect("z") @docstring._concatenate_inherited @docstring._snippet_manager def imshow(self, z, **kwargs): @@ -4125,12 +4483,12 @@ def imshow(self, z, **kwargs): kw = kwargs.copy() kw = self._parse_cmap(z, default_discrete=False, **kw) guide_kw = _pop_params(kw, self._update_guide) - m = self._call_native('imshow', z, **kw) + m = self._call_native("imshow", z, **kw) self._update_guide(m, queue_colorbar=False, **guide_kw) return m # WARNING: breaking change from native 'Z' - @inputs._preprocess_or_redirect('z') + @inputs._preprocess_or_redirect("z") @docstring._concatenate_inherited @docstring._snippet_manager def matshow(self, z, **kwargs): @@ -4141,7 +4499,7 @@ def matshow(self, z, **kwargs): return super().matshow(z, **kwargs) # WARNING: breaking change from native 'Z' - @inputs._preprocess_or_redirect('z') + @inputs._preprocess_or_redirect("z") @docstring._concatenate_inherited @docstring._snippet_manager def spy(self, z, **kwargs): @@ -4149,11 +4507,11 @@ def spy(self, z, **kwargs): %(plot.spy)s """ kw = kwargs.copy() - kw.update(_pop_props(kw, 'line')) # takes valid Line2D properties - default_cmap = pcolors.DiscreteColormap(['w', 'k'], '_no_name') + kw.update(_pop_props(kw, "line")) # takes valid Line2D properties + default_cmap = pcolors.DiscreteColormap(["w", "k"], "_no_name") kw = self._parse_cmap(z, default_cmap=default_cmap, **kw) guide_kw = _pop_params(kw, self._update_guide) - m = self._call_native('spy', z, **kw) + m = self._call_native("spy", z, **kw) self._update_guide(m, queue_colorbar=False, **guide_kw) return m @@ -4178,20 +4536,20 @@ def _iter_arg_cols(self, *args, label=None, labels=None, values=None, **kwargs): """ Iterate over columns of positional arguments. """ - # Handle cycle args and label lists - # NOTE: Arrays here should have had metadata stripped by _parse_1d_args - # but could still be pint quantities that get processed by axis converter. - is_array = lambda data: hasattr(data, 'ndim') and hasattr(data, 'shape') # noqa: E731, E501 + is_array = lambda data: hasattr(data, "ndim") and hasattr(data, "shape") + + # Determine the number of columns n = max(1 if not is_array(a) or a.ndim < 2 else a.shape[-1] for a in args) + + # Handle labels labels = _not_none(label=label, values=values, labels=labels) if not np.iterable(labels) or isinstance(labels, str): labels = n * [labels] if len(labels) != n: - raise ValueError(f'Array has {n} columns but got {len(labels)} labels.') + raise ValueError(f"Array has {n} columns but got {len(labels)} labels.") if labels is not None: labels = [ - str(_not_none(label, '')) - for label in inputs._to_numpy_array(labels) + str(_not_none(label, "")) for label in inputs._to_numpy_array(labels) ] else: labels = n * [None] @@ -4199,7 +4557,7 @@ def _iter_arg_cols(self, *args, label=None, labels=None, values=None, **kwargs): # Yield successive columns for i in range(n): kw = kwargs.copy() - kw['label'] = labels[i] or None + kw["label"] = labels[i] or None a = tuple(a if not is_array(a) or a.ndim < 2 else a[..., i] for a in args) yield (i, n, *a, kw) @@ -4207,5 +4565,5 @@ def _iter_arg_cols(self, *args, label=None, labels=None, values=None, **kwargs): _level_parsers = (_parse_level_vals, _parse_level_num, _parse_level_lim) # Rename the shorthands - boxes = warnings._rename_objs('0.8.0', boxes=box) - violins = warnings._rename_objs('0.8.0', violins=violin) + boxes = warnings._rename_objs("0.8.0", boxes=box) + violins = warnings._rename_objs("0.8.0", violins=violin) diff --git a/proplot/axes/polar.py b/proplot/axes/polar.py index 9a6bd2bec..69437710d 100644 --- a/proplot/axes/polar.py +++ b/proplot/axes/polar.py @@ -14,7 +14,7 @@ from ..internals import _not_none, _pop_rc, docstring from . import plot, shared -__all__ = ['PolarAxes'] +__all__ = ["PolarAxes"] # Format docstring @@ -90,7 +90,7 @@ labelweight, gridlabelweight : str, default: :rc:`grid.labelweight` Font weight for the gridline labels. """ -docstring._snippet_manager['polar.format'] = _format_docstring +docstring._snippet_manager["polar.format"] = _format_docstring class PolarAxes(shared._SharedAxes, plot.PlotAxes, mpolar.PolarAxes): @@ -104,7 +104,8 @@ class PolarAxes(shared._SharedAxes, plot.PlotAxes, mpolar.PolarAxes): to axes-creation commands like `~proplot.figure.Figure.add_axes`, `~proplot.figure.Figure.add_subplot`, and `~proplot.figure.Figure.subplots`. """ - _name = 'polar' + + _name = "polar" @docstring._snippet_manager def __init__(self, *args, **kwargs): @@ -135,14 +136,14 @@ def __init__(self, *args, **kwargs): self.yaxis.set_major_formatter(pticker.AutoFormatter()) self.yaxis.isDefault_majfmt = True for axis in (self.xaxis, self.yaxis): - axis.set_tick_params(which='both', size=0) + axis.set_tick_params(which="both", size=0) def _update_formatter(self, x, *, formatter=None, formatter_kw=None): """ Update the gridline label formatter. """ # Tick formatter and toggling - axis = getattr(self, x + 'axis') + axis = getattr(self, x + "axis") formatter_kw = formatter_kw or {} if formatter is not None: formatter = constructor.Formatter(formatter, **formatter_kw) # noqa: E501 @@ -153,26 +154,31 @@ def _update_limits(self, x, *, min_=None, max_=None, lim=None): Update the limits. """ # Try to use public API where possible - r = 'theta' if x == 'x' else 'r' + r = "theta" if x == "x" else "r" min_, max_ = self._min_max_lim(r, min_, max_, lim) if min_ is not None: - getattr(self, f'set_{r}min')(min_) + getattr(self, f"set_{r}min")(min_) if max_ is not None: - getattr(self, f'set_{r}max')(max_) + getattr(self, f"set_{r}max")(max_) def _update_locators( - self, x, *, - locator=None, locator_kw=None, minorlocator=None, minorlocator_kw=None, + self, + x, + *, + locator=None, + locator_kw=None, + minorlocator=None, + minorlocator_kw=None, ): """ Update the gridline locator. """ # TODO: Add minor tick 'toggling' as with cartesian axes? # NOTE: Must convert theta locator input to radians, then back to deg. - r = 'theta' if x == 'x' else 'r' - axis = getattr(self, x + 'axis') - min_ = getattr(self, f'get_{r}min')() - max_ = getattr(self, f'get_{r}max')() + r = "theta" if x == "x" else "r" + axis = getattr(self, x + "axis") + min_ = getattr(self, f"get_{r}min")() + max_ = getattr(self, f"get_{r}max")() for i, (loc, loc_kw) in enumerate( zip((locator, minorlocator), (locator_kw, minorlocator_kw)) ): @@ -184,7 +190,7 @@ def _update_locators( # Sanitize values array = loc.tick_values(min_, max_) array = array[(array >= min_) & (array <= max_)] - if x == 'x': + if x == "x": array = np.deg2rad(array) if np.isclose(array[-1], min_ + 2 * np.pi): # exclusive if 360 deg array = array[:-1] @@ -197,21 +203,49 @@ def _update_locators( @docstring._snippet_manager def format( - self, *, r0=None, theta0=None, thetadir=None, - thetamin=None, thetamax=None, thetalim=None, - rmin=None, rmax=None, rlim=None, - thetagrid=None, rgrid=None, - thetagridminor=None, rgridminor=None, - thetagridcolor=None, rgridcolor=None, - rlabelpos=None, rscale=None, rborder=None, - thetalocator=None, rlocator=None, thetalines=None, rlines=None, - thetalocator_kw=None, rlocator_kw=None, - thetaminorlocator=None, rminorlocator=None, thetaminorlines=None, rminorlines=None, # noqa: E501 - thetaminorlocator_kw=None, rminorlocator_kw=None, - thetaformatter=None, rformatter=None, thetalabels=None, rlabels=None, - thetaformatter_kw=None, rformatter_kw=None, - labelpad=None, labelsize=None, labelcolor=None, labelweight=None, - **kwargs + self, + *, + r0=None, + theta0=None, + thetadir=None, + thetamin=None, + thetamax=None, + thetalim=None, + rmin=None, + rmax=None, + rlim=None, + thetagrid=None, + rgrid=None, + thetagridminor=None, + rgridminor=None, + thetagridcolor=None, + rgridcolor=None, + rlabelpos=None, + rscale=None, + rborder=None, + thetalocator=None, + rlocator=None, + thetalines=None, + rlines=None, + thetalocator_kw=None, + rlocator_kw=None, + thetaminorlocator=None, + rminorlocator=None, + thetaminorlines=None, + rminorlines=None, # noqa: E501 + thetaminorlocator_kw=None, + rminorlocator_kw=None, + thetaformatter=None, + rformatter=None, + thetalabels=None, + rlabels=None, + thetaformatter_kw=None, + rformatter_kw=None, + labelpad=None, + labelsize=None, + labelcolor=None, + labelweight=None, + **kwargs, ): """ Modify axes limits, radial and azimuthal gridlines, and more. Note that @@ -235,7 +269,7 @@ def format( # NOTE: Here we capture 'label.pad' rc argument normally used for # x and y axis labels as shorthand for 'tick.labelpad'. rc_kw, rc_mode = _pop_rc(kwargs) - labelcolor = _not_none(labelcolor, kwargs.get('color', None)) + labelcolor = _not_none(labelcolor, kwargs.get("color", None)) with rc.context(rc_kw, mode=rc_mode): # Not mutable default args thetalocator_kw = thetalocator_kw or {} @@ -247,11 +281,17 @@ def format( # Flexible input thetalocator = _not_none(thetalines=thetalines, thetalocator=thetalocator) - thetaformatter = _not_none(thetalabels=thetalabels, thetaformatter=thetaformatter) # noqa: E501 - thetaminorlocator = _not_none(thetaminorlines=thetaminorlines, thetaminorlocator=thetaminorlocator) # noqa: E501 + thetaformatter = _not_none( + thetalabels=thetalabels, thetaformatter=thetaformatter + ) # noqa: E501 + thetaminorlocator = _not_none( + thetaminorlines=thetaminorlines, thetaminorlocator=thetaminorlocator + ) # noqa: E501 rlocator = _not_none(rlines=rlines, rlocator=rlocator) rformatter = _not_none(rlabels=rlabels, rformatter=rformatter) - rminorlocator = _not_none(rminorlines=rminorlines, rminorlocator=rminorlocator) # noqa: E501 + rminorlocator = _not_none( + rminorlines=rminorlines, rminorlocator=rminorlocator + ) # noqa: E501 # Special radius settings if r0 is not None: @@ -261,7 +301,7 @@ def format( if rscale is not None: self.set_rscale(rscale) if rborder is not None: - self.spines['polar'].set_visible(bool(rborder)) + self.spines["polar"].set_visible(bool(rborder)) # Special azimuth settings if theta0 is not None: @@ -285,7 +325,7 @@ def format( minorlocator, minorlocator_kw, ) in zip( - ('x', 'y'), + ("x", "y"), (thetamin, rmin), (thetamax, rmax), (thetalim, rlim), @@ -306,15 +346,24 @@ def format( # NOTE: Here use 'grid.labelpad' instead of 'tick.labelpad'. Default # offset for grid labels is larger than for tick labels. self._update_ticks( - x, grid=grid, gridminor=gridminor, gridcolor=gridcolor, - gridpad=True, labelpad=labelpad, labelcolor=labelcolor, - labelsize=labelsize, labelweight=labelweight, + x, + grid=grid, + gridminor=gridminor, + gridcolor=gridcolor, + gridpad=True, + labelpad=labelpad, + labelcolor=labelcolor, + labelsize=labelsize, + labelweight=labelweight, ) # Axis locator self._update_locators( - x, locator=locator, locator_kw=locator_kw, - minorlocator=minorlocator, minorlocator_kw=minorlocator_kw + x, + locator=locator, + locator_kw=locator_kw, + minorlocator=minorlocator, + minorlocator_kw=minorlocator_kw, ) # Axis formatter diff --git a/proplot/axes/shared.py b/proplot/axes/shared.py index 2e15ffc11..98de18347 100644 --- a/proplot/axes/shared.py +++ b/proplot/axes/shared.py @@ -17,6 +17,7 @@ class _SharedAxes(object): Mix-in class with methods shared between `~proplot.axes.CartesianAxes` and `~proplot.axes.PolarAxes`. """ + @staticmethod def _min_max_lim(key, min_=None, max_=None, lim=None): """ @@ -25,14 +26,12 @@ def _min_max_lim(key, min_=None, max_=None, lim=None): if lim is None: lim = (None, None) if not np.iterable(lim) or not len(lim) == 2: - raise ValueError(f'Invalid {key}{lim!r}. Must be 2-tuple of values.') - min_ = _not_none(**{f'{key}min': min_, f'{key}lim_0': lim[0]}) - max_ = _not_none(**{f'{key}max': max_, f'{key}lim_1': lim[1]}) + raise ValueError(f"Invalid {key}{lim!r}. Must be 2-tuple of values.") + min_ = _not_none(**{f"{key}min": min_, f"{key}lim_0": lim[0]}) + max_ = _not_none(**{f"{key}max": max_, f"{key}lim_1": lim[1]}) return min_, max_ - def _update_background( - self, x=None, tickwidth=None, tickwidthratio=None, **kwargs - ): + def _update_background(self, x=None, tickwidth=None, tickwidthratio=None, **kwargs): """ Update the background patch and spines. """ @@ -41,110 +40,129 @@ def _update_background( self.patch.update(kw_face) if x is None: opts = self.spines - elif x == 'x': - opts = ('bottom', 'top', 'inner', 'polar') + elif x == "x": + opts = ("bottom", "top", "inner", "polar") else: - opts = ('left', 'right', 'start', 'end') + opts = ("left", "right", "start", "end") for opt in opts: self.spines.get(opt, {}).update(kw_edge) # Update the tick colors - axis = 'both' if x is None else x - x = _not_none(x, 'x') - obj = getattr(self, x + 'axis') - edgecolor = kw_edge.get('edgecolor', None) + axis = "both" if x is None else x + x = _not_none(x, "x") + obj = getattr(self, x + "axis") + edgecolor = kw_edge.get("edgecolor", None) if edgecolor is not None: - self.tick_params(axis=axis, which='both', color=edgecolor) + self.tick_params(axis=axis, which="both", color=edgecolor) # Update the tick widths # NOTE: Only use 'linewidth' if it was explicitly passed. Do not # include 'linewidth' inferred from rc['axes.linewidth'] setting. - kwmajor = getattr(obj, '_major_tick_kw', {}) # graceful fallback if API changes - kwminor = getattr(obj, '_minor_tick_kw', {}) - if 'linewidth' in kwargs: - tickwidth = _not_none(tickwidth, kwargs['linewidth']) - tickwidth = _not_none(tickwidth, rc.find('tick.width', context=True)) - tickwidthratio = _not_none(tickwidthratio, rc.find('tick.widthratio', context=True)) # noqa: E501 - tickwidth_prev = kwmajor.get('width', rc[x + 'tick.major.width']) + kwmajor = getattr(obj, "_major_tick_kw", {}) # graceful fallback if API changes + kwminor = getattr(obj, "_minor_tick_kw", {}) + if "linewidth" in kwargs: + tickwidth = _not_none(tickwidth, kwargs["linewidth"]) + tickwidth = _not_none(tickwidth, rc.find("tick.width", context=True)) + tickwidthratio = _not_none( + tickwidthratio, rc.find("tick.widthratio", context=True) + ) # noqa: E501 + tickwidth_prev = kwmajor.get("width", rc[x + "tick.major.width"]) if tickwidth_prev == 0: - tickwidthratio_prev = rc['tick.widthratio'] # no other way of knowing + tickwidthratio_prev = rc["tick.widthratio"] # no other way of knowing else: - tickwidthratio_prev = kwminor.get('width', rc[x + 'tick.minor.width']) / tickwidth_prev # noqa: E501 - for which in ('major', 'minor'): + tickwidthratio_prev = ( + kwminor.get("width", rc[x + "tick.minor.width"]) / tickwidth_prev + ) # noqa: E501 + for which in ("major", "minor"): kwticks = {} if tickwidth is not None or tickwidthratio is not None: tickwidth = _not_none(tickwidth, tickwidth_prev) - kwticks['width'] = tickwidth = units(tickwidth, 'pt') + kwticks["width"] = tickwidth = units(tickwidth, "pt") if tickwidth == 0: # avoid unnecessary padding - kwticks['size'] = 0 - elif which == 'minor': + kwticks["size"] = 0 + elif which == "minor": tickwidthratio = _not_none(tickwidthratio, tickwidthratio_prev) - kwticks['width'] *= tickwidthratio + kwticks["width"] *= tickwidthratio self.tick_params(axis=axis, which=which, **kwticks) def _update_ticks( - self, x, *, grid=None, gridminor=None, gridpad=None, gridcolor=None, - ticklen=None, ticklenratio=None, tickdir=None, tickcolor=None, - labeldir=None, labelpad=None, labelcolor=None, labelsize=None, labelweight=None, + self, + x, + *, + grid=None, + gridminor=None, + gridpad=None, + gridcolor=None, + ticklen=None, + ticklenratio=None, + tickdir=None, + tickcolor=None, + labeldir=None, + labelpad=None, + labelcolor=None, + labelsize=None, + labelweight=None, ): """ Update the gridlines and labels. Set `gridpad` to ``True`` to use grid padding. """ # Filter out text properties - axis = 'both' if x is None else x + axis = "both" if x is None else x kwtext = rc._get_ticklabel_props(axis) - kwtext_extra = _pop_kwargs(kwtext, 'weight', 'family') - kwtext = {'label' + key: value for key, value in kwtext.items()} + kwtext_extra = _pop_kwargs(kwtext, "weight", "family") + kwtext = {"label" + key: value for key, value in kwtext.items()} if labelcolor is not None: - kwtext['labelcolor'] = labelcolor + kwtext["labelcolor"] = labelcolor if labelsize is not None: - kwtext['labelsize'] = labelsize + kwtext["labelsize"] = labelsize if labelweight is not None: - kwtext_extra['weight'] = labelweight + kwtext_extra["weight"] = labelweight # Apply tick settings with tick_params when possible - x = _not_none(x, 'x') - obj = getattr(self, x + 'axis') - kwmajor = getattr(obj, '_major_tick_kw', {}) # graceful fallback if API changes - kwminor = getattr(obj, '_minor_tick_kw', {}) - ticklen_prev = kwmajor.get('size', rc[x + 'tick.major.size']) + x = _not_none(x, "x") + obj = getattr(self, x + "axis") + kwmajor = getattr(obj, "_major_tick_kw", {}) # graceful fallback if API changes + kwminor = getattr(obj, "_minor_tick_kw", {}) + ticklen_prev = kwmajor.get("size", rc[x + "tick.major.size"]) if ticklen_prev == 0: - ticklenratio_prev = rc['tick.lenratio'] # no other way of knowing + ticklenratio_prev = rc["tick.lenratio"] # no other way of knowing else: - ticklenratio_prev = kwminor.get('size', rc[x + 'tick.minor.size']) / ticklen_prev # noqa: E501 - for b, which in zip((grid, gridminor), ('major', 'minor')): + ticklenratio_prev = ( + kwminor.get("size", rc[x + "tick.minor.size"]) / ticklen_prev + ) # noqa: E501 + for b, which in zip((grid, gridminor), ("major", "minor")): # Tick properties # NOTE: Must make 'tickcolor' overwrite 'labelcolor' or else 'color' # passed to __init__ will not apply correctly. Annoying but unavoidable kwticks = rc._get_tickline_props(axis, which=which) if labelpad is not None: - kwticks['pad'] = labelpad + kwticks["pad"] = labelpad if tickcolor is not None: - kwticks['color'] = tickcolor + kwticks["color"] = tickcolor if ticklen is not None or ticklenratio is not None: ticklen = _not_none(ticklen, ticklen_prev) - kwticks['size'] = ticklen = units(ticklen, 'pt') - if ticklen > 0 and which == 'minor': + kwticks["size"] = ticklen = units(ticklen, "pt") + if ticklen > 0 and which == "minor": ticklenratio = _not_none(ticklenratio, ticklenratio_prev) - kwticks['size'] *= ticklenratio + kwticks["size"] *= ticklenratio if gridpad: # use grid.labelpad instead of tick.labelpad - kwticks.pop('pad', None) - pad = rc.find('grid.labelpad', context=True) + kwticks.pop("pad", None) + pad = rc.find("grid.labelpad", context=True) if pad is not None: - kwticks['pad'] = units(pad, 'pt') + kwticks["pad"] = units(pad, "pt") # Tick direction properties # NOTE: These have no x and y-specific versions but apply here anyway - if labeldir == 'in': # put tick labels inside the plot - tickdir = 'in' + if labeldir == "in": # put tick labels inside the plot + tickdir = "in" kwticks.setdefault( - 'pad', - - rc[f'{axis}tick.major.size'] - - _not_none(labelpad, rc[f'{axis}tick.major.pad']) - - _fontsize_to_pt(rc[f'{axis}tick.labelsize']) + "pad", + -rc[f"{axis}tick.major.size"] + - _not_none(labelpad, rc[f"{axis}tick.major.pad"]) + - _fontsize_to_pt(rc[f"{axis}tick.labelsize"]), ) if tickdir is not None: - kwticks['direction'] = tickdir + kwticks["direction"] = tickdir # Gridline properties # NOTE: Internally ax.grid() passes gridOn to ax.tick_params() but this @@ -153,12 +171,13 @@ def _update_ticks( if b is not None: self.grid(b, axis=axis, which=which) kwlines = rc._get_gridline_props(which=which) - if 'axisbelow' in kwlines: - self.set_axisbelow(kwlines.pop('axisbelow')) + if "axisbelow" in kwlines: + self.set_axisbelow(kwlines.pop("axisbelow")) if gridcolor is not None: - kwlines['grid_color'] = gridcolor + kwlines["grid_color"] = gridcolor # Apply tick and gridline properties + kwticks.pop("ndivs", None) # not in mpl self.tick_params(axis=axis, which=which, **kwticks, **kwlines, **kwtext) # Apply settings that can't be controlled with tick_params diff --git a/proplot/axes/three.py b/proplot/axes/three.py index 6ab45148a..07d74f056 100644 --- a/proplot/axes/three.py +++ b/proplot/axes/three.py @@ -21,12 +21,14 @@ class ThreeAxes(shared._SharedAxes, base.Axes, Axes3D): ``proj='three'`` to axes-creation commands like `~proplot.figure.Figure.add_axes`, `~proplot.figure.Figure.add_subplot`, and `~proplot.figure.Figure.subplots`. """ + # TODO: Figure out a way to have internal Axes3D calls to plotting commands # access the overrides rather than the originals? May be impossible. - _name = 'three' - _name_aliases = ('3d',) + _name = "three" + _name_aliases = ("3d",) def __init__(self, *args, **kwargs): import mpl_toolkits.mplot3d # noqa: F401 verify package is available - kwargs.setdefault('alpha', 0.0) + + kwargs.setdefault("alpha", 0.0) super().__init__(*args, **kwargs) diff --git a/proplot/colors.py b/proplot/colors.py index 531d70f02..76ee11b15 100644 --- a/proplot/colors.py +++ b/proplot/colors.py @@ -24,6 +24,7 @@ from xml.etree import ElementTree import matplotlib.cm as mcm +import matplotlib as mpl import matplotlib.colors as mcolors import numpy as np import numpy.ma as ma @@ -41,135 +42,136 @@ from .utils import set_alpha, to_hex, to_rgb, to_rgba, to_xyz, to_xyza __all__ = [ - 'DiscreteColormap', - 'ContinuousColormap', - 'PerceptualColormap', - 'DiscreteNorm', - 'DivergingNorm', - 'SegmentedNorm', - 'ColorDatabase', - 'ColormapDatabase', - 'ListedColormap', # deprecated - 'LinearSegmentedColormap', # deprecated - 'PerceptuallyUniformColormap', # deprecated - 'LinearSegmentedNorm', # deprecated + "DiscreteColormap", + "ContinuousColormap", + "PerceptualColormap", + "DiscreteNorm", + "DivergingNorm", + "SegmentedNorm", + "ColorDatabase", + "ColormapDatabase", + "ListedColormap", # deprecated + "LinearSegmentedColormap", # deprecated + "PerceptuallyUniformColormap", # deprecated + "LinearSegmentedNorm", # deprecated ] # Default colormap properties -DEFAULT_NAME = '_no_name' -DEFAULT_SPACE = 'hsl' +DEFAULT_NAME = "_no_name" +DEFAULT_SPACE = "hsl" # Color regexes # NOTE: We do not compile hex regex because config.py needs this surrounded by \A\Z -_regex_hex = r'#(?:[0-9a-fA-F]{3,4}){2}' # 6-8 digit hex +_regex_hex = r"#(?:[0-9a-fA-F]{3,4}){2}" # 6-8 digit hex REGEX_HEX_MULTI = re.compile(_regex_hex) -REGEX_HEX_SINGLE = re.compile(rf'\A{_regex_hex}\Z') -REGEX_ADJUST = re.compile(r'\A(light|dark|medium|pale|charcoal)?\s*(gr[ea]y[0-9]?)?\Z') +REGEX_HEX_SINGLE = re.compile(rf"\A{_regex_hex}\Z") +REGEX_ADJUST = re.compile(r"\A(light|dark|medium|pale|charcoal)?\s*(gr[ea]y[0-9]?)?\Z") # Colormap constants CMAPS_CYCLIC = tuple( # cyclic colormaps loaded from rgb files - key.lower() for key in ( - 'MonoCycle', - 'twilight', - 'Phase', - 'romaO', - 'brocO', - 'corkO', - 'vikO', - 'bamO', + key.lower() + for key in ( + "MonoCycle", + "twilight", + "Phase", + "romaO", + "brocO", + "corkO", + "vikO", + "bamO", ) ) CMAPS_DIVERGING = { # mirrored dictionary mapping for reversed names key.lower(): value.lower() for key1, key2 in ( - ('BR', 'RB'), - ('NegPos', 'PosNeg'), - ('CoolWarm', 'WarmCool'), - ('ColdHot', 'HotCold'), - ('DryWet', 'WetDry'), - ('PiYG', 'GYPi'), - ('PRGn', 'GnRP'), - ('BrBG', 'GBBr'), - ('PuOr', 'OrPu'), - ('RdGy', 'GyRd'), - ('RdBu', 'BuRd'), - ('RdYlBu', 'BuYlRd'), - ('RdYlGn', 'GnYlRd'), + ("BR", "RB"), + ("NegPos", "PosNeg"), + ("CoolWarm", "WarmCool"), + ("ColdHot", "HotCold"), + ("DryWet", "WetDry"), + ("PiYG", "GYPi"), + ("PRGn", "GnRP"), + ("BrBG", "GBBr"), + ("PuOr", "OrPu"), + ("RdGy", "GyRd"), + ("RdBu", "BuRd"), + ("RdYlBu", "BuYlRd"), + ("RdYlGn", "GnYlRd"), ) for key, value in ((key1, key2), (key2, key1)) } for _cmap_diverging in ( # remaining diverging cmaps (see PlotAxes._parse_cmap) - 'Div', - 'Vlag', - 'Spectral', - 'Balance', - 'Delta', - 'Curl', - 'roma', - 'broc', - 'cork', - 'vik', - 'bam', - 'lisbon', - 'tofino', - 'berlin', - 'vanimo', + "Div", + "Vlag", + "Spectral", + "Balance", + "Delta", + "Curl", + "roma", + "broc", + "cork", + "vik", + "bam", + "lisbon", + "tofino", + "berlin", + "vanimo", ): CMAPS_DIVERGING[_cmap_diverging.lower()] = _cmap_diverging.lower() CMAPS_REMOVED = { - 'Blue0': '0.6.0', - 'Cool': '0.6.0', - 'Warm': '0.6.0', - 'Hot': '0.6.0', - 'Floral': '0.6.0', - 'Contrast': '0.6.0', - 'Sharp': '0.6.0', - 'Viz': '0.6.0', + "Blue0": "0.6.0", + "Cool": "0.6.0", + "Warm": "0.6.0", + "Hot": "0.6.0", + "Floral": "0.6.0", + "Contrast": "0.6.0", + "Sharp": "0.6.0", + "Viz": "0.6.0", } CMAPS_RENAMED = { - 'GrayCycle': ('MonoCycle', '0.6.0'), - 'Blue1': ('Blues1', '0.7.0'), - 'Blue2': ('Blues2', '0.7.0'), - 'Blue3': ('Blues3', '0.7.0'), - 'Blue4': ('Blues4', '0.7.0'), - 'Blue5': ('Blues5', '0.7.0'), - 'Blue6': ('Blues6', '0.7.0'), - 'Blue7': ('Blues7', '0.7.0'), - 'Blue8': ('Blues8', '0.7.0'), - 'Blue9': ('Blues9', '0.7.0'), - 'Green1': ('Greens1', '0.7.0'), - 'Green2': ('Greens2', '0.7.0'), - 'Green3': ('Greens3', '0.7.0'), - 'Green4': ('Greens4', '0.7.0'), - 'Green5': ('Greens5', '0.7.0'), - 'Green6': ('Greens6', '0.7.0'), - 'Green7': ('Greens7', '0.7.0'), - 'Green8': ('Greens8', '0.7.0'), - 'Orange1': ('Yellows1', '0.7.0'), - 'Orange2': ('Yellows2', '0.7.0'), - 'Orange3': ('Yellows3', '0.7.0'), - 'Orange4': ('Oranges2', '0.7.0'), - 'Orange5': ('Oranges1', '0.7.0'), - 'Orange6': ('Oranges3', '0.7.0'), - 'Orange7': ('Oranges4', '0.7.0'), - 'Orange8': ('Yellows4', '0.7.0'), - 'Brown1': ('Browns1', '0.7.0'), - 'Brown2': ('Browns2', '0.7.0'), - 'Brown3': ('Browns3', '0.7.0'), - 'Brown4': ('Browns4', '0.7.0'), - 'Brown5': ('Browns5', '0.7.0'), - 'Brown6': ('Browns6', '0.7.0'), - 'Brown7': ('Browns7', '0.7.0'), - 'Brown8': ('Browns8', '0.7.0'), - 'Brown9': ('Browns9', '0.7.0'), - 'RedPurple1': ('Reds1', '0.7.0'), - 'RedPurple2': ('Reds2', '0.7.0'), - 'RedPurple3': ('Reds3', '0.7.0'), - 'RedPurple4': ('Reds4', '0.7.0'), - 'RedPurple5': ('Reds5', '0.7.0'), - 'RedPurple6': ('Purples1', '0.7.0'), - 'RedPurple7': ('Purples2', '0.7.0'), - 'RedPurple8': ('Purples3', '0.7.0'), + "GrayCycle": ("MonoCycle", "0.6.0"), + "Blue1": ("Blues1", "0.7.0"), + "Blue2": ("Blues2", "0.7.0"), + "Blue3": ("Blues3", "0.7.0"), + "Blue4": ("Blues4", "0.7.0"), + "Blue5": ("Blues5", "0.7.0"), + "Blue6": ("Blues6", "0.7.0"), + "Blue7": ("Blues7", "0.7.0"), + "Blue8": ("Blues8", "0.7.0"), + "Blue9": ("Blues9", "0.7.0"), + "Green1": ("Greens1", "0.7.0"), + "Green2": ("Greens2", "0.7.0"), + "Green3": ("Greens3", "0.7.0"), + "Green4": ("Greens4", "0.7.0"), + "Green5": ("Greens5", "0.7.0"), + "Green6": ("Greens6", "0.7.0"), + "Green7": ("Greens7", "0.7.0"), + "Green8": ("Greens8", "0.7.0"), + "Orange1": ("Yellows1", "0.7.0"), + "Orange2": ("Yellows2", "0.7.0"), + "Orange3": ("Yellows3", "0.7.0"), + "Orange4": ("Oranges2", "0.7.0"), + "Orange5": ("Oranges1", "0.7.0"), + "Orange6": ("Oranges3", "0.7.0"), + "Orange7": ("Oranges4", "0.7.0"), + "Orange8": ("Yellows4", "0.7.0"), + "Brown1": ("Browns1", "0.7.0"), + "Brown2": ("Browns2", "0.7.0"), + "Brown3": ("Browns3", "0.7.0"), + "Brown4": ("Browns4", "0.7.0"), + "Brown5": ("Browns5", "0.7.0"), + "Brown6": ("Browns6", "0.7.0"), + "Brown7": ("Browns7", "0.7.0"), + "Brown8": ("Browns8", "0.7.0"), + "Brown9": ("Browns9", "0.7.0"), + "RedPurple1": ("Reds1", "0.7.0"), + "RedPurple2": ("Reds2", "0.7.0"), + "RedPurple3": ("Reds3", "0.7.0"), + "RedPurple4": ("Reds4", "0.7.0"), + "RedPurple5": ("Reds5", "0.7.0"), + "RedPurple6": ("Purples1", "0.7.0"), + "RedPurple7": ("Purples2", "0.7.0"), + "RedPurple8": ("Purples3", "0.7.0"), } # Color constants @@ -177,94 +179,169 @@ COLORS_XKCD = {} # populated during register_colors COLORS_KEEP = ( *( # always load these XKCD colors regardless of settings - 'charcoal', 'tomato', 'burgundy', 'maroon', 'burgundy', 'lavendar', - 'taupe', 'sand', 'stone', 'earth', 'sand brown', 'sienna', - 'terracotta', 'moss', 'crimson', 'mauve', 'rose', 'teal', 'forest', - 'grass', 'sage', 'pine', 'vermillion', 'russet', 'cerise', 'avocado', - 'wine', 'brick', 'umber', 'mahogany', 'puce', 'grape', 'blurple', - 'cranberry', 'sand', 'aqua', 'jade', 'coral', 'olive', 'magenta', - 'turquoise', 'sea blue', 'royal blue', 'slate blue', 'slate grey', - 'baby blue', 'salmon', 'beige', 'peach', 'mustard', 'lime', 'indigo', - 'cornflower', 'marine', 'cloudy blue', 'tangerine', 'scarlet', 'navy', - 'cool grey', 'warm grey', 'chocolate', 'raspberry', 'denim', - 'gunmetal', 'midnight', 'chartreuse', 'ivory', 'khaki', 'plum', - 'silver', 'tan', 'wheat', 'buff', 'bisque', 'cerulean', + "charcoal", + "tomato", + "burgundy", + "maroon", + "burgundy", + "lavendar", + "taupe", + "sand", + "stone", + "earth", + "sand brown", + "sienna", + "terracotta", + "moss", + "crimson", + "mauve", + "rose", + "teal", + "forest", + "grass", + "sage", + "pine", + "vermillion", + "russet", + "cerise", + "avocado", + "wine", + "brick", + "umber", + "mahogany", + "puce", + "grape", + "blurple", + "cranberry", + "sand", + "aqua", + "jade", + "coral", + "olive", + "magenta", + "turquoise", + "sea blue", + "royal blue", + "slate blue", + "slate grey", + "baby blue", + "salmon", + "beige", + "peach", + "mustard", + "lime", + "indigo", + "cornflower", + "marine", + "cloudy blue", + "tangerine", + "scarlet", + "navy", + "cool grey", + "warm grey", + "chocolate", + "raspberry", + "denim", + "gunmetal", + "midnight", + "chartreuse", + "ivory", + "khaki", + "plum", + "silver", + "tan", + "wheat", + "buff", + "bisque", + "cerulean", ), *( # common combinations - 'red orange', 'yellow orange', 'yellow green', - 'blue green', 'blue violet', 'red violet', - 'bright red', # backwards compatibility + "red orange", + "yellow orange", + "yellow green", + "blue green", + "blue violet", + "red violet", + "bright red", # backwards compatibility ), *( # common names prefix + color for color in ( - 'red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', - 'brown', 'grey', 'gray', + "red", + "orange", + "yellow", + "green", + "blue", + "indigo", + "violet", + "brown", + "grey", + "gray", ) - for prefix in ('', 'light ', 'dark ', 'medium ', 'pale ') - ) + for prefix in ("", "light ", "dark ", "medium ", "pale ") + ), ) COLORS_REMOVE = ( # filter these out, let's try to be professional here... - 'shit', - 'poop', - 'poo', - 'pee', - 'piss', - 'puke', - 'vomit', - 'snot', - 'booger', - 'bile', - 'diarrhea', - 'icky', - 'sickly', + "shit", + "poop", + "poo", + "pee", + "piss", + "puke", + "vomit", + "snot", + "booger", + "bile", + "diarrhea", + "icky", + "sickly", ) COLORS_REPLACE = ( # prevent registering similar-sounding names # these can all be combined - ('/', ' '), # convert [color1]/[color2] to compound (e.g. grey/blue to grey blue) - ("'s", 's'), # robin's egg - ('egg blue', 'egg'), # robin's egg blue - ('grey', 'gray'), # 'Murica - ('ochre', 'ocher'), # ... - ('forrest', 'forest'), # ... - ('ocre', 'ocher'), # correct spelling - ('kelley', 'kelly'), # ... - ('reddish', 'red'), # remove [color]ish where it modifies the spelling of color - ('purplish', 'purple'), # ... - ('pinkish', 'pink'), - ('yellowish', 'yellow'), - ('bluish', 'blue'), - ('greyish', 'grey'), - ('ish', ''), # these are all [color]ish ('ish' substring appears nowhere else) - ('bluey', 'blue'), # remove [color]y trailing y - ('greeny', 'green'), # ... - ('reddy', 'red'), - ('pinky', 'pink'), - ('purply', 'purple'), - ('purpley', 'purple'), - ('yellowy', 'yellow'), - ('orangey', 'orange'), - ('browny', 'brown'), - ('minty', 'mint'), # now remove [object]y trailing y - ('grassy', 'grass'), # ... - ('mossy', 'moss'), - ('dusky', 'dusk'), - ('rusty', 'rust'), - ('muddy', 'mud'), - ('sandy', 'sand'), - ('leafy', 'leaf'), - ('dusty', 'dust'), - ('dirty', 'dirt'), - ('peachy', 'peach'), - ('stormy', 'storm'), - ('cloudy', 'cloud'), - ('grayblue', 'gray blue'), # separate merge compounds - ('bluegray', 'gray blue'), # ... - ('lightblue', 'light blue'), - ('yellowgreen', 'yellow green'), - ('yelloworange', 'yellow orange'), + ("/", " "), # convert [color1]/[color2] to compound (e.g. grey/blue to grey blue) + ("'s", "s"), # robin's egg + ("egg blue", "egg"), # robin's egg blue + ("grey", "gray"), # 'Murica + ("ochre", "ocher"), # ... + ("forrest", "forest"), # ... + ("ocre", "ocher"), # correct spelling + ("kelley", "kelly"), # ... + ("reddish", "red"), # remove [color]ish where it modifies the spelling of color + ("purplish", "purple"), # ... + ("pinkish", "pink"), + ("yellowish", "yellow"), + ("bluish", "blue"), + ("greyish", "grey"), + ("ish", ""), # these are all [color]ish ('ish' substring appears nowhere else) + ("bluey", "blue"), # remove [color]y trailing y + ("greeny", "green"), # ... + ("reddy", "red"), + ("pinky", "pink"), + ("purply", "purple"), + ("purpley", "purple"), + ("yellowy", "yellow"), + ("orangey", "orange"), + ("browny", "brown"), + ("minty", "mint"), # now remove [object]y trailing y + ("grassy", "grass"), # ... + ("mossy", "moss"), + ("dusky", "dusk"), + ("rusty", "rust"), + ("muddy", "mud"), + ("sandy", "sand"), + ("leafy", "leaf"), + ("dusty", "dust"), + ("dirty", "dirt"), + ("peachy", "peach"), + ("stormy", "storm"), + ("cloudy", "cloud"), + ("grayblue", "gray blue"), # separate merge compounds + ("bluegray", "gray blue"), # ... + ("lightblue", "light blue"), + ("yellowgreen", "yellow green"), + ("yelloworange", "yellow orange"), ) # Simple snippets @@ -312,13 +389,13 @@ ``len(colors) - 1``. Larger numbers indicate a slower transition, smaller numbers indicate a faster transition. """ -docstring._snippet_manager['colors.N'] = _N_docstring -docstring._snippet_manager['colors.alpha'] = _alpha_docstring -docstring._snippet_manager['colors.cyclic'] = _cyclic_docstring -docstring._snippet_manager['colors.gamma'] = _gamma_docstring -docstring._snippet_manager['colors.space'] = _space_docstring -docstring._snippet_manager['colors.ratios'] = _ratios_docstring -docstring._snippet_manager['colors.name'] = _name_docstring +docstring._snippet_manager["colors.N"] = _N_docstring +docstring._snippet_manager["colors.alpha"] = _alpha_docstring +docstring._snippet_manager["colors.cyclic"] = _cyclic_docstring +docstring._snippet_manager["colors.gamma"] = _gamma_docstring +docstring._snippet_manager["colors.space"] = _space_docstring +docstring._snippet_manager["colors.ratios"] = _ratios_docstring +docstring._snippet_manager["colors.name"] = _name_docstring # List classmethod snippets _from_list_docstring = """ @@ -336,7 +413,7 @@ creates a colormap with the transition from red to blue taking *twice as long* as the transition from blue to green. """ -docstring._snippet_manager['colors.from_list'] = _from_list_docstring +docstring._snippet_manager["colors.from_list"] = _from_list_docstring def _clip_colors(colors, clip=True, gray=0.2, warn=False): @@ -365,14 +442,14 @@ def _clip_colors(colors, clip=True, gray=0.2, warn=False): else: colors[under | over] = gray if warn: - msg = 'Clipped' if clip else 'Invalid' - for i, name in enumerate('rgb'): + msg = "Clipped" if clip else "Invalid" + for i, name in enumerate("rgb"): if np.any(under[:, i]) or np.any(over[:, i]): - warnings._warn_proplot(f'{msg} {name!r} channel.') + warnings._warn_proplot(f"{msg} {name!r} channel.") return colors -def _get_channel(color, channel, space='hcl'): +def _get_channel(color, channel, space="hcl"): """ Get the hue, saturation, or luminance channel value from the input color. The color name `color` can optionally be a string with the format ``'color+x'`` @@ -395,21 +472,21 @@ def _get_channel(color, channel, space='hcl'): # Interpret channel if callable(color) or isinstance(color, Number): return color - if channel == 'hue': + if channel == "hue": channel = 0 - elif channel in ('chroma', 'saturation'): + elif channel in ("chroma", "saturation"): channel = 1 - elif channel == 'luminance': + elif channel == "luminance": channel = 2 else: - raise ValueError(f'Unknown channel {channel!r}.') + raise ValueError(f"Unknown channel {channel!r}.") # Interpret string or RGB tuple offset = 0 if isinstance(color, str): - m = re.search('([-+][0-9.]+)$', color) + m = re.search("([-+][0-9.]+)$", color) if m: offset = float(m.group(0)) - color = color[:m.start()] + color = color[: m.start()] return offset + to_xyz(color, space)[channel] @@ -437,24 +514,21 @@ def _make_segment_data(values, coords=None, ratios=None): # Get coordinates if not np.iterable(values): - raise TypeError('Colors must be iterable, got {values!r}.') + raise TypeError("Colors must be iterable, got {values!r}.") if coords is not None: coords = np.atleast_1d(coords) if ratios is not None: warnings._warn_proplot( - f'Segment coordinates were provided, ignoring ' - f'ratios={ratios!r}.' + f"Segment coordinates were provided, ignoring " f"ratios={ratios!r}." ) if len(coords) != len(values) or coords[0] != 0 or coords[-1] != 1: - raise ValueError( - f'Coordinates must range from 0 to 1, got {coords!r}.' - ) + raise ValueError(f"Coordinates must range from 0 to 1, got {coords!r}.") elif ratios is not None: coords = np.atleast_1d(ratios) if len(coords) != len(values) - 1: raise ValueError( - f'Need {len(values) - 1} ratios for {len(values)} colors, ' - f'but got {len(coords)} ratios.' + f"Need {len(values) - 1} ratios for {len(values)} colors, " + f"but got {len(coords)} ratios." ) coords = np.concatenate(([0], np.cumsum(coords))) coords = coords / np.max(coords) # normalize to 0-1 @@ -512,11 +586,11 @@ def _make_lookup_table(N, data, gamma=1.0, inverse=False): # Allow for *callable* instead of linearly interpolating between segments gammas = np.atleast_1d(gamma) if np.any(gammas < 0.01) or np.any(gammas > 10): - raise ValueError('Gamma can only be in range [0.01,10].') + raise ValueError("Gamma can only be in range [0.01,10].") if callable(data): if len(gammas) > 1: - raise ValueError('Only one gamma allowed for functional segmentdata.') - x = np.linspace(0, 1, N)**gamma + raise ValueError("Only one gamma allowed for functional segmentdata.") + x = np.linspace(0, 1, N) ** gamma lut = np.array(data(x), dtype=float) return lut @@ -524,9 +598,11 @@ def _make_lookup_table(N, data, gamma=1.0, inverse=False): data = np.array(data) shape = data.shape if len(shape) != 2 or shape[1] != 3: - raise ValueError('Mapping data must have shape N x 3.') + raise ValueError("Mapping data must have shape N x 3.") if len(gammas) != 1 and len(gammas) != shape[0] - 1: - raise ValueError(f'Expected {shape[0] - 1} gammas for {shape[0]} coords. Got {len(gamma)}.') # noqa: E501 + raise ValueError( + f"Expected {shape[0] - 1} gammas for {shape[0]} coords. Got {len(gamma)}." + ) # noqa: E501 if len(gammas) == 1: gammas = np.repeat(gammas, shape[:1]) @@ -535,9 +611,9 @@ def _make_lookup_table(N, data, gamma=1.0, inverse=False): y0 = data[:, 1] y1 = data[:, 2] if x[0] != 0.0 or x[-1] != 1.0: - raise ValueError('Data mapping points must start with x=0 and end with x=1.') + raise ValueError("Data mapping points must start with x=0 and end with x=1.") if np.any(np.diff(x) < 0): - raise ValueError('Data mapping points must have x in increasing order.') + raise ValueError("Data mapping points must have x in increasing order.") x = x * (N - 1) # Get distances from the segmentdata entry to the *left* for each requested @@ -564,9 +640,9 @@ def _make_lookup_table(N, data, gamma=1.0, inverse=False): if inverse: # reverse if we are transitioning to *higher* channel value reverse = not reverse if reverse: - offsets[ui:ui + ci] = 1 - (1 - offsets[ui:ui + ci]) ** gamma + offsets[ui : ui + ci] = 1 - (1 - offsets[ui : ui + ci]) ** gamma else: - offsets[ui:ui + ci] **= gamma + offsets[ui : ui + ci] **= gamma # Perform successive linear interpolations rolled up into one equation lut = np.zeros((N,), float) @@ -587,7 +663,7 @@ def _load_colors(path, warn_on_failure=True): """ # Warn or raise error (matches Colormap._from_file behavior) if not os.path.exists(path): - message = f'Failed to load color data file {path!r}. File not found.' + message = f"Failed to load color data file {path!r}. File not found." if warn_on_failure: warnings._warn_proplot(message) else: @@ -595,21 +671,20 @@ def _load_colors(path, warn_on_failure=True): # Iterate through lines loaded = {} - with open(path, 'r') as fh: + with open(path, "r") as fh: for count, line in enumerate(fh): stripped = line.strip() - if not stripped or stripped[0] == '#': + if not stripped or stripped[0] == "#": continue - pair = tuple(item.strip().lower() for item in line.split(':')) + pair = tuple(item.strip().lower() for item in line.split(":")) if len(pair) != 2 or not REGEX_HEX_SINGLE.match(pair[1]): warnings._warn_proplot( - f'Illegal line #{count + 1} in color file {path!r}:\n' - f'{line!r}\n' + f"Illegal line #{count + 1} in color file {path!r}:\n" + f"{line!r}\n" f'Lines must be formatted as "name: hexcolor".' ) continue loaded[pair[0]] = pair[1] - return loaded @@ -637,8 +712,8 @@ def _standardize_colors(input, space, margin): color = input.pop(name, None) if color is None: continue - if 'grey' in name: - name = name.replace('grey', 'gray') + if "grey" in name: + name = name.replace("grey", "gray") colors.append((name, color)) channels.append(to_xyz(color, space=space)) output[name] = color # required in case "kept" colors are close to each other @@ -675,6 +750,7 @@ class _Colormap(object): """ Mixin class used to add some helper methods. """ + def _get_data(self, ext, alpha=True): """ Return a string containing the colormap colors for saving. @@ -692,15 +768,15 @@ def _get_data(self, ext, alpha=True): colors = self._lut[:-3, :] # Get data string - if ext == 'hex': - data = ', '.join(mcolors.to_hex(color) for color in colors) - elif ext in ('txt', 'rgb'): + if ext == "hex": + data = ", ".join(mcolors.to_hex(color) for color in colors) + elif ext in ("txt", "rgb"): rgb = mcolors.to_rgba if alpha else mcolors.to_rgb data = [rgb(color) for color in colors] - data = '\n'.join(' '.join(f'{num:0.6f}' for num in line) for line in data) + data = "\n".join(" ".join(f"{num:0.6f}" for num in line) for line in data) else: raise ValueError( - f'Invalid extension {ext!r}. Options are: ' + f"Invalid extension {ext!r}. Options are: " "'hex', 'txt', 'rgb', 'rgba'." ) return data @@ -711,12 +787,12 @@ def _make_name(self, suffix=None): leading underscore or more than one identical suffix. """ name = self.name - name = name or '' - if name[:1] != '_': - name = '_' + name - suffix = suffix or 'copy' - suffix = '_' + suffix - if name[-len(suffix):] != suffix: + name = name or "" + if name[:1] != "_": + name = "_" + name + suffix = suffix or "copy" + suffix = "_" + suffix + if name[-len(suffix) :] != suffix: name = name + suffix return name @@ -736,14 +812,14 @@ def _parse_path(self, path, ext=None, subfolder=None): # Get the folder folder = rc.user_folder(subfolder=subfolder) if path is not None: - path = os.path.expanduser(path or '.') # interpret empty string as '.' + path = os.path.expanduser(path or ".") # interpret empty string as '.' if os.path.isdir(path): folder, path = path, None # Get the filename if path is None: path = os.path.join(folder, self.name) if not os.path.splitext(path)[1]: - path = path + '.' + ext # default file extension + path = path + "." + ext # default file extension return path @staticmethod @@ -756,7 +832,7 @@ def _pop_args(*args, names=None, **kwargs): names = names or () if isinstance(names, str): names = (names,) - names = ('name', *names) + names = ("name", *names) args, kwargs = _kwargs_to_args(names, *args, **kwargs) if args[0] is not None and args[1] is None: args[:2] = (None, args[0]) @@ -772,35 +848,36 @@ def _from_file(cls, path, warn_on_failure=False): path = os.path.expanduser(path) name, ext = os.path.splitext(os.path.basename(path)) listed = issubclass(cls, mcolors.ListedColormap) - reversed = name[-2:] == '_r' + reversed = name[-2:] == "_r" # Warn if loading failed during `register_cmaps` or `register_cycles` # but raise error if user tries to load a file. def _warn_or_raise(descrip, error=RuntimeError): - prefix = f'Failed to load colormap or color cycle file {path!r}.' + prefix = f"Failed to load colormap or color cycle file {path!r}." if warn_on_failure: - warnings._warn_proplot(prefix + ' ' + descrip) + warnings._warn_proplot(prefix + " " + descrip) else: - raise error(prefix + ' ' + descrip) + raise error(prefix + " " + descrip) + if not os.path.exists(path): - return _warn_or_raise('File not found.', FileNotFoundError) + return _warn_or_raise("File not found.", FileNotFoundError) # Directly read segmentdata json file # NOTE: This is special case! Immediately return name and cmap ext = ext[1:] - if ext == 'json': + if ext == "json": if listed: - return _warn_or_raise('Cannot load cycles from JSON files.') + return _warn_or_raise("Cannot load cycles from JSON files.") try: - with open(path, 'r') as fh: + with open(path, "r") as fh: data = json.load(fh) except json.JSONDecodeError: - return _warn_or_raise('JSON decoding error.', json.JSONDecodeError) + return _warn_or_raise("JSON decoding error.", json.JSONDecodeError) kw = {} - for key in ('cyclic', 'gamma', 'gamma1', 'gamma2', 'space'): + for key in ("cyclic", "gamma", "gamma1", "gamma2", "space"): if key in data: kw[key] = data.pop(key, None) - if 'red' in data: + if "red" in data: cmap = ContinuousColormap(name, data) else: cmap = PerceptualColormap(name, data, **kw) @@ -809,29 +886,29 @@ def _warn_or_raise(descrip, error=RuntimeError): return cmap # Read .rgb and .rgba files - if ext in ('txt', 'rgb'): + if ext in ("txt", "rgb"): # Load file # NOTE: This appears to be biggest import time bottleneck! Increases # time from 0.05s to 0.2s, with numpy loadtxt or with this regex thing. - delim = re.compile(r'[,\s]+') + delim = re.compile(r"[,\s]+") data = [ delim.split(line.strip()) for line in open(path) - if line.strip() and line.strip()[0] != '#' + if line.strip() and line.strip()[0] != "#" ] try: data = [[float(num) for num in line] for line in data] except ValueError: return _warn_or_raise( - 'Expected a table of comma or space-separated floats.' + "Expected a table of comma or space-separated floats." ) # Build x-coordinates and standardize shape data = np.array(data) if data.shape[1] not in (3, 4): return _warn_or_raise( - f'Expected 3 or 4 columns of floats. Got {data.shape[1]} columns.' + f"Expected 3 or 4 columns of floats. Got {data.shape[1]} columns." ) - if ext[0] != 'x': # i.e. no x-coordinates specified explicitly + if ext[0] != "x": # i.e. no x-coordinates specified explicitly x = np.linspace(0, 1, data.shape[0]) else: x, data = data[:, 0], data[:, 1:] @@ -839,43 +916,41 @@ def _warn_or_raise(descrip, error=RuntimeError): # Load XML files created with scivizcolor # Adapted from script found here: # https://sciviscolor.org/matlab-matplotlib-pv44/ - elif ext == 'xml': + elif ext == "xml": try: doc = ElementTree.parse(path) except ElementTree.ParseError: - return _warn_or_raise('XML parsing error.', ElementTree.ParseError) + return _warn_or_raise("XML parsing error.", ElementTree.ParseError) x, data = [], [] - for s in doc.getroot().findall('.//Point'): + for s in doc.getroot().findall(".//Point"): # Verify keys - if any(key not in s.attrib for key in 'xrgb'): + if any(key not in s.attrib for key in "xrgb"): return _warn_or_raise( - 'Missing an x, r, g, or b key inside one or more tags.' + "Missing an x, r, g, or b key inside one or more tags." ) # Get data color = [] - for key in 'rgbao': # o for opacity + for key in "rgbao": # o for opacity if key not in s.attrib: continue color.append(float(s.attrib[key])) - x.append(float(s.attrib['x'])) + x.append(float(s.attrib["x"])) data.append(color) # Convert to array if not all( len(data[0]) == len(color) and len(color) in (3, 4) for color in data ): return _warn_or_raise( - 'Unexpected channel number or mixed channels across tags.' + "Unexpected channel number or mixed channels across tags." ) # Read hex strings - elif ext == 'hex': + elif ext == "hex": # Read arbitrary format string = open(path).read() # into single string data = REGEX_HEX_MULTI.findall(string) if len(data) < 2: - return _warn_or_raise( - 'Failed to find 6-digit or 8-digit HEX strings.' - ) + return _warn_or_raise("Failed to find 6-digit or 8-digit HEX strings.") # Convert to array x = np.linspace(0, 1, len(data)) data = [to_rgb(color) for color in data] @@ -883,9 +958,9 @@ def _warn_or_raise(descrip, error=RuntimeError): # Invalid extension else: return _warn_or_raise( - 'Unknown colormap file extension {ext!r}. Options are: ' - + ', '.join(map(repr, ('json', 'txt', 'rgb', 'hex'))) - + '.' + "Unknown colormap file extension {ext!r}. Options are: " + + ", ".join(map(repr, ("json", "txt", "rgb", "hex"))) + + "." ) # Standardize and reverse if necessary to cmap @@ -911,23 +986,24 @@ class ContinuousColormap(mcolors.LinearSegmentedColormap, _Colormap): r""" Replacement for `~matplotlib.colors.LinearSegmentedColormap`. """ + def __str__(self): - return type(self).__name__ + f'(name={self.name!r})' + return type(self).__name__ + f"(name={self.name!r})" def __repr__(self): string = f" 'name': {self.name!r},\n" - if hasattr(self, '_space'): + if hasattr(self, "_space"): string += f" 'space': {self._space!r},\n" - if hasattr(self, '_cyclic'): + if hasattr(self, "_cyclic"): string += f" 'cyclic': {self._cyclic!r},\n" for key, data in self._segmentdata.items(): if callable(data): - string += f' {key!r}: ,\n' + string += f" {key!r}: ,\n" else: stop = data[-1][1] start = data[0][2] - string += f' {key!r}: [{start:.2f}, ..., {stop:.2f}],\n' - return type(self).__name__ + '({\n' + string + '})' + string += f" {key!r}: [{start:.2f}, ..., {stop:.2f}],\n" + return type(self).__name__ + "({\n" + string + "})" @docstring._snippet_manager def __init__(self, *args, gamma=1, alpha=None, cyclic=False, **kwargs): @@ -961,14 +1037,14 @@ def __init__(self, *args, gamma=1, alpha=None, cyclic=False, **kwargs): """ # NOTE: Additional keyword args should raise matplotlib error name, segmentdata, N, kwargs = self._pop_args( - *args, names=('segmentdata', 'N'), **kwargs + *args, names=("segmentdata", "N"), **kwargs ) if not isinstance(segmentdata, dict): - raise ValueError(f'Invalid segmentdata {segmentdata}. Must be a dict.') - N = _not_none(N, rc['image.lut']) - data = _pop_props(segmentdata, 'rgba', 'hsla') + raise ValueError(f"Invalid segmentdata {segmentdata}. Must be a dict.") + N = _not_none(N, rc["image.lut"]) + data = _pop_props(segmentdata, "rgba", "hsla") if segmentdata: - raise ValueError(f'Invalid segmentdata keys {tuple(segmentdata)}.') + raise ValueError(f"Invalid segmentdata keys {tuple(segmentdata)}.") super().__init__(name, data, N=N, gamma=gamma, **kwargs) self._cyclic = cyclic if alpha is not None: @@ -1015,11 +1091,11 @@ def append(self, *args, ratios=None, name=None, N=None, **kwargs): if not args: return self if not all(isinstance(cmap, mcolors.LinearSegmentedColormap) for cmap in args): - raise TypeError(f'Arguments {args!r} must be LinearSegmentedColormaps.') + raise TypeError(f"Arguments {args!r} must be LinearSegmentedColormaps.") # PerceptualColormap --> ContinuousColormap conversions cmaps = [self, *args] - spaces = {getattr(cmap, '_space', None) for cmap in cmaps} + spaces = {getattr(cmap, "_space", None) for cmap in cmaps} to_continuous = len(spaces) > 1 # mixed colorspaces *or* mixed types if to_continuous: for i, cmap in enumerate(cmaps): @@ -1030,7 +1106,7 @@ def append(self, *args, ratios=None, name=None, N=None, **kwargs): # we never interpolate between end colors of different colormaps segmentdata = {} if name is None: - name = '_' + '_'.join(cmap.name for cmap in cmaps) + name = "_" + "_".join(cmap.name for cmap in cmaps) if not np.iterable(ratios): ratios = [1] * len(cmaps) ratios = np.asarray(ratios) / np.sum(ratios) @@ -1043,6 +1119,7 @@ def append(self, *args, ratios=None, name=None, N=None, **kwargs): # embed 'funcs' into the definition using a keyword argument. datas = [cmap._segmentdata[key] for cmap in cmaps] if all(map(callable, datas)): # expand range from x-to-w to 0-1 + def xyy(ix, funcs=datas): # noqa: E306 ix = np.atleast_1d(ix) kx = np.empty(ix.shape) @@ -1068,23 +1145,23 @@ def xyy(ix, funcs=datas): # noqa: E306 else: raise TypeError( - 'Cannot merge colormaps with mixed callable ' - 'and non-callable segment data.' + "Cannot merge colormaps with mixed callable " + "and non-callable segment data." ) segmentdata[key] = xyy # Handle gamma values ikey = None - if key == 'saturation': - ikey = 'gamma1' - elif key == 'luminance': - ikey = 'gamma2' + if key == "saturation": + ikey = "gamma1" + elif key == "luminance": + ikey = "gamma2" if not ikey or ikey in kwargs: continue gamma = [] callable_ = all(map(callable, datas)) for cmap in cmaps: - igamma = getattr(cmap, '_' + ikey) + igamma = getattr(cmap, "_" + ikey) if not np.iterable(igamma): if callable_: igamma = (igamma,) @@ -1094,8 +1171,8 @@ def xyy(ix, funcs=datas): # noqa: E306 if callable_: if any(igamma != gamma[0] for igamma in gamma[1:]): warnings._warn_proplot( - 'Cannot use multiple segment gammas when concatenating ' - f'callable segments. Using the first gamma of {gamma[0]}.' + "Cannot use multiple segment gammas when concatenating " + f"callable segments. Using the first gamma of {gamma[0]}." ) gamma = gamma[0] kwargs[ikey] = gamma @@ -1153,7 +1230,7 @@ def cut(self, cut=None, name=None, left=None, right=None, **kwargs): # Decompose cut into two truncations followed by concatenation if 0.5 - offset < left or 0.5 + offset > right: - raise ValueError(f'Invalid cut={cut} for left={left} and right={right}.') + raise ValueError(f"Invalid cut={cut} for left={left} and right={right}.") if name is None: name = self._make_name() cmap_left = self.truncate(left, 0.5 - offset) @@ -1164,13 +1241,13 @@ def cut(self, cut=None, name=None, left=None, right=None, **kwargs): args = [] if cut < 0: ratio = 0.5 - 0.5 * abs(cut) # ratio for flanks on either side - space = getattr(self, '_space', None) or 'rgb' + space = getattr(self, "_space", None) or "rgb" xyza = to_xyza(self(0.5), space=space) segmentdata = { - key: _make_segment_data(x) for key, x in zip(space + 'a', xyza) + key: _make_segment_data(x) for key, x in zip(space + "a", xyza) } args.append(type(self)(DEFAULT_NAME, segmentdata, self.N)) - kwargs.setdefault('ratios', (ratio, abs(cut), ratio)) + kwargs.setdefault("ratios", (ratio, abs(cut), ratio)) args.append(cmap_right) return cmap_left.append(*args, name=name, **kwargs) @@ -1198,19 +1275,19 @@ def reversed(self, name=None, **kwargs): segmentdata = { key: ( (lambda x, func=data: func(x)) - if callable(data) else - [(1.0 - x, y1, y0) for x, y0, y1 in reversed(data)] + if callable(data) + else [(1.0 - x, y1, y0) for x, y0, y1 in reversed(data)] ) for key, data in self._segmentdata.items() } # Reverse gammas if name is None: - name = self._make_name(suffix='r') - for key in ('gamma1', 'gamma2'): + name = self._make_name(suffix="r") + for key in ("gamma1", "gamma2"): if key in kwargs: continue - gamma = getattr(self, '_' + key, None) + gamma = getattr(self, "_" + key, None) if gamma is not None and np.iterable(gamma): kwargs[key] = gamma[::-1] @@ -1247,32 +1324,32 @@ def save(self, path=None, alpha=True): # cmap.append() embeds functions as keyword arguments, this seems to make it # *impossible* to load back up the function with FunctionType (error message: # arg 5 (closure) must be tuple). Instead use this brute force workaround. - filename = self._parse_path(path, ext='json', subfolder='cmaps') + filename = self._parse_path(path, ext="json", subfolder="cmaps") _, ext = os.path.splitext(filename) - if ext[1:] != 'json': + if ext[1:] != "json": # Save lookup table colors data = self._get_data(ext[1:], alpha=alpha) - with open(filename, 'w') as fh: + with open(filename, "w") as fh: fh.write(data) else: # Save segment data itself data = {} for key, value in self._segmentdata.items(): if callable(value): - x = np.linspace(0, 1, rc['image.lut']) # just save the transitions + x = np.linspace(0, 1, rc["image.lut"]) # just save the transitions y = np.array([value(_) for _ in x]).squeeze() value = np.vstack((x, y, y)).T data[key] = np.asarray(value).astype(float).tolist() keys = () if isinstance(self, PerceptualColormap): - keys = ('cyclic', 'gamma1', 'gamma2', 'space') + keys = ("cyclic", "gamma1", "gamma2", "space") elif isinstance(self, ContinuousColormap): - keys = ('cyclic', 'gamma') + keys = ("cyclic", "gamma") for key in keys: # add all attrs to dictionary - data[key] = getattr(self, '_' + key) - with open(filename, 'w') as fh: + data[key] = getattr(self, "_" + key) + with open(filename, "w") as fh: json.dump(data, fh, indent=4) - print(f'Saved colormap to {filename!r}.') + print(f"Saved colormap to {filename!r}.") def set_alpha(self, alpha, coords=None, ratios=None): """ @@ -1298,7 +1375,7 @@ def set_alpha(self, alpha, coords=None, ratios=None): DiscreteColormap.set_alpha """ alpha = _make_segment_data(alpha, coords=coords, ratios=ratios) - self._segmentdata['alpha'] = alpha + self._segmentdata["alpha"] = alpha self._isinit = False def set_cyclic(self, b): @@ -1335,11 +1412,11 @@ def shifted(self, shift=180, name=None, **kwargs): if shift == 0: return self if name is None: - name = self._make_name(suffix='s') + name = self._make_name(suffix="s") if not self._cyclic: warnings._warn_proplot( - f'Shifting non-cyclic colormap {self.name!r}. To suppress this ' - 'warning use cmap.set_cyclic(True) or Colormap(..., cyclic=True).' + f"Shifting non-cyclic colormap {self.name!r}. To suppress this " + "warning use cmap.set_cyclic(True) or Colormap(..., cyclic=True)." ) self._cyclic = True ratios = (1 - shift, shift) @@ -1388,6 +1465,7 @@ def truncate(self, left=None, right=None, name=None, **kwargs): # the lambda function it gets overwritten in the loop! Must embed # the old callable in the new one as a default keyword arg. if callable(data): + def xyy(x, func=data): return func(left + x * (right - left)) @@ -1399,7 +1477,7 @@ def xyy(x, func=data): x = xyy[:, 0] l = np.searchsorted(x, left) # first x value > left # noqa r = np.searchsorted(x, right) - 1 # last x value < right - xc = xyy[l:r + 1, :].copy() + xc = xyy[l : r + 1, :].copy() xl = xyy[l - 1, 1:] + (left - x[l - 1]) * ( (xyy[l, 1:] - xyy[l - 1, 1:]) / (x[l] - x[l - 1]) ) @@ -1411,26 +1489,26 @@ def xyy(x, func=data): # Retain the corresponding gamma *segments* segmentdata[key] = xyy - if key == 'saturation': - ikey = 'gamma1' - elif key == 'luminance': - ikey = 'gamma2' + if key == "saturation": + ikey = "gamma1" + elif key == "luminance": + ikey = "gamma2" else: continue if ikey in kwargs: continue - gamma = getattr(self, '_' + ikey) + gamma = getattr(self, "_" + ikey) if np.iterable(gamma): if callable(xyy): if any(igamma != gamma[0] for igamma in gamma[1:]): warnings._warn_proplot( - 'Cannot use multiple segment gammas when ' - 'truncating colormap. Using the first gamma ' - f'of {gamma[0]}.' + "Cannot use multiple segment gammas when " + "truncating colormap. Using the first gamma " + f"of {gamma[0]}." ) gamma = gamma[0] else: - igamma = gamma[l - 1:r + 1] + igamma = gamma[l - 1 : r + 1] if len(igamma) == 0: # TODO: issue warning? gamma = gamma[0] else: @@ -1440,8 +1518,14 @@ def xyy(x, func=data): return self.copy(name, segmentdata, **kwargs) def copy( - self, name=None, segmentdata=None, N=None, *, - alpha=None, gamma=None, cyclic=None + self, + name=None, + segmentdata=None, + N=None, + *, + alpha=None, + gamma=None, + cyclic=None, ): """ Return a new colormap with relevant properties copied from this one @@ -1471,8 +1555,7 @@ def copy( if N is None: N = self.N cmap = ContinuousColormap( - name, segmentdata, N, - alpha=alpha, gamma=gamma, cyclic=cyclic + name, segmentdata, N, alpha=alpha, gamma=gamma, cyclic=cyclic ) cmap._rgba_bad = self._rgba_bad cmap._rgba_under = self._rgba_under @@ -1505,7 +1588,7 @@ def to_discrete(self, samples=10, name=None, **kwargs): if isinstance(samples, Integral): samples = np.linspace(0, 1, samples) elif not np.iterable(samples): - raise TypeError('Samples must be integer or iterable.') + raise TypeError("Samples must be integer or iterable.") samples = np.asarray(samples) colors = self(samples) if name is None: @@ -1562,11 +1645,11 @@ def from_list(cls, *args, **kwargs): """ # Get coordinates name, colors, ratios, kwargs = cls._pop_args( - *args, names=('colors', 'ratios'), **kwargs + *args, names=("colors", "ratios"), **kwargs ) coords = None if not np.iterable(colors): - raise TypeError('Colors must be iterable.') + raise TypeError("Colors must be iterable.") if ( np.iterable(colors[0]) and len(colors[0]) == 2 @@ -1576,19 +1659,16 @@ def from_list(cls, *args, **kwargs): colors = [to_rgba(color) for color in colors] # Build segmentdata - keys = ('red', 'green', 'blue', 'alpha') + keys = ("red", "green", "blue", "alpha") cdict = {} for key, values in zip(keys, zip(*colors)): cdict[key] = _make_segment_data(values, coords, ratios) return cls(name, cdict, **kwargs) # Deprecated - to_listed = warnings._rename_objs( - '0.8.0', - to_listed=to_discrete - ) + to_listed = warnings._rename_objs("0.8.0", to_listed=to_discrete) concatenate, punched, truncated, updated = warnings._rename_objs( - '0.6.0', + "0.6.0", concatenate=append, punched=cut, truncated=truncate, @@ -1600,15 +1680,16 @@ class DiscreteColormap(mcolors.ListedColormap, _Colormap): r""" Replacement for `~matplotlib.colors.ListedColormap`. """ + def __str__(self): - return f'DiscreteColormap(name={self.name!r})' + return f"DiscreteColormap(name={self.name!r})" def __repr__(self): colors = [c if isinstance(c, str) else to_hex(c) for c in self.colors] - string = 'DiscreteColormap({\n' + string = "DiscreteColormap({\n" string += f" 'name': {self.name!r},\n" string += f" 'colors': {colors!r},\n" - string += '})' + string += "})" return string def __init__(self, colors, name=None, N=None, alpha=None, **kwargs): @@ -1680,10 +1761,10 @@ def append(self, *args, name=None, N=None, **kwargs): if not args: return self if not all(isinstance(cmap, mcolors.ListedColormap) for cmap in args): - raise TypeError(f'Arguments {args!r} must be DiscreteColormap.') + raise TypeError(f"Arguments {args!r} must be DiscreteColormap.") cmaps = (self, *args) if name is None: - name = '_' + '_'.join(cmap.name for cmap in cmaps) + name = "_" + "_".join(cmap.name for cmap in cmaps) colors = [color for cmap in cmaps for color in cmap.colors] N = _not_none(N, len(colors)) return self.copy(colors, name, N, **kwargs) @@ -1711,12 +1792,12 @@ def save(self, path=None, alpha=True): -------- ContinuousColormap.save """ - filename = self._parse_path(path, ext='hex', subfolder='cycles') + filename = self._parse_path(path, ext="hex", subfolder="cycles") _, ext = os.path.splitext(filename) data = self._get_data(ext[1:], alpha=alpha) - with open(filename, 'w') as fh: + with open(filename, "w") as fh: fh.write(data) - print(f'Saved colormap to {filename!r}.') + print(f"Saved colormap to {filename!r}.") def set_alpha(self, alpha): """ @@ -1753,7 +1834,7 @@ def reversed(self, name=None, **kwargs): matplotlib.colors.ListedColormap.reversed """ if name is None: - name = self._make_name(suffix='r') + name = self._make_name(suffix="r") colors = self.colors[::-1] cmap = self.copy(colors, name, **kwargs) cmap._rgba_under, cmap._rgba_over = cmap._rgba_over, cmap._rgba_under @@ -1777,7 +1858,7 @@ def shifted(self, shift=1, name=None): if not shift: return self if name is None: - name = self._make_name(suffix='s') + name = self._make_name(suffix="s") shift = shift % len(self.colors) colors = list(self.colors) colors = colors[shift:] + colors[:shift] @@ -1864,7 +1945,7 @@ def from_file(cls, path, *, warn_on_failure=False): # Rename methods concatenate, truncated, updated = warnings._rename_objs( - '0.6.0', + "0.6.0", concatenate=append, truncated=truncate, updated=copy, @@ -1876,10 +1957,17 @@ class PerceptualColormap(ContinuousColormap): A `ContinuousColormap` with linear transitions across hue, saturation, and luminance rather than red, blue, and green. """ + @docstring._snippet_manager def __init__( - self, *args, space=None, clip=True, gamma=None, gamma1=None, gamma2=None, - **kwargs + self, + *args, + space=None, + clip=True, + gamma=None, + gamma1=None, + gamma2=None, + **kwargs, ): """ Parameters @@ -1929,14 +2017,14 @@ def __init__( """ # Checks name, segmentdata, N, kwargs = self._pop_args( - *args, names=('segmentdata', 'N'), **kwargs + *args, names=("segmentdata", "N"), **kwargs ) - data = _pop_props(segmentdata, 'hsla') + data = _pop_props(segmentdata, "hsla") if segmentdata: - raise ValueError(f'Invalid segmentdata keys {tuple(segmentdata)}.') + raise ValueError(f"Invalid segmentdata keys {tuple(segmentdata)}.") space = _not_none(space, DEFAULT_SPACE).lower() - if space not in ('rgb', 'hsv', 'hpl', 'hsl', 'hcl'): - raise ValueError(f'Unknown colorspace {space!r}.') + if space not in ("rgb", "hsv", "hpl", "hsl", "hcl"): + raise ValueError(f"Unknown colorspace {space!r}.") # Convert color strings to channel values for key, array in data.items(): if callable(array): # permit callable @@ -1959,7 +2047,7 @@ def _init(self): each value in the lookup table from ``self._space`` to RGB. """ # First generate the lookup table - channels = ('hue', 'saturation', 'luminance') + channels = ("hue", "saturation", "luminance") inverses = (False, False, True) # weight low chroma, high luminance gammas = (1.0, self._gamma1, self._gamma2) self._lut_hsl = np.ones((self.N + 3, 4), float) # fill @@ -1967,9 +2055,9 @@ def _init(self): self._lut_hsl[:-3, i] = _make_lookup_table( self.N, self._segmentdata[channel], gamma, inverse ) - if 'alpha' in self._segmentdata: + if "alpha" in self._segmentdata: self._lut_hsl[:-3, 3] = _make_lookup_table( - self.N, self._segmentdata['alpha'] + self.N, self._segmentdata["alpha"] ) self._lut_hsl[:-3, 0] %= 360 @@ -2001,9 +2089,18 @@ def set_gamma(self, gamma=None, gamma1=None, gamma2=None): self._init() def copy( - self, name=None, segmentdata=None, N=None, *, - alpha=None, gamma=None, cyclic=None, - clip=None, gamma1=None, gamma2=None, space=None + self, + name=None, + segmentdata=None, + N=None, + *, + alpha=None, + gamma=None, + cyclic=None, + clip=None, + gamma1=None, + gamma2=None, + space=None, ): """ Return a new colormap with relevant properties copied from this one @@ -2041,9 +2138,15 @@ def copy( if N is None: N = self.N cmap = PerceptualColormap( - name, segmentdata, N, - alpha=alpha, clip=clip, cyclic=cyclic, - gamma1=gamma1, gamma2=gamma2, space=space + name, + segmentdata, + N, + alpha=alpha, + clip=clip, + cyclic=cyclic, + gamma1=gamma1, + gamma2=gamma2, + space=space, ) cmap._rgba_bad = self._rgba_bad cmap._rgba_under = self._rgba_under @@ -2077,7 +2180,7 @@ def to_continuous(self, name=None, **kwargs): @classmethod @docstring._snippet_manager - @warnings._rename_kwargs('0.7.0', fade='saturation', shade='luminance') + @warnings._rename_kwargs("0.7.0", fade="saturation", shade="luminance") def from_color(cls, *args, **kwargs): """ Return a simple monochromatic "sequential" colormap that blends from white @@ -2117,22 +2220,24 @@ def from_color(cls, *args, **kwargs): PerceptualColormap.from_list """ name, color, space, kwargs = cls._pop_args( - *args, names=('color', 'space'), **kwargs + *args, names=("color", "space"), **kwargs ) space = _not_none(space, DEFAULT_SPACE).lower() - props = _pop_props(kwargs, 'hsla') - if props.get('hue', None) is not None: + props = _pop_props(kwargs, "hsla") + if props.get("hue", None) is not None: raise TypeError("from_color() got an unexpected keyword argument 'hue'") hue, saturation, luminance, alpha = to_xyza(color, space) - alpha_fade = props.pop('alpha', 1) - luminance_fade = props.pop('luminance', 100) - saturation_fade = props.pop('saturation', saturation) + alpha_fade = props.pop("alpha", 1) + luminance_fade = props.pop("luminance", 100) + saturation_fade = props.pop("saturation", saturation) return cls.from_hsl( - name, hue=hue, space=space, + name, + hue=hue, + space=space, alpha=(alpha_fade, alpha), saturation=(saturation_fade, saturation), luminance=(luminance_fade, luminance), - **kwargs + **kwargs, ) @classmethod @@ -2185,15 +2290,15 @@ def from_hsl(cls, *args, **kwargs): PerceptualColormap.from_list """ name, space, ratios, kwargs = cls._pop_args( - *args, names=('space', 'ratios'), **kwargs + *args, names=("space", "ratios"), **kwargs ) cdict = {} - props = _pop_props(kwargs, 'hsla') + props = _pop_props(kwargs, "hsla") for key, default in ( - ('hue', 0), - ('saturation', 100), - ('luminance', (100, 20)), - ('alpha', 1), + ("hue", 0), + ("saturation", 100), + ("luminance", (100, 20)), + ("alpha", 1), ): value = props.pop(key, default) cdict[key] = _make_segment_data(value, ratios=ratios) @@ -2234,20 +2339,21 @@ def from_list(cls, *args, adjust_grays=True, **kwargs): """ # Get coordinates coords = None - space = kwargs.get('space', DEFAULT_SPACE).lower() + space = kwargs.get("space", DEFAULT_SPACE).lower() name, colors, ratios, kwargs = cls._pop_args( - *args, names=('colors', 'ratios'), **kwargs + *args, names=("colors", "ratios"), **kwargs ) if not np.iterable(colors): - raise ValueError(f'Colors must be iterable, got colors={colors!r}') + raise ValueError(f"Colors must be iterable, got colors={colors!r}") if ( - np.iterable(colors[0]) and len(colors[0]) == 2 + np.iterable(colors[0]) + and len(colors[0]) == 2 and not isinstance(colors[0], str) ): coords, colors = zip(*colors) # Build segmentdata - keys = ('hue', 'saturation', 'luminance', 'alpha') + keys = ("hue", "saturation", "luminance", "alpha") hslas = [to_xyza(color, space) for color in colors] cdict = {} for key, values in zip(keys, zip(*hslas)): @@ -2255,7 +2361,7 @@ def from_list(cls, *args, adjust_grays=True, **kwargs): # Adjust grays if adjust_grays: - hues = cdict['hue'] # segment data + hues = cdict["hue"] # segment data for i, color in enumerate(colors): rgb = to_rgb(color) if isinstance(color, str) and REGEX_ADJUST.match(color): @@ -2272,8 +2378,7 @@ def from_list(cls, *args, adjust_grays=True, **kwargs): # Deprecated to_linear_segmented = warnings._rename_objs( - '0.8.0', - to_linear_segmented=to_continuous + "0.8.0", to_linear_segmented=to_continuous ) @@ -2315,11 +2420,11 @@ def _sanitize_levels(levels, minsize=2): # NOTE: Matplotlib does not support datetime colormap levels as of 3.5 levels = inputs._to_numpy_array(levels) if levels.ndim != 1 or levels.size < minsize: - raise ValueError(f'Levels {levels} must be a 1D array with size >= {minsize}.') + raise ValueError(f"Levels {levels} must be a 1D array with size >= {minsize}.") if isinstance(levels, ma.core.MaskedArray): levels = levels.filled(np.nan) if not inputs._is_numeric(levels) or not np.all(np.isfinite(levels)): - raise ValueError(f'Levels {levels} does not support non-numeric cmap levels.') + raise ValueError(f"Levels {levels} does not support non-numeric cmap levels.") diffs = np.sign(np.diff(levels)) if np.all(diffs == 1): descending = False @@ -2327,7 +2432,7 @@ def _sanitize_levels(levels, minsize=2): descending = True levels = levels[::-1] else: - raise ValueError(f'Levels {levels} must be monotonic.') + raise ValueError(f"Levels {levels} must be monotonic.") return levels, descending @@ -2336,16 +2441,23 @@ class DiscreteNorm(mcolors.BoundaryNorm): Meta-normalizer that discretizes the possible color values returned by arbitrary continuous normalizers given a sequence of level boundaries. """ + # See this post: https://stackoverflow.com/a/48614231/4970632 # WARNING: Must be child of BoundaryNorm. Many methods in ColorBarBase # test for class membership, crucially including _process_values(), which # if it doesn't detect BoundaryNorm will try to use DiscreteNorm.inverse(). @warnings._rename_kwargs( - '0.7.0', extend='unique', descending='DiscreteNorm(descending_levels)' + "0.7.0", extend="unique", descending="DiscreteNorm(descending_levels)" ) def __init__( - self, levels, - norm=None, unique=None, step=None, clip=False, ticks=None, labels=None + self, + levels, + norm=None, + unique=None, + step=None, + clip=False, + ticks=None, + labels=None, ): """ Parameters @@ -2404,19 +2516,19 @@ def __init__( if step is None: step = 1.0 if unique is None: - unique = 'neither' + unique = "neither" if not norm: norm = mcolors.Normalize() elif isinstance(norm, mcolors.BoundaryNorm): - raise ValueError('Normalizer cannot be instance of BoundaryNorm.') + raise ValueError("Normalizer cannot be instance of BoundaryNorm.") elif not isinstance(norm, mcolors.Normalize): - raise ValueError('Normalizer must be instance of Normalize.') - uniques = ('min', 'max', 'both', 'neither') + raise ValueError("Normalizer must be instance of Normalize.") + uniques = ("min", "max", "both", "neither") if unique not in uniques: raise ValueError( - f'Unknown unique setting {unique!r}. Options are: ' - + ', '.join(map(repr, uniques)) - + '.' + f"Unknown unique setting {unique!r}. Options are: " + + ", ".join(map(repr, uniques)) + + "." ) # Process level boundaries and centers @@ -2428,7 +2540,7 @@ def __init__( vmin = norm.vmin = np.min(levels) vmax = norm.vmax = np.max(levels) bins, _ = _sanitize_levels(norm(levels)) - vcenter = getattr(norm, 'vcenter', None) + vcenter = getattr(norm, "vcenter", None) mids = np.zeros((levels.size + 1,)) mids[1:-1] = 0.5 * (levels[1:] + levels[:-1]) mids[0], mids[-1] = mids[1], mids[-2] @@ -2442,9 +2554,9 @@ def __init__( # minimum 0 maximum 1, would mess up color distribution. However this is still # not perfect... get asymmetric color intensity either side of central point. # So we add special handling for diverging norms below to improve symmetry. - if unique in ('min', 'both'): + if unique in ("min", "both"): mids[0] += step * (mids[1] - mids[2]) - if unique in ('max', 'both'): + if unique in ("max", "both"): mids[-1] += step * (mids[-2] - mids[-3]) mmin, mmax = np.min(mids), np.max(mids) if vcenter is None: @@ -2523,7 +2635,7 @@ def inverse(self, value): # noqa: U100 ValueError Inversion after discretization is impossible. """ - raise ValueError('DiscreteNorm is not invertible.') + raise ValueError("DiscreteNorm is not invertible.") @property def descending(self): @@ -2538,6 +2650,7 @@ class SegmentedNorm(mcolors.Normalize): Normalizer that scales data linearly with respect to the interpolated index in an arbitrary monotonic level sequence. """ + def __init__(self, levels, vmin=None, vmax=None, clip=False): """ Parameters @@ -2633,12 +2746,11 @@ class DivergingNorm(mcolors.Normalize): Normalizer that ensures some central data value lies at the central colormap color. The default central value is ``0``. """ + def __str__(self): - return type(self).__name__ + f'(center={self.vcenter!r})' + return type(self).__name__ + f"(center={self.vcenter!r})" - def __init__( - self, vcenter=0, vmin=None, vmax=None, fair=True, clip=None - ): + def __init__(self, vcenter=0, vmin=None, vmax=None, fair=True, clip=None): """ Parameters ---------- @@ -2690,7 +2802,7 @@ def __call__(self, value, clip=None): if clip: # note that np.clip can handle masked arrays value = np.clip(value, self.vmin, self.vmax) if self.vmin > self.vmax: - raise ValueError('vmin must be less than or equal to vmax.') + raise ValueError("vmin must be less than or equal to vmax.") elif self.vmin == self.vmax: x = [self.vmin, self.vmax] y = [0.0, 0.0] @@ -2730,7 +2842,7 @@ def _init_color_database(): database = mcolors._colors_full_map if not isinstance(database, ColorDatabase): database = mcolors._colors_full_map = ColorDatabase(database) - if hasattr(mcolors, 'colorConverter'): # suspect deprecation is coming soon + if hasattr(mcolors, "colorConverter"): # suspect deprecation is coming soon mcolors.colorConverter.cache = database.cache mcolors.colorConverter.colors = database return database @@ -2740,75 +2852,51 @@ def _init_cmap_database(): """ Initialize the subclassed database. """ - # WARNING: Skip over the matplotlib native duplicate entries - # with suffixes '_r' and '_shifted'. - attr = '_cmap_registry' if hasattr(mcm, '_cmap_registry') else 'cmap_d' - database = getattr(mcm, attr) - if mcm.get_cmap is not _get_cmap: - mcm.get_cmap = _get_cmap - if mcm.register_cmap is not _register_cmap: - mcm.register_cmap = _register_cmap + # We override the matplotlib base class + # to add some functionality to it. Key features includes + # - key insensitive lookup + # - allow for dynamically generated shifted or reversed colormaps + # with the extensions _r and _s(hifted) + # This means we have to collect the base colormaps + # and register them under the new object + database = mcm._colormaps # shallow copy of mpl's colormaps if not isinstance(database, ColormapDatabase): + # Collect the mpl colormaps and include them + # in proplot's registry database = { - key: value for key, value in database.items() - if key[-2:] != '_r' and key[-8:] != '_shifted' + key: value + for key, value in database.items() + if not key.endswith("_r") and not key.endswith("_shifted") } database = ColormapDatabase(database) - setattr(mcm, attr, database) + setattr( + mcm, "_colormaps", database + ) # not sure if this is necessary since colormaps is a (shallow?) copy of _colormaps + setattr(mpl, "colormaps", database) # this is necessary return database -_mpl_register_cmap = mcm.register_cmap -@functools.wraps(_mpl_register_cmap) # noqa: E302 -def _register_cmap(*args, **kwargs): - """ - Monkey patch for `~matplotlib.cm.register_cmap`. Ignores warning - message when re-registering existing colormaps. This is unnecessary - and triggers 100 warnings when importing seaborn. - """ - with warnings.catch_warnings(): - warnings.simplefilter('ignore', UserWarning) - return _mpl_register_cmap(*args, **kwargs) - - -@functools.wraps(mcm.get_cmap) -def _get_cmap(name=None, lut=None): - """ - Monkey patch for `~matplotlib.cm.get_cmap`. Permits case-insensitive - search of monkey-patched colormap database. This was broken in v3.2.0 - because matplotlib now uses _check_in_list with cmap dictionary keys. - """ - if name is None: - name = rc['image.cmap'] - if isinstance(name, mcolors.Colormap): - return name - cmap = _cmap_database[name] - if lut is not None: - cmap = cmap._resample(lut) - return cmap - - def _get_cmap_subtype(name, subtype): """ Get a colormap belonging to a particular class. If none are found then raise a useful error message that omits colormaps from other classes. """ # NOTE: Right now this is just used in rc validation but could be used elsewhere - if subtype == 'discrete': + if subtype == "discrete": cls = DiscreteColormap - elif subtype == 'continuous': + elif subtype == "continuous": cls = ContinuousColormap - elif subtype == 'perceptual': + elif subtype == "perceptual": cls = PerceptualColormap else: - raise RuntimeError(f'Invalid subtype {subtype!r}.') - cmap = _cmap_database.get(name, None) + raise RuntimeError(f"Invalid subtype {subtype!r}.") + cmap = _cmap_database.get_cmap(name) if not isinstance(cmap, cls): names = sorted(k for k, v in _cmap_database.items() if isinstance(v, cls)) raise ValueError( - f'Invalid {subtype} colormap name {name!r}. Options are: ' - + ', '.join(map(repr, names)) - + '.' + f"Invalid {subtype} colormap name {name!r}. Options are: " + + ", ".join(map(repr, names)) + + "." ) return cmap @@ -2821,9 +2909,9 @@ def _translate_cmap(cmap, lut=None, cyclic=None, listedthresh=None): # Parse args # WARNING: Apply default 'cyclic' property to native matplotlib colormaps # based on known names. Maybe slightly dangerous but cleanest approach - lut = _not_none(lut, rc['image.lut']) + lut = _not_none(lut, rc["image.lut"]) cyclic = _not_none(cyclic, cmap.name and cmap.name.lower() in CMAPS_CYCLIC) - listedthresh = _not_none(listedthresh, rc['cmap.listedthresh']) + listedthresh = _not_none(listedthresh, rc["cmap.listedthresh"]) # Translate the colormap # WARNING: Here we ignore 'N' in order to respect proplotrc lut sizes @@ -2847,8 +2935,8 @@ def _translate_cmap(cmap, lut=None, cyclic=None, listedthresh=None): pass else: raise ValueError( - f'Invalid colormap type {type(cmap).__name__!r}. ' - 'Must be instance of matplotlib.colors.Colormap.' + f"Invalid colormap type {type(cmap).__name__!r}. " + "Must be instance of matplotlib.colors.Colormap." ) # Apply hidden settings @@ -2863,6 +2951,7 @@ class _ColorCache(dict): """ Replacement for the native color cache. """ + def __getitem__(self, key): """ Get the standard color, colormap color, or color cycle color. @@ -2890,15 +2979,15 @@ def _get_rgba(self, arg, alpha): if isinstance(cmap, DiscreteColormap): if not 0 <= arg[1] < len(cmap.colors): raise ValueError( - f'Color cycle sample for {arg[0]!r} cycle must be ' - f'between 0 and {len(cmap.colors) - 1}, got {arg[1]}.' + f"Color cycle sample for {arg[0]!r} cycle must be " + f"between 0 and {len(cmap.colors) - 1}, got {arg[1]}." ) rgba = cmap.colors[arg[1]] # draw from list of colors else: if not 0 <= arg[1] <= 1: raise ValueError( - f'Colormap sample for {arg[0]!r} colormap must be ' - f'between 0 and 1, got {arg[1]}.' + f"Colormap sample for {arg[0]!r} colormap must be " + f"between 0 and 1, got {arg[1]}." ) rgba = cmap(arg[1]) # get color selection # Return the colormap value @@ -2912,18 +3001,13 @@ class ColorDatabase(MutableMapping, dict): Dictionary subclass used to replace the builtin matplotlib color database. See `~ColorDatabase.__getitem__` for details. """ + _colors_replace = ( - ('grey', 'gray'), # British --> American synonyms - ('ochre', 'ocher'), # ... - ('kelley', 'kelly'), # backwards compatibility to correct spelling + ("grey", "gray"), # British --> American synonyms + ("ochre", "ocher"), # ... + ("kelley", "kelly"), # backwards compatibility to correct spelling ) - def __iter__(self): - yield from dict.__iter__(self) - - def __len__(self): - return dict.__len__(self) - def __delitem__(self, key): key = self._parse_key(key) dict.__delitem__(self, key) @@ -2974,7 +3058,7 @@ def _parse_key(self, key): Parse the color key. Currently this just translates grays. """ if not isinstance(key, str): - raise ValueError(f'Invalid color name {key!r}. Must be string.') + raise ValueError(f"Invalid color name {key!r}. Must be string.") if isinstance(key, str) and len(key) > 1: # ignore base colors key = key.lower() for sub, rep in self._colors_replace: @@ -2988,24 +3072,15 @@ def cache(self): return self._cache -class ColormapDatabase(MutableMapping, dict): +class ColormapDatabase(mcm.ColormapRegistry): """ Dictionary subclass used to replace the matplotlib colormap registry. See `~ColormapDatabase.__getitem__` and `~ColormapDatabase.__setitem__` for details. """ - _regex_grays = re.compile(r'\A(grays)(_r|_s)*\Z', flags=re.IGNORECASE) - _regex_suffix = re.compile(r'(_r|_s)*\Z', flags=re.IGNORECASE) - - def __iter__(self): - yield from dict.__iter__(self) - def __len__(self): - return dict.__len__(self) - - def __delitem__(self, key): - key = self._parse_key(key, mirror=True) - dict.__delitem__(self, key) + _regex_grays = re.compile(r"\A(grays)(_r|_s)*\Z", flags=re.IGNORECASE) + _regex_suffix = re.compile(r"(_r|_s)*\Z", flags=re.IGNORECASE) def __init__(self, kwargs): """ @@ -3014,31 +3089,13 @@ def __init__(self, kwargs): kwargs : dict-like The source dictionary. """ - for key, value in kwargs.items(): - self.__setitem__(key, value) - - def __getitem__(self, key): - """ - Retrieve the colormap associated with the sanitized key name. The - key name is case insensitive. - - * If the key ends in ``'_r'``, the result of ``cmap.reversed()`` is - returned for the colormap registered under the preceding name. - * If the key ends in ``'_s'``, the result of ``cmap.shifted(180)`` is - returned for the colormap registered under the preceding name. - * Reversed diverging colormaps can be requested with their "reversed" - name -- for example, ``'BuRd'`` is equivalent to ``'RdBu_r'``. - """ - return self._get_item(key) - - def __setitem__(self, key, value): - """ - Store the colormap under its lowercase name. If the object is a - `matplotlib.colors.ListedColormap` and ``cmap.N`` is smaller than - :rc:`cmap.listedthresh`, it is converted to a `proplot.colors.DiscreteColormap`. - Otherwise, it is converted to a `proplot.colors.ContinuousColormap`. - """ - self._set_item(key, value) + super().__init__(kwargs) + # The colormap is initialized with all the base colormaps + # We have to change the classes internally to Perceptual, Continuous or Discrete + # such that proplot knows what these objects are. We piggy back on the registering mechanism + # by overriding matplotlib's behavior + for name in tuple(self._cmaps.keys()): + self.register(self._cmaps[name], name=name) def _translate_deprecated(self, key): """ @@ -3048,73 +3105,96 @@ def _translate_deprecated(self, key): # helpfully "redirect" user to SciVisColor cmap when they are trying to # generate open-color monochromatic cmaps and would disallow some color names if isinstance(key, str): - test = self._regex_suffix.sub('', key) + test = self._regex_suffix.sub("", key) else: test = None if not self._has_item(test) and test in CMAPS_REMOVED: version = CMAPS_REMOVED[test] raise ValueError( - f'The colormap name {key!r} was removed in version {version}.' + f"The colormap name {key!r} was removed in version {version}." ) if not self._has_item(test) and test in CMAPS_RENAMED: test_new, version = CMAPS_RENAMED[test] warnings._warn_proplot( - f'The colormap name {test!r} was deprecated in version {version} ' - f'and may be removed in {warnings._next_release()}. Please use ' - f'the colormap name {test_new!r} instead.' + f"The colormap name {test!r} was deprecated in version {version} " + f"and may be removed in {warnings.next_release()}. Please use " + f"the colormap name {test_new!r} instead." ) key = re.sub(test, test_new, key, flags=re.IGNORECASE) return key - def _translate_key(self, key, mirror=True): + def _translate_key(self, original_key, mirror=True): """ Return the sanitized colormap name. Used for lookups and assignments. """ # Sanitize key - if not isinstance(key, str): - raise KeyError(f'Invalid key {key!r}. Key must be a string.') - key = key.lower() - key = self._regex_grays.sub(r'greys\2', key) - # Mirror diverging - reverse = key[-2:] == '_r' + if not isinstance(original_key, str): + raise KeyError(f"Invalid key {original_key!r}. Key must be a string.") + + key = original_key.lower() + key = self._regex_grays.sub(r"greys\2", key) + + # Handle reversal + reverse = key.endswith("_r") if reverse: - key = key[:-2] - if mirror and not self._has_item(key): # avoid recursion here + key = key.rstrip("_r") + + # Check if the key exists in builtin colormaps + if self._has_item(key): + return key + "_r" if reverse else key + + # Mirror diverging colormaps + if mirror: + # Check for diverging colormaps key_mirror = CMAPS_DIVERGING.get(key, None) if key_mirror and self._has_item(key_mirror): - reverse = not reverse - key = key_mirror - if reverse: - key = key + '_r' + return key_mirror + "_r" if not reverse else key_mirror + + # Check for reversed builtin colormaps + if self._has_item(key + "_r"): + return key if reverse else key + "_r" + + # Try mirroring the non-lowered key + if reverse: + original_key = original_key.strip("_r") + half = len(original_key) // 2 + mirrored_key = original_key[half:] + original_key[:half] + if self._has_item(mirrored_key): + return mirrored_key + "_r" if not reverse else mirrored_key + # Restore key + if reverse: + original_key = original_key + "_r" + # If no match found, return the original key return key def _has_item(self, key): - """ - Redirect to unsanitized `dict.__contains__`. - """ - return dict.__contains__(self, key) + return key in self._cmaps + + def get_cmap(self, cmap): + return self.__getitem__(cmap) - def _get_item(self, key): + def __getitem__(self, key): """ Get the colormap with flexible input keys. """ # Sanitize key key = self._translate_deprecated(key) key = self._translate_key(key, mirror=True) - shift = key[-2:] == '_s' and not self._has_item(key) + shift = key.endswith("_s") and not self._has_item(key) if shift: - key = key[:-2] - reverse = key[-2:] == '_r' and not self._has_item(key) + key = key.rstrip("_s") + reverse = key.endswith("_r") and not self._has_item(key) + if reverse: - key = key[:-2] + key = key.rstrip("_r") # Retrieve colormap - try: - value = dict.__getitem__(self, key) # may raise keyerror - except KeyError: + if self._has_item(key): + value = self._cmaps[key].copy() + else: raise KeyError( - f'Invalid colormap or color cycle name {key!r}. Options are: ' - + ', '.join(map(repr, self)) - + '.' + f"Invalid colormap or color cycle name {key!r}. Options are: " + + ", ".join(map(repr, self)) + + "." ) # Modify colormap if reverse: @@ -3123,17 +3203,23 @@ def _get_item(self, key): value = value.shifted(180) return value - def _set_item(self, key, value): + def register(self, cmap, *, name=None, force=False): """ Add the colormap after validating and converting. """ - if not isinstance(key, str): - raise KeyError(f'Invalid key {key!r}. Must be string.') - if not isinstance(value, mcolors.Colormap): - raise ValueError('Object is not a colormap.') - key = self._translate_key(key, mirror=False) - value = _translate_cmap(value) - dict.__setitem__(self, key, value) + if name is None and cmap.name is None: + raise ValueError("Please register the cmap under a string") + elif name is None and cmap.name is not None: + name = cmap.name + name = self._translate_key(name, mirror=False) + cmap = _translate_cmap(cmap) + # The builtin cmaps are a different class + # Proplot internally uses different classes for the different colormaps + if force and name in self._cmaps: + # surpress warning if the colormap is not generate by proplot + if name not in self._builtin_cmaps: + print(f"Overwriting {name!r} that was already registered") + self._cmaps[name] = cmap.copy() # Initialize databases @@ -3147,7 +3233,7 @@ def _set_item(self, key, value): PerceptuallyUniformColormap, LinearSegmentedNorm, ) = warnings._rename_objs( # noqa: E501 - '0.8.0', + "0.8.0", ListedColormap=DiscreteColormap, LinearSegmentedColormap=ContinuousColormap, PerceptuallyUniformColormap=PerceptualColormap, diff --git a/proplot/config.py b/proplot/config.py index 79f9e6f7b..e2c7e5dfb 100644 --- a/proplot/config.py +++ b/proplot/config.py @@ -42,39 +42,32 @@ try: from IPython import get_ipython except ImportError: + def get_ipython(): return + # Suppress warnings emitted by mathtext.py (_mathtext.py in recent versions) # when when substituting dummy unavailable glyph due to fallback disabled. -logging.getLogger('matplotlib.mathtext').setLevel(logging.ERROR) +logging.getLogger("matplotlib.mathtext").setLevel(logging.ERROR) __all__ = [ - 'Configurator', - 'rc', - 'rc_proplot', - 'rc_matplotlib', - 'use_style', - 'config_inline_backend', - 'register_cmaps', - 'register_cycles', - 'register_colors', - 'register_fonts', - 'RcConfigurator', # deprecated - 'inline_backend_fmt', # deprecated + "Configurator", + "rc", + "rc_proplot", + "rc_matplotlib", + "use_style", + "config_inline_backend", + "register_cmaps", + "register_cycles", + "register_colors", + "register_fonts", + "RcConfigurator", # deprecated + "inline_backend_fmt", # deprecated ] # Constants -COLORS_KEEP = ( - 'red', - 'green', - 'blue', - 'cyan', - 'yellow', - 'magenta', - 'white', - 'black' -) +COLORS_KEEP = ("red", "green", "blue", "cyan", "yellow", "magenta", "white", "black") # Configurator docstrings _rc_docstring = """ @@ -85,7 +78,7 @@ def get_ipython(): default : bool, default: True Whether to reload built-in default proplot settings. """ -docstring._snippet_manager['rc.params'] = _rc_docstring +docstring._snippet_manager["rc.params"] = _rc_docstring # Registration docstrings _shared_docstring = """ @@ -142,16 +135,28 @@ def get_ipython(): Whether to reload the default {objects} packaged with proplot. Default is always ``False``. """ -docstring._snippet_manager['rc.cmap_params'] = _register_docstring.format(objects='colormaps') # noqa: E501 -docstring._snippet_manager['rc.cycle_params'] = _register_docstring.format(objects='color cycles') # noqa: E501 -docstring._snippet_manager['rc.color_params'] = _register_docstring.format(objects='colors') # noqa: E501 -docstring._snippet_manager['rc.font_params'] = _register_docstring.format(objects='fonts') # noqa: E501 -docstring._snippet_manager['rc.cmap_args'] = _shared_docstring.format(objects='colormaps', type='Continuous') # noqa: E501 -docstring._snippet_manager['rc.cycle_args'] = _shared_docstring.format(objects='color cycles', type='Discrete') # noqa: E501 -docstring._snippet_manager['rc.color_args'] = _color_docstring -docstring._snippet_manager['rc.font_args'] = _font_docstring -docstring._snippet_manager['rc.cmap_exts'] = _cmap_exts_docstring -docstring._snippet_manager['rc.cycle_exts'] = _cycle_exts_docstring +docstring._snippet_manager["rc.cmap_params"] = _register_docstring.format( + objects="colormaps" +) # noqa: E501 +docstring._snippet_manager["rc.cycle_params"] = _register_docstring.format( + objects="color cycles" +) # noqa: E501 +docstring._snippet_manager["rc.color_params"] = _register_docstring.format( + objects="colors" +) # noqa: E501 +docstring._snippet_manager["rc.font_params"] = _register_docstring.format( + objects="fonts" +) # noqa: E501 +docstring._snippet_manager["rc.cmap_args"] = _shared_docstring.format( + objects="colormaps", type="Continuous" +) # noqa: E501 +docstring._snippet_manager["rc.cycle_args"] = _shared_docstring.format( + objects="color cycles", type="Discrete" +) # noqa: E501 +docstring._snippet_manager["rc.color_args"] = _color_docstring +docstring._snippet_manager["rc.font_args"] = _font_docstring +docstring._snippet_manager["rc.cmap_exts"] = _cmap_exts_docstring +docstring._snippet_manager["rc.cycle_exts"] = _cycle_exts_docstring def _init_user_file(): @@ -167,7 +172,7 @@ def _init_user_folders(): """ Initialize .proplot folder. """ - for subfolder in ('', 'cmaps', 'cycles', 'colors', 'fonts'): + for subfolder in ("", "cmaps", "cycles", "colors", "fonts"): folder = Configurator.user_folder(subfolder) if not os.path.isdir(folder): os.mkdir(folder) @@ -201,7 +206,7 @@ def _iter_data_objects(folder, *args, **kwargs): for i, path in enumerate(_get_data_folders(folder, **kwargs)): for dirname, dirnames, filenames in os.walk(path): for filename in filenames: - if filename[0] == '.': # UNIX-style hidden files + if filename[0] == ".": # UNIX-style hidden files continue path = os.path.join(dirname, filename) yield i, path @@ -211,7 +216,7 @@ def _iter_data_objects(folder, *args, **kwargs): if os.path.isfile(path): yield i, path else: - raise FileNotFoundError(f'Invalid file path {path!r}.') + raise FileNotFoundError(f"Invalid file path {path!r}.") def _filter_style_dict(rcdict, warn=True): @@ -227,8 +232,8 @@ def _filter_style_dict(rcdict, warn=True): if key in mstyle.STYLE_BLACKLIST: if warn: warnings._warn_proplot( - f'Dictionary includes a parameter, {key!r}, that is not related ' - 'to style. Ignoring.' + f"Dictionary includes a parameter, {key!r}, that is not related " + "to style. Ignoring." ) else: rcdict_filtered[key] = rcdict[key] @@ -249,13 +254,13 @@ def _get_default_style_dict(): # RcParams in early versions. Manually pop it out here. rcdict = _filter_style_dict(mpl.rcParamsDefault, warn=False) with warnings.catch_warnings(): - warnings.simplefilter('ignore', mpl.MatplotlibDeprecationWarning) + warnings.simplefilter("ignore", mpl.MatplotlibDeprecationWarning) rcdict = dict(RcParams(rcdict)) - for attr in ('_deprecated_set', '_deprecated_remain_as_none'): + for attr in ("_deprecated_set", "_deprecated_remain_as_none"): deprecated = getattr(mpl, attr, ()) for key in deprecated: # _deprecated_set is in matplotlib < 3.4 rcdict.pop(key, None) - rcdict.pop('examples.directory', None) # special case for matplotlib < 3.2 + rcdict.pop("examples.directory", None) # special case for matplotlib < 3.2 return rcdict @@ -277,23 +282,24 @@ def _get_style_dict(style, filter=True): # (e.g. every time you make an axes and every format() call). Instead of # copying the entire rcParams dict we just track the keys that were changed. style_aliases = { - '538': 'fivethirtyeight', - 'mpl20': 'default', - 'mpl15': 'classic', - 'original': mpl.matplotlib_fname(), + "538": "fivethirtyeight", + "mpl20": "default", + "mpl15": "classic", + "original": mpl.matplotlib_fname(), + "seaborn": "seaborn-v0_8", } # Always apply the default style *first* so styles are rigid kw_matplotlib = _get_default_style_dict() - if style == 'default' or style is mpl.rcParamsDefault: + if style == "default" or style is mpl.rcParamsDefault: return kw_matplotlib # Apply limited deviations from the matplotlib style that we want to propagate to # other styles. Want users selecting stylesheets to have few surprises, so # currently just enforce the new aesthetically pleasing fonts. - kw_matplotlib['font.family'] = 'sans-serif' - for fmly in ('serif', 'sans-serif', 'monospace', 'cursive', 'fantasy'): - kw_matplotlib['font.' + fmly] = rcsetup._rc_matplotlib_default['font.' + fmly] + kw_matplotlib["font.family"] = "sans-serif" + for fmly in ("serif", "sans-serif", "monospace", "cursive", "fantasy"): + kw_matplotlib["font." + fmly] = rcsetup._rc_matplotlib_default["font." + fmly] # Apply user input style(s) one by one if isinstance(style, str) or isinstance(style, dict): @@ -312,13 +318,13 @@ def _get_style_dict(style, filter=True): kw = mpl.rc_params_from_file(style, use_default_template=False) except IOError: raise IOError( - f'Style {style!r} not found in the style library and input ' - 'is not a valid URL or file path. Available styles are: ' - + ', '.join(map(repr, mstyle.available)) - + '.' + f"Style {style!r} not found in the style library and input " + "is not a valid URL or file path. Available styles are: " + + ", ".join(map(repr, mstyle.available)) + + "." ) else: - raise ValueError(f'Invalid style {style!r}. Must be string or dictionary.') + raise ValueError(f"Invalid style {style!r}. Must be string or dictionary.") if filter: kw = _filter_style_dict(kw, warn=True) kw_matplotlib.update(kw) @@ -332,22 +338,33 @@ def _infer_proplot_dict(kw_params): """ kw_proplot = {} mpl_to_proplot = { - 'xtick.labelsize': ( - 'tick.labelsize', 'grid.labelsize', + "xtick.labelsize": ( + "tick.labelsize", + "grid.labelsize", ), - 'ytick.labelsize': ( - 'tick.labelsize', 'grid.labelsize', + "ytick.labelsize": ( + "tick.labelsize", + "grid.labelsize", ), - 'axes.titlesize': ( - 'abc.size', 'suptitle.size', 'title.size', - 'leftlabel.size', 'rightlabel.size', - 'toplabel.size', 'bottomlabel.size', + "axes.titlesize": ( + "abc.size", + "suptitle.size", + "title.size", + "leftlabel.size", + "rightlabel.size", + "toplabel.size", + "bottomlabel.size", ), - 'text.color': ( - 'abc.color', 'suptitle.color', 'title.color', - 'tick.labelcolor', 'grid.labelcolor', - 'leftlabel.color', 'rightlabel.color', - 'toplabel.color', 'bottomlabel.color', + "text.color": ( + "abc.color", + "suptitle.color", + "title.color", + "tick.labelcolor", + "grid.labelcolor", + "leftlabel.color", + "rightlabel.color", + "toplabel.color", + "bottomlabel.color", ), } for key, params in mpl_to_proplot.items(): @@ -389,16 +406,16 @@ def config_inline_backend(fmt=None): ipython = get_ipython() if ipython is None: return - fmt = _not_none(fmt, rc_proplot['inlineformat']) + fmt = _not_none(fmt, rc_proplot["inlineformat"]) if isinstance(fmt, str): fmt = [fmt] elif np.iterable(fmt): fmt = list(fmt) else: - raise ValueError(f'Invalid inline backend format {fmt!r}. Must be string.') - ipython.magic('config InlineBackend.figure_formats = ' + repr(fmt)) - ipython.magic('config InlineBackend.rc = {}') - ipython.magic('config InlineBackend.close_figures = True') + raise ValueError(f"Invalid inline backend format {fmt!r}. Must be string.") + ipython.magic("config InlineBackend.figure_formats = " + repr(fmt)) + ipython.magic("config InlineBackend.rc = {}") + ipython.magic("config InlineBackend.close_figures = True") ipython.magic("config InlineBackend.print_figure_kwargs = {'bbox_inches': None}") @@ -452,25 +469,26 @@ def register_cmaps(*args, user=None, local=None, default=False): """ # Register input colormaps from . import colors as pcolors + user = _not_none(user, not bool(args)) # skip user folder if input args passed local = _not_none(local, not bool(args)) paths = [] for arg in args: if isinstance(arg, mcolors.Colormap): - pcolors._cmap_database[arg.name] = arg + pcolors._cmap_database.register(arg, name=arg.name) else: paths.append(arg) # Register data files for i, path in _iter_data_objects( - 'cmaps', *paths, user=user, local=local, default=default + "cmaps", *paths, user=user, local=local, default=default ): cmap = pcolors.ContinuousColormap.from_file(path, warn_on_failure=True) if not cmap: continue if i == 0 and cmap.name.lower() in pcolors.CMAPS_CYCLIC: cmap.set_cyclic(True) - pcolors._cmap_database[cmap.name] = cmap + pcolors._cmap_database.register(cmap, name=cmap.name) @docstring._snippet_manager @@ -495,23 +513,24 @@ def register_cycles(*args, user=None, local=None, default=False): """ # Register input color cycles from . import colors as pcolors + user = _not_none(user, not bool(args)) # skip user folder if input args passed local = _not_none(local, not bool(args)) paths = [] for arg in args: if isinstance(arg, mcolors.Colormap): - pcolors._cmap_database[arg.name] = arg + pcolors._cmap_database.register(arg, name=arg.name) else: paths.append(arg) # Register data files for _, path in _iter_data_objects( - 'cycles', *paths, user=user, local=local, default=default + "cycles", *paths, user=user, local=local, default=default ): cmap = pcolors.DiscreteColormap.from_file(path, warn_on_failure=True) if not cmap: continue - pcolors._cmap_database[cmap.name] = cmap + pcolors._cmap_database.register(cmap, name=cmap.name) @docstring._snippet_manager @@ -548,13 +567,14 @@ def register_colors( proplot.demos.show_colors """ from . import colors as pcolors + default = default or space is not None or margin is not None margin = _not_none(margin, 0.1) - space = _not_none(space, 'hcl') + space = _not_none(space, "hcl") # Remove previously registered colors # NOTE: Try not to touch matplotlib colors for compatibility - srcs = {'xkcd': pcolors.COLORS_XKCD, 'opencolor': pcolors.COLORS_OPEN} + srcs = {"xkcd": pcolors.COLORS_XKCD, "opencolor": pcolors.COLORS_OPEN} if default: # possibly slow but not these dicts are empty on startup for src in srcs.values(): for key in src: @@ -575,20 +595,20 @@ def register_colors( if mcolors.is_color_like(color): pcolors._color_database[key] = mcolors.to_rgba(color) else: - raise ValueError(f'Invalid color {key}={color!r}.') + raise ValueError(f"Invalid color {key}={color!r}.") # Load colors from file and get their HCL values # NOTE: Colors that come *later* overwrite colors that come earlier. for i, path in _iter_data_objects( - 'colors', *paths, user=user, local=local, default=default + "colors", *paths, user=user, local=local, default=default ): loaded = pcolors._load_colors(path, warn_on_failure=True) if i == 0: cat, _ = os.path.splitext(os.path.basename(path)) if cat not in srcs: - raise RuntimeError(f'Unknown proplot color database {path!r}.') + raise RuntimeError(f"Unknown proplot color database {path!r}.") src = srcs[cat] - if cat == 'xkcd': + if cat == "xkcd": for key in COLORS_KEEP: loaded[key] = pcolors._color_database[key] # keep the same loaded = pcolors._standardize_colors(loaded, space, margin) @@ -625,7 +645,7 @@ def register_fonts(*args, user=True, local=True, default=False): # For macOS the only fonts with 'Thin' in one of the .ttf file names # are Helvetica Neue and .SF NS Display Condensed. Never try to use these! paths_proplot = _get_data_folders( - 'fonts', user=user, local=local, default=default, reverse=True + "fonts", user=user, local=local, default=default, reverse=True ) fnames_proplot = set(mfonts.findSystemFonts(paths_proplot)) for path in args: @@ -633,18 +653,18 @@ def register_fonts(*args, user=True, local=True, default=False): if os.path.isfile(path): fnames_proplot.add(path) else: - raise FileNotFoundError(f'Invalid font file path {path!r}.') + raise FileNotFoundError(f"Invalid font file path {path!r}.") # Detect user-input ttc fonts and issue warning fnames_proplot_ttc = { - file for file in fnames_proplot if os.path.splitext(file)[1] == '.ttc' + file for file in fnames_proplot if os.path.splitext(file)[1] == ".ttc" } if fnames_proplot_ttc: warnings._warn_proplot( - 'Ignoring the following .ttc fonts because they cannot be ' - 'saved into PDF or EPS files (see matplotlib issue #3135): ' - + ', '.join(map(repr, sorted(fnames_proplot_ttc))) - + '. Please consider expanding them into separate .ttf files.' + "Ignoring the following .ttc fonts because they cannot be " + "saved into PDF or EPS files (see matplotlib issue #3135): " + + ", ".join(map(repr, sorted(fnames_proplot_ttc))) + + ". Please consider expanding them into separate .ttf files." ) # Rebuild font cache only if necessary! Can be >50% of total import time! @@ -652,10 +672,10 @@ def register_fonts(*args, user=True, local=True, default=False): fnames_proplot -= fnames_proplot_ttc if not fnames_all >= fnames_proplot: warnings._warn_proplot( - 'Rebuilding font cache. This usually happens ' - 'after installing or updating proplot.' + "Rebuilding font cache. This usually happens " + "after installing or updating proplot." ) - if hasattr(mfonts.fontManager, 'addfont'): + if hasattr(mfonts.fontManager, "addfont"): # Newer API lets us add font files manually and deprecates TTFPATH. However # to cache fonts added this way, we must call json_dump explicitly. # NOTE: Previously, cache filename was specified as _fmcache variable, but @@ -665,8 +685,7 @@ def register_fonts(*args, user=True, local=True, default=False): for fname in fnames_proplot: mfonts.fontManager.addfont(fname) cache = os.path.join( - mpl.get_cachedir(), - f'fontlist-v{mfonts.FontManager.__version__}.json' + mpl.get_cachedir(), f"fontlist-v{mfonts.FontManager.__version__}.json" ) mfonts.json_dump(mfonts.fontManager, cache) else: @@ -675,11 +694,11 @@ def register_fonts(*args, user=True, local=True, default=False): # font manager with hope that it would load proplot fonts on # initialization. But 99% of the time font manager just imports # the FontManager from cache, so we would have to rebuild anyway. - paths = ':'.join(paths_proplot) - if 'TTFPATH' not in os.environ: - os.environ['TTFPATH'] = paths - elif paths not in os.environ['TTFPATH']: - os.environ['TTFPATH'] += ':' + paths + paths = ":".join(paths_proplot) + if "TTFPATH" not in os.environ: + os.environ["TTFPATH"] = paths + elif paths not in os.environ["TTFPATH"]: + os.environ["TTFPATH"] += ":" + paths mfonts._rebuild() # Remove ttc files and 'Thin' fonts *after* rebuild @@ -688,10 +707,8 @@ def register_fonts(*args, user=True, local=True, default=False): mfonts.fontManager.ttflist = [ font for font in mfonts.fontManager.ttflist - if os.path.splitext(font.fname)[1] != '.ttc' and ( - _version_mpl >= '3.3' - or 'Thin' not in os.path.basename(font.fname) - ) + if os.path.splitext(font.fname)[1] != ".ttc" + and (_version_mpl >= "3.3" or "Thin" not in os.path.basename(font.fname)) ] @@ -703,15 +720,16 @@ class Configurator(MutableMapping, dict): stored in `rc_proplot`. This class is instantiated as the `rc` object on import. See the :ref:`user guide ` for details. """ + def __repr__(self): - cls = type('rc', (dict,), {}) # temporary class with short name - src = cls({key: val for key, val in rc_proplot.items() if '.' not in key}) - return type(rc_matplotlib).__repr__(src).strip()[:-1] + ',\n ...\n })' + cls = type("rc", (dict,), {}) # temporary class with short name + src = cls({key: val for key, val in rc_proplot.items() if "." not in key}) + return type(rc_matplotlib).__repr__(src).strip()[:-1] + ",\n ...\n })" def __str__(self): - cls = type('rc', (dict,), {}) # temporary class with short name - src = cls({key: val for key, val in rc_proplot.items() if '.' not in key}) - return type(rc_matplotlib).__str__(src) + '\n...' + cls = type("rc", (dict,), {}) # temporary class with short name + src = cls({key: val for key, val in rc_proplot.items() if "." not in key}) + return type(rc_matplotlib).__str__(src) + "\n..." def __iter__(self): yield from rc_proplot # sorted proplot settings, ignoring deprecations @@ -721,10 +739,10 @@ def __len__(self): return len(tuple(iter(self))) def __delitem__(self, key): # noqa: U100 - raise RuntimeError('rc settings cannot be deleted.') + raise RuntimeError("rc settings cannot be deleted.") def __delattr__(self, attr): # noqa: U100 - raise RuntimeError('rc settings cannot be deleted.') + raise RuntimeError("rc settings cannot be deleted.") @docstring._snippet_manager def __init__(self, local=True, user=True, default=True, **kwargs): @@ -762,7 +780,7 @@ def __getattr__(self, attr): Return an `rc_matplotlib` or `rc_proplot` setting using "dot" notation (e.g., ``value = pplt.rc.name``). """ - if attr[:1] == '_': + if attr[:1] == "_": return super().__getattribute__(attr) # raise built-in error else: return self.__getitem__(attr) @@ -772,7 +790,7 @@ def __setattr__(self, attr, value): Modify an `rc_matplotlib` or `rc_proplot` setting using "dot" notation (e.g., ``pplt.rc.name = value``). """ - if attr[:1] == '_': + if attr[:1] == "_": super().__setattr__(attr, value) else: self.__setitem__(attr, value) @@ -783,7 +801,7 @@ def __enter__(self): """ if not self._context: raise RuntimeError( - 'rc object must be initialized for context block using rc.context().' + "rc object must be initialized for context block using rc.context()." ) context = self._context[-1] kwargs = context.kwargs @@ -805,7 +823,7 @@ def __exit__(self, *args): # noqa: U100 """ if not self._context: raise RuntimeError( - 'rc object must be initialized for context block using rc.context().' + "rc object must be initialized for context block using rc.context()." ) context = self._context[-1] for key, value in context.rc_old.items(): @@ -824,7 +842,7 @@ def _init(self, *, local, user, default, skip_cycle=False): # Update from default settings # NOTE: see _remove_blacklisted_style_params bugfix if default: - rc_matplotlib.update(_get_style_dict('original', filter=False)) + rc_matplotlib.update(_get_style_dict("original", filter=False)) rc_matplotlib.update(rcsetup._rc_matplotlib_default) rc_proplot.update(rcsetup._rc_proplot_default) for key, value in rc_proplot.items(): @@ -858,9 +876,9 @@ def _validate_key(key, value=None): # Think deprecated matplotlib keys are not involved in any synced settings. # Also note _check_key includes special handling for some renamed keys. if not isinstance(key, str): - raise KeyError(f'Invalid key {key!r}. Must be string.') + raise KeyError(f"Invalid key {key!r}. Must be string.") key = key.lower() - if '.' not in key: + if "." not in key: key = rcsetup._rc_nodots.get(key, key) key, value = rc_proplot._check_key(key, value) # may issue deprecation warning return key, value @@ -878,7 +896,7 @@ def _validate_value(key, value): # are being read rather than after the end of the file reading. if isinstance(value, np.ndarray): value = value.item() if value.size == 1 else value.tolist() - validate_matplotlib = getattr(rc_matplotlib, 'validate', None) + validate_matplotlib = getattr(rc_matplotlib, "validate", None) validate_proplot = rc_proplot._validate if validate_matplotlib is not None and key in validate_matplotlib: value = validate_matplotlib[key](value) @@ -902,7 +920,7 @@ def _get_item_context(self, key, mode=None): elif mode == 2: rcdicts = (*cache,) else: - raise ValueError(f'Invalid caching mode {mode!r}.') + raise ValueError(f"Invalid caching mode {mode!r}.") for rcdict in rcdicts: if not rcdict: continue @@ -911,7 +929,7 @@ def _get_item_context(self, key, mode=None): except KeyError: continue if mode == 0: # otherwise return None - raise KeyError(f'Invalid rc setting {key!r}.') + raise KeyError(f"Invalid rc setting {key!r}.") def _get_item_dicts(self, key, value, skip_cycle=False): """ @@ -931,22 +949,22 @@ def _get_item_dicts(self, key, value, skip_cycle=False): kw_proplot = {} # custom properties kw_matplotlib = {} # builtin properties with warnings.catch_warnings(): - warnings.simplefilter('ignore', mpl.MatplotlibDeprecationWarning) - warnings.simplefilter('ignore', warnings.ProplotWarning) + warnings.simplefilter("ignore", mpl.MatplotlibDeprecationWarning) + warnings.simplefilter("ignore", warnings.ProplotWarning) for key in keys: if key in rc_matplotlib: kw_matplotlib[key] = value elif key in rc_proplot: kw_proplot[key] = value else: - raise KeyError(f'Invalid rc setting {key!r}.') + raise KeyError(f"Invalid rc setting {key!r}.") # Special key: configure inline backend - if contains('inlineformat'): + if contains("inlineformat"): config_inline_backend(value) # Special key: apply stylesheet - elif contains('style'): + elif contains("style"): if value is not None: ikw_matplotlib = _get_style_dict(value) kw_matplotlib.update(ikw_matplotlib) @@ -954,75 +972,78 @@ def _get_item_dicts(self, key, value, skip_cycle=False): # Cycler # NOTE: Have to skip this step during initial proplot import - elif contains('cycle') and not skip_cycle: + elif contains("cycle") and not skip_cycle: from .colors import _get_cmap_subtype - cmap = _get_cmap_subtype(value, 'discrete') - kw_matplotlib['axes.prop_cycle'] = cycler.cycler('color', cmap.colors) - kw_matplotlib['patch.facecolor'] = 'C0' + + cmap = _get_cmap_subtype(value, "discrete") + kw_matplotlib["axes.prop_cycle"] = cycler.cycler("color", cmap.colors) + kw_matplotlib["patch.facecolor"] = "C0" # Turning bounding box on should turn border off and vice versa - elif contains('abc.bbox', 'title.bbox', 'abc.border', 'title.border'): + elif contains("abc.bbox", "title.bbox", "abc.border", "title.border"): if value: - name, this = key.split('.') - other = 'border' if this == 'bbox' else 'bbox' - kw_proplot[name + '.' + other] = False + name, this = key.split(".") + other = "border" if this == "bbox" else "bbox" + kw_proplot[name + "." + other] = False # Fontsize # NOTE: Re-application of e.g. size='small' uses the updated 'font.size' - elif contains('font.size'): + elif contains("font.size"): kw_proplot.update( { - key: value for key, value in rc_proplot.items() + key: value + for key, value in rc_proplot.items() if key in rcsetup.FONT_KEYS and value in mfonts.font_scalings } ) kw_matplotlib.update( { - key: value for key, value in rc_matplotlib.items() + key: value + for key, value in rc_matplotlib.items() if key in rcsetup.FONT_KEYS and value in mfonts.font_scalings } ) # Tick length/major-minor tick length ratio - elif contains('tick.len', 'tick.lenratio'): - if contains('tick.len'): + elif contains("tick.len", "tick.lenratio"): + if contains("tick.len"): ticklen = value - ratio = rc_proplot['tick.lenratio'] + ratio = rc_proplot["tick.lenratio"] else: - ticklen = rc_proplot['tick.len'] + ticklen = rc_proplot["tick.len"] ratio = value - kw_matplotlib['xtick.minor.size'] = ticklen * ratio - kw_matplotlib['ytick.minor.size'] = ticklen * ratio + kw_matplotlib["xtick.minor.size"] = ticklen * ratio + kw_matplotlib["ytick.minor.size"] = ticklen * ratio # Spine width/major-minor tick width ratio - elif contains('tick.width', 'tick.widthratio'): - if contains('tick.width'): + elif contains("tick.width", "tick.widthratio"): + if contains("tick.width"): tickwidth = value - ratio = rc_proplot['tick.widthratio'] + ratio = rc_proplot["tick.widthratio"] else: - tickwidth = rc_proplot['tick.width'] + tickwidth = rc_proplot["tick.width"] ratio = value - kw_matplotlib['xtick.minor.width'] = tickwidth * ratio - kw_matplotlib['ytick.minor.width'] = tickwidth * ratio + kw_matplotlib["xtick.minor.width"] = tickwidth * ratio + kw_matplotlib["ytick.minor.width"] = tickwidth * ratio # Gridline width - elif contains('grid.width', 'grid.widthratio'): - if contains('grid.width'): + elif contains("grid.width", "grid.widthratio"): + if contains("grid.width"): gridwidth = value - ratio = rc_proplot['grid.widthratio'] + ratio = rc_proplot["grid.widthratio"] else: - gridwidth = rc_proplot['grid.width'] + gridwidth = rc_proplot["grid.width"] ratio = value - kw_proplot['gridminor.linewidth'] = gridwidth * ratio - kw_proplot['gridminor.width'] = gridwidth * ratio + kw_proplot["gridminor.linewidth"] = gridwidth * ratio + kw_proplot["gridminor.width"] = gridwidth * ratio # Gridline toggling - elif contains('grid', 'gridminor'): + elif contains("grid", "gridminor"): b, which = _translate_grid( - value, 'gridminor' if contains('gridminor') else 'grid' + value, "gridminor" if contains("gridminor") else "grid" ) - kw_matplotlib['axes.grid'] = b - kw_matplotlib['axes.grid.which'] = which + kw_matplotlib["axes.grid"] = b + kw_matplotlib["axes.grid.which"] = which return kw_proplot, kw_matplotlib @@ -1035,10 +1056,10 @@ def _get_axisbelow_zorder(axisbelow): zorder = 0.5 elif axisbelow is False: zorder = 2.5 - elif axisbelow in ('line', 'lines'): + elif axisbelow in ("line", "lines"): zorder = 1.5 else: - raise ValueError(f'Unexpected axisbelow value {axisbelow!r}.') + raise ValueError(f"Unexpected axisbelow value {axisbelow!r}.") return zorder def _get_background_props(self, patch_kw=None, native=True, **kwargs): @@ -1051,33 +1072,33 @@ def _get_background_props(self, patch_kw=None, native=True, **kwargs): if patch_kw: warnings._warn_proplot( "'patch_kw' is no longer necessary as of proplot v0.8. " - 'Pass the parameters as keyword arguments instead.' + "Pass the parameters as keyword arguments instead." ) kwargs.update(patch_kw) # Get user-input properties and changed rc settings # NOTE: Here we use 'color' as an alias for just 'edgecolor' rather than # both 'edgecolor' and 'facecolor' to match 'xcolor' and 'ycolor' arguments. - props = _pop_props(kwargs, 'patch') - if 'color' in props: - props.setdefault('edgecolor', props.pop('color')) - for key in ('alpha', 'facecolor', 'linewidth', 'edgecolor'): - value = self.find('axes.' + key, context=context) + props = _pop_props(kwargs, "patch") + if "color" in props: + props.setdefault("edgecolor", props.pop("color")) + for key in ("alpha", "facecolor", "linewidth", "edgecolor"): + value = self.find("axes." + key, context=context) if value is not None: props.setdefault(key, value) # Partition properties into face and edge - kw_face = _pop_kwargs(props, 'alpha', 'facecolor') - kw_edge = _pop_kwargs(props, 'edgecolor', 'linewidth', 'linestyle') - kw_edge['capstyle'] = 'projecting' # NOTE: needed to fix cartopy bounds - if 'color' in props: - kw_edge.setdefault('edgecolor', props.pop('color')) + kw_face = _pop_kwargs(props, "alpha", "facecolor") + kw_edge = _pop_kwargs(props, "edgecolor", "linewidth", "linestyle") + kw_edge["capstyle"] = "projecting" # NOTE: needed to fix cartopy bounds + if "color" in props: + kw_edge.setdefault("edgecolor", props.pop("color")) if kwargs: - raise TypeError(f'Unexpected keyword argument(s): {kwargs!r}') + raise TypeError(f"Unexpected keyword argument(s): {kwargs!r}") return kw_face, kw_edge - def _get_gridline_bool(self, grid=None, axis=None, which='major', native=True): + def _get_gridline_bool(self, grid=None, axis=None, which="major", native=True): """ Return major and minor gridline toggles from ``axes.grid``, ``axes.grid.which``, and ``axes.grid.axis``, optionally returning `None` based on the context. @@ -1086,19 +1107,19 @@ def _get_gridline_bool(self, grid=None, axis=None, which='major', native=True): # NOTE: Very careful to return not None only if setting was changed. # Avoid unnecessarily triggering grid redraws (esp. bad for geo.py) context = native or self._context_mode == 2 - grid_on = self.find('axes.grid', context=context) - which_on = self.find('axes.grid.which', context=context) + grid_on = self.find("axes.grid", context=context) + which_on = self.find("axes.grid.which", context=context) if grid_on is not None or which_on is not None: # if *one* was changed - axis_on = self['axes.grid.axis'] # always need this property - grid_on = _not_none(grid_on, self['axes.grid']) - which_on = _not_none(which_on, self['axes.grid.which']) - axis = _not_none(axis, 'x') - axis_on = axis is None or axis_on in (axis, 'both') - which_on = which_on in (which, 'both') + axis_on = self["axes.grid.axis"] # always need this property + grid_on = _not_none(grid_on, self["axes.grid"]) + which_on = _not_none(which_on, self["axes.grid.which"]) + axis = _not_none(axis, "x") + axis_on = axis is None or axis_on in (axis, "both") + which_on = which_on in (which, "both") grid = _not_none(grid, grid_on and axis_on and which_on) return grid - def _get_gridline_props(self, which='major', native=True, rebuild=False): + def _get_gridline_props(self, which="major", native=True, rebuild=False): """ Return gridline properties, optionally filtering the output dictionary based on the context. @@ -1106,24 +1127,24 @@ def _get_gridline_props(self, which='major', native=True, rebuild=False): # Line properties # NOTE: Gridline zorder is controlled automatically by matplotlib but # must be controlled manually for geographic projections - key = 'grid' if which == 'major' else 'gridminor' - prefix = 'grid_' if native else '' # for native gridlines use this prefix + key = "grid" if which == "major" else "gridminor" + prefix = "grid_" if native else "" # for native gridlines use this prefix context = not rebuild and (native or self._context_mode == 2) kwlines = self.fill( { - f'{prefix}alpha': f'{key}.alpha', - f'{prefix}color': f'{key}.color', - f'{prefix}linewidth': f'{key}.linewidth', - f'{prefix}linestyle': f'{key}.linestyle', + f"{prefix}alpha": f"{key}.alpha", + f"{prefix}color": f"{key}.color", + f"{prefix}linewidth": f"{key}.linewidth", + f"{prefix}linestyle": f"{key}.linestyle", }, context=context, ) - axisbelow = self.find('axes.axisbelow', context=context) + axisbelow = self.find("axes.axisbelow", context=context) if axisbelow is not None: if native: # this is a native plot so use set_axisbelow() down the line - kwlines['axisbelow'] = axisbelow + kwlines["axisbelow"] = axisbelow else: # this is a geographic plot so apply with zorder - kwlines['zorder'] = self._get_axisbelow_zorder(axisbelow) + kwlines["zorder"] = self._get_axisbelow_zorder(axisbelow) return kwlines def _get_label_props(self, native=True, **kwargs): @@ -1136,11 +1157,11 @@ def _get_label_props(self, native=True, **kwargs): context = native or self._context_mode == 2 kw = self.fill( { - 'color': 'axes.labelcolor', - 'weight': 'axes.labelweight', - 'size': 'axes.labelsize', - 'family': 'font.family', - 'labelpad': 'axes.labelpad', # read by set_xlabel/set_ylabel + "color": "axes.labelcolor", + "weight": "axes.labelweight", + "size": "axes.labelsize", + "family": "font.family", + "labelpad": "axes.labelpad", # read by set_xlabel/set_ylabel }, context=context, ) @@ -1155,34 +1176,34 @@ def _get_loc_string(self, string, axis=None, native=True): optionally returning `None` based on the context. """ context = native or self._context_mode == 2 - axis = _not_none(axis, 'x') - opt1, opt2 = ('top', 'bottom') if axis == 'x' else ('left', 'right') - b1 = self.find(f'{string}.{opt1}', context=context) - b2 = self.find(f'{string}.{opt2}', context=context) + axis = _not_none(axis, "x") + opt1, opt2 = ("top", "bottom") if axis == "x" else ("left", "right") + b1 = self.find(f"{string}.{opt1}", context=context) + b2 = self.find(f"{string}.{opt2}", context=context) if b1 is None and b2 is None: return None elif b1 and b2: - return 'both' + return "both" elif b1: return opt1 elif b2: return opt2 else: - return 'neither' + return "neither" - def _get_tickline_props(self, axis=None, which='major', native=True, rebuild=False): + def _get_tickline_props(self, axis=None, which="major", native=True, rebuild=False): """ Return the tick line properties, optionally filtering the output dictionary based on the context. """ # Tick properties obtained with rc.category # NOTE: This loads 'size', 'width', 'pad', 'bottom', and 'top' - axis = _not_none(axis, 'x') + axis = _not_none(axis, "x") context = not rebuild and (native or self._context_mode == 2) - kwticks = self.category(f'{axis}tick.{which}', context=context) - kwticks.pop('visible', None) - for key in ('color', 'direction'): - value = self.find(f'{axis}tick.{key}', context=context) + kwticks = self.category(f"{axis}tick.{which}", context=context) + kwticks.pop("visible", None) + for key in ("color", "direction"): + value = self.find(f"{axis}tick.{key}", context=context) if value is not None: kwticks[key] = value return kwticks @@ -1193,22 +1214,22 @@ def _get_ticklabel_props(self, axis=None, native=True, rebuild=False): based on the context. """ # NOTE: 'tick.label' properties are now synonyms of 'grid.label' properties - sprefix = axis or '' - cprefix = sprefix if _version_mpl >= '3.4' else '' # new settings + sprefix = axis or "" + cprefix = sprefix if _version_mpl >= "3.4" else "" # new settings context = not rebuild and (native or self._context_mode == 2) kwtext = self.fill( { - 'color': f'{cprefix}tick.labelcolor', # native setting sometimes avail - 'size': f'{sprefix}tick.labelsize', # native setting always avail - 'weight': 'tick.labelweight', # native setting never avail - 'family': 'font.family', # apply manually + "color": f"{cprefix}tick.labelcolor", # native setting sometimes avail + "size": f"{sprefix}tick.labelsize", # native setting always avail + "weight": "tick.labelweight", # native setting never avail + "family": "font.family", # apply manually }, context=context, ) - if kwtext.get('color', None) == 'inherit': + if kwtext.get("color", None) == "inherit": # Inheritence is not automatic for geographic # gridline labels so we apply inheritence here. - kwtext['color'] = self[f'{sprefix}tick.color'] + kwtext["color"] = self[f"{sprefix}tick.color"] return kwtext @staticmethod @@ -1226,7 +1247,7 @@ def local_files(): cdir = os.getcwd() paths = [] while cdir: # i.e. not root - for name in ('proplotrc', '.proplotrc'): + for name in ("proplotrc", ".proplotrc"): path = os.path.join(cdir, name) if os.path.isfile(path): paths.append(path) @@ -1252,13 +1273,13 @@ def local_folders(subfolder=None): cdir = os.getcwd() paths = [] if subfolder is None: - subfolder = ('cmaps', 'cycles', 'colors', 'fonts') + subfolder = ("cmaps", "cycles", "colors", "fonts") if isinstance(subfolder, str): subfolder = (subfolder,) while cdir: # i.e. not root - for prefix in ('proplot', '.proplot'): + for prefix in ("proplot", ".proplot"): for suffix in subfolder: - path = os.path.join(cdir, '_'.join((prefix, suffix))) + path = os.path.join(cdir, "_".join((prefix, suffix))) if os.path.isdir(path): paths.append(path) ndir = os.path.dirname(cdir) @@ -1272,14 +1293,14 @@ def _config_folder(): """ Get the XDG proplot folder. """ - home = os.path.expanduser('~') - base = os.environ.get('XDG_CONFIG_HOME') + home = os.path.expanduser("~") + base = os.environ.get("XDG_CONFIG_HOME") if not base: - base = os.path.join(home, '.config') - if sys.platform.startswith(('linux', 'freebsd')) and os.path.isdir(base): - configdir = os.path.join(base, 'proplot') + base = os.path.join(home, ".config") + if sys.platform.startswith(("linux", "freebsd")) and os.path.isdir(base): + configdir = os.path.join(base, "proplot") else: - configdir = os.path.join(home, '.proplot') + configdir = os.path.join(home, ".proplot") return configdir @staticmethod @@ -1299,13 +1320,13 @@ def user_file(): Configurator.local_files """ # Support both loose files and files inside .proplot - file = os.path.join(Configurator.user_folder(), 'proplotrc') - universal = os.path.join(os.path.expanduser('~'), '.proplotrc') + file = os.path.join(Configurator.user_folder(), "proplotrc") + universal = os.path.join(os.path.expanduser("~"), ".proplotrc") if os.path.isfile(universal): if file != universal and os.path.isfile(file): warnings._warn_proplot( - 'Found conflicting default user proplotrc files at ' - f'{universal!r} and {file!r}. Ignoring the second one.' + "Found conflicting default user proplotrc files at " + f"{universal!r} and {file!r}. Ignoring the second one." ) file = universal return file @@ -1327,19 +1348,19 @@ def user_folder(subfolder=None): """ # Try the XDG standard location # NOTE: This is borrowed from matplotlib.get_configdir - home = os.path.expanduser('~') - universal = folder = os.path.join(home, '.proplot') - if sys.platform.startswith(('linux', 'freebsd')): - xdg = os.environ.get('XDG_CONFIG_HOME') - xdg = xdg or os.path.join(home, '.config') - folder = os.path.join(xdg, 'proplot') + home = os.path.expanduser("~") + universal = folder = os.path.join(home, ".proplot") + if sys.platform.startswith(("linux", "freebsd")): + xdg = os.environ.get("XDG_CONFIG_HOME") + xdg = xdg or os.path.join(home, ".config") + folder = os.path.join(xdg, "proplot") # Fallback to the loose ~/.proplot if it is present # NOTE: This is critical or we might ignore previously stored settings! if os.path.isdir(universal): if folder != universal and os.path.isdir(folder): warnings._warn_proplot( - 'Found conflicting default user proplot folders at ' - f'{universal!r} and {folder!r}. Ignoring the second one.' + "Found conflicting default user proplot folders at " + f"{universal!r} and {folder!r}. Ignoring the second one." ) folder = universal # Return the folder @@ -1413,7 +1434,7 @@ def context(self, *args, mode=0, file=None, **kwargs): # Add input dictionaries for arg in args: if not isinstance(arg, dict): - raise ValueError(f'Non-dictionary argument {arg!r}.') + raise ValueError(f"Non-dictionary argument {arg!r}.") kwargs.update(arg) # Add settings from file @@ -1424,8 +1445,8 @@ def context(self, *args, mode=0, file=None, **kwargs): # Activate context object if mode not in range(3): - raise ValueError(f'Invalid mode {mode!r}.') - cls = namedtuple('RcContext', ('mode', 'kwargs', 'rc_new', 'rc_old')) + raise ValueError(f"Invalid mode {mode!r}.") + cls = namedtuple("RcContext", ("mode", "kwargs", "rc_new", "rc_old")) context = cls(mode=mode, kwargs=kwargs, rc_new={}, rc_old={}) self._context.append(context) return self @@ -1453,18 +1474,18 @@ def category(self, cat, *, trimcat=True, context=False): kw = {} if cat not in rcsetup._rc_categories: raise ValueError( - f'Invalid rc category {cat!r}. Valid categories are: ' - + ', '.join(map(repr, rcsetup._rc_categories)) - + '.' + f"Invalid rc category {cat!r}. Valid categories are: " + + ", ".join(map(repr, rcsetup._rc_categories)) + + "." ) for key in self: - if not re.match(fr'\A{cat}\.[^.]+\Z', key): + if not re.match(rf"\A{cat}\.[^.]+\Z", key): continue value = self._get_item_context(key, None if context else 0) if value is None: continue if trimcat: - key = re.sub(fr'\A{cat}\.', '', key) + key = re.sub(rf"\A{cat}\.", "", key) kw[key] = value return kw @@ -1534,7 +1555,7 @@ def update(self, *args, **kwargs): Configurator.category Configurator.fill """ - prefix, kw = '', {} + prefix, kw = "", {} if not args: pass elif len(args) == 1 and isinstance(args[0], str): @@ -1545,11 +1566,11 @@ def update(self, *args, **kwargs): prefix, kw = args else: raise ValueError( - f'Invalid arguments {args!r}. Usage is either ' - 'rc.update(dict), rc.update(kwy=value, ...), ' - 'rc.update(category, dict), or rc.update(category, key=value, ...).' + f"Invalid arguments {args!r}. Usage is either " + "rc.update(dict), rc.update(kwy=value, ...), " + "rc.update(category, dict), or rc.update(category, key=value, ...)." ) - prefix = prefix and prefix + '.' + prefix = prefix and prefix + "." kw.update(kwargs) for key, value in kw.items(): self.__setitem__(prefix + key, value) @@ -1574,42 +1595,46 @@ def _load_file(self, path): path = os.path.expanduser(path) added = set() rcdict = {} - with open(path, 'r') as fh: + with open(path, "r") as fh: for idx, line in enumerate(fh): # Strip comments - message = f'line #{idx + 1} in file {path!r}' - stripped = line.split('#', 1)[0].strip() + message = f"line #{idx + 1} in file {path!r}" + stripped = line.split("#", 1)[0].strip() if not stripped: pass # no warning continue # Parse the pair - pair = stripped.split(':', 1) + pair = stripped.split(":", 1) if len(pair) != 2: warnings._warn_proplot(f'Illegal {message}:\n{line}"') continue # Detect duplicates key, value = map(str.strip, pair) if key in added: - warnings._warn_proplot(f'Duplicate rc key {key!r} on {message}.') + warnings._warn_proplot(f"Duplicate rc key {key!r} on {message}.") added.add(key) # Get child dictionaries. Careful to have informative messages with warnings.catch_warnings(): - warnings.simplefilter('error', warnings.ProplotWarning) + warnings.simplefilter("error", warnings.ProplotWarning) try: key, value = self._validate_key(key, value) value = self._validate_value(key, value) except KeyError: - warnings.simplefilter('default', warnings.ProplotWarning) - warnings._warn_proplot(f'Invalid rc key {key!r} on {message}.') + warnings.simplefilter("default", warnings.ProplotWarning) + warnings._warn_proplot(f"Invalid rc key {key!r} on {message}.") continue except ValueError as err: - warnings.simplefilter('default', warnings.ProplotWarning) - warnings._warn_proplot(f'Invalid rc value {value!r} for key {key!r} on {message}: {err}') # noqa: E501 + warnings.simplefilter("default", warnings.ProplotWarning) + warnings._warn_proplot( + f"Invalid rc value {value!r} for key {key!r} on {message}: {err}" + ) # noqa: E501 continue except warnings.ProplotWarning as err: - warnings.simplefilter('default', warnings.ProplotWarning) - warnings._warn_proplot(f'Outdated rc key {key!r} on {message}: {err}') # noqa: E501 - warnings.simplefilter('ignore', warnings.ProplotWarning) + warnings.simplefilter("default", warnings.ProplotWarning) + warnings._warn_proplot( + f"Outdated rc key {key!r} on {message}: {err}" + ) # noqa: E501 + warnings.simplefilter("ignore", warnings.ProplotWarning) key, value = self._validate_key(key, value) value = self._validate_value(key, value) # Update the settings @@ -1640,7 +1665,7 @@ def _save_rst(path): Create an RST table file. Used for online docs. """ string = rcsetup._rst_table() - with open(path, 'w') as fh: + with open(path, "w") as fh: fh.write(string) @staticmethod @@ -1652,26 +1677,30 @@ def _save_yaml(path, user_dict=None, *, comment=False, description=False): user_table = () if user_dict: # add always-uncommented user settings user_table = rcsetup._yaml_table(user_dict, comment=False) - user_table = ('# Changed settings', user_table, '') - proplot_dict = rcsetup._rc_proplot_table if description else rcsetup._rc_proplot_default # noqa: E501 - proplot_table = rcsetup._yaml_table(proplot_dict, comment=comment, description=description) # noqa: E501 - proplot_table = ('# Proplot settings', proplot_table, '') + user_table = ("# Changed settings", user_table, "") + proplot_dict = ( + rcsetup._rc_proplot_table if description else rcsetup._rc_proplot_default + ) # noqa: E501 + proplot_table = rcsetup._yaml_table( + proplot_dict, comment=comment, description=description + ) # noqa: E501 + proplot_table = ("# Proplot settings", proplot_table, "") matplotlib_dict = rcsetup._rc_matplotlib_default matplotlib_table = rcsetup._yaml_table(matplotlib_dict, comment=comment) - matplotlib_table = ('# Matplotlib settings', matplotlib_table) + matplotlib_table = ("# Matplotlib settings", matplotlib_table) parts = ( - '#--------------------------------------------------------------------', - '# Use this file to change the default proplot and matplotlib settings.', - '# The syntax is identical to matplotlibrc syntax. For details see:', - '# https://proplot.readthedocs.io/en/latest/configuration.html', - '# https://matplotlib.org/stable/tutorials/introductory/customizing.html', - '#--------------------------------------------------------------------', + "#--------------------------------------------------------------------", + "# Use this file to change the default proplot and matplotlib settings.", + "# The syntax is identical to matplotlibrc syntax. For details see:", + "# https://proplot.readthedocs.io/en/latest/configuration.html", + "# https://matplotlib.org/stable/tutorials/introductory/customizing.html", + "#--------------------------------------------------------------------", *user_table, # empty if nothing passed *proplot_table, *matplotlib_table, ) - with open(path, 'w') as fh: - fh.write('\n'.join(parts)) + with open(path, "w") as fh: + fh.write("\n".join(parts)) def save(self, path=None, user=True, comment=None, backup=True, description=False): """ @@ -1702,13 +1731,13 @@ def save(self, path=None, user=True, comment=None, backup=True, description=Fals Configurator.load Configurator.changed """ - path = os.path.expanduser(path or '.') + path = os.path.expanduser(path or ".") if os.path.isdir(path): # includes '' - path = os.path.join(path, 'proplotrc') + path = os.path.join(path, "proplotrc") if os.path.isfile(path) and backup: - backup = path + '.bak' + backup = path + ".bak" os.rename(path, backup) - warnings._warn_proplot(f'Existing file {path!r} was moved to {backup!r}.') + warnings._warn_proplot(f"Existing file {path!r} was moved to {backup!r}.") comment = _not_none(comment, user) user_dict = self.changed if user else None self._save_yaml(path, user_dict, comment=comment, description=description) @@ -1733,7 +1762,11 @@ def changed(self): rcdict = {} for key, value in self.items(): default = rcsetup._get_default_param(key) - if isinstance(value, Real) and isinstance(default, Real) and np.isclose(value, default): # noqa: E501 + if ( + isinstance(value, Real) + and isinstance(default, Real) + and np.isclose(value, default) + ): # noqa: E501 pass elif value == default: pass @@ -1742,12 +1775,12 @@ def changed(self): # Ignore non-style-related settings. See mstyle.STYLE_BLACKLIST # TODO: For now not sure how to detect if prop cycle changed since # we cannot load it from _cmap_database in rcsetup. - rcdict.pop('interactive', None) # changed by backend - rcdict.pop('axes.prop_cycle', None) + rcdict.pop("interactive", None) # changed by backend + rcdict.pop("axes.prop_cycle", None) return _filter_style_dict(rcdict, warn=False) # Renamed methods - load_file = warnings._rename_objs('0.8.0', load_file=load) + load_file = warnings._rename_objs("0.8.0", load_file=load) # Initialize locations @@ -1768,8 +1801,9 @@ def changed(self): # Deprecated RcConfigurator = warnings._rename_objs( - '0.8.0', RcConfigurator=Configurator, + "0.8.0", + RcConfigurator=Configurator, ) inline_backend_fmt = warnings._rename_objs( - '0.6.0', inline_backend_fmt=config_inline_backend + "0.6.0", inline_backend_fmt=config_inline_backend ) diff --git a/proplot/constructor.py b/proplot/constructor.py index 17117f898..b431009ca 100644 --- a/proplot/constructor.py +++ b/proplot/constructor.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -The constructor functions used to build class instances from simple shorthand arguments. +T"he constructor functions used to build class instances from simple shorthand arguments. """ # NOTE: These functions used to be in separate files like crs.py and # ticker.py but makes more sense to group them together to ensure usage is @@ -45,14 +45,14 @@ Projection = object __all__ = [ - 'Proj', - 'Locator', - 'Formatter', - 'Scale', - 'Colormap', - 'Norm', - 'Cycle', - 'Colors', # deprecated + "Proj", + "Locator", + "Formatter", + "Scale", + "Colormap", + "Norm", + "Cycle", + "Colors", # deprecated ] # Color cycle constants @@ -63,58 +63,58 @@ # Normalizer registry NORMS = { - 'none': mcolors.NoNorm, - 'null': mcolors.NoNorm, - 'div': pcolors.DivergingNorm, - 'diverging': pcolors.DivergingNorm, - 'segmented': pcolors.SegmentedNorm, - 'segments': pcolors.SegmentedNorm, - 'log': mcolors.LogNorm, - 'linear': mcolors.Normalize, - 'power': mcolors.PowerNorm, - 'symlog': mcolors.SymLogNorm, + "none": mcolors.NoNorm, + "null": mcolors.NoNorm, + "div": pcolors.DivergingNorm, + "diverging": pcolors.DivergingNorm, + "segmented": pcolors.SegmentedNorm, + "segments": pcolors.SegmentedNorm, + "log": mcolors.LogNorm, + "linear": mcolors.Normalize, + "power": mcolors.PowerNorm, + "symlog": mcolors.SymLogNorm, } -if hasattr(mcolors, 'TwoSlopeNorm'): - NORMS['twoslope'] = mcolors.TwoSlopeNorm +if hasattr(mcolors, "TwoSlopeNorm"): + NORMS["twoslope"] = mcolors.TwoSlopeNorm # Locator registry # NOTE: Will raise error when you try to use degree-minute-second # locators with cartopy < 0.18. LOCATORS = { - 'none': mticker.NullLocator, - 'null': mticker.NullLocator, - 'auto': mticker.AutoLocator, - 'log': mticker.LogLocator, - 'maxn': mticker.MaxNLocator, - 'linear': mticker.LinearLocator, - 'multiple': mticker.MultipleLocator, - 'fixed': mticker.FixedLocator, - 'index': pticker.IndexLocator, - 'discrete': pticker.DiscreteLocator, - 'discreteminor': partial(pticker.DiscreteLocator, minor=True), - 'symlog': mticker.SymmetricalLogLocator, - 'logit': mticker.LogitLocator, - 'minor': mticker.AutoMinorLocator, - 'date': mdates.AutoDateLocator, - 'microsecond': mdates.MicrosecondLocator, - 'second': mdates.SecondLocator, - 'minute': mdates.MinuteLocator, - 'hour': mdates.HourLocator, - 'day': mdates.DayLocator, - 'weekday': mdates.WeekdayLocator, - 'month': mdates.MonthLocator, - 'year': mdates.YearLocator, - 'lon': partial(pticker.LongitudeLocator, dms=False), - 'lat': partial(pticker.LatitudeLocator, dms=False), - 'deglon': partial(pticker.LongitudeLocator, dms=False), - 'deglat': partial(pticker.LatitudeLocator, dms=False), + "none": mticker.NullLocator, + "null": mticker.NullLocator, + "auto": mticker.AutoLocator, + "log": mticker.LogLocator, + "maxn": mticker.MaxNLocator, + "linear": mticker.LinearLocator, + "multiple": mticker.MultipleLocator, + "fixed": mticker.FixedLocator, + "index": pticker.IndexLocator, + "discrete": pticker.DiscreteLocator, + "discreteminor": partial(pticker.DiscreteLocator, minor=True), + "symlog": mticker.SymmetricalLogLocator, + "logit": mticker.LogitLocator, + "minor": mticker.AutoMinorLocator, + "date": mdates.AutoDateLocator, + "microsecond": mdates.MicrosecondLocator, + "second": mdates.SecondLocator, + "minute": mdates.MinuteLocator, + "hour": mdates.HourLocator, + "day": mdates.DayLocator, + "weekday": mdates.WeekdayLocator, + "month": mdates.MonthLocator, + "year": mdates.YearLocator, + "lon": partial(pticker.LongitudeLocator, dms=False), + "lat": partial(pticker.LatitudeLocator, dms=False), + "deglon": partial(pticker.LongitudeLocator, dms=False), + "deglat": partial(pticker.LatitudeLocator, dms=False), } -if hasattr(mpolar, 'ThetaLocator'): - LOCATORS['theta'] = mpolar.ThetaLocator -if _version_cartopy >= '0.18': - LOCATORS['dms'] = partial(pticker.DegreeLocator, dms=True) - LOCATORS['dmslon'] = partial(pticker.LongitudeLocator, dms=True) - LOCATORS['dmslat'] = partial(pticker.LatitudeLocator, dms=True) +if hasattr(mpolar, "ThetaLocator"): + LOCATORS["theta"] = mpolar.ThetaLocator +if _version_cartopy >= "0.18": + LOCATORS["dms"] = partial(pticker.DegreeLocator, dms=True) + LOCATORS["dmslon"] = partial(pticker.LongitudeLocator, dms=True) + LOCATORS["dmslat"] = partial(pticker.LatitudeLocator, dms=True) # Formatter registry # NOTE: Critical to use SimpleFormatter for cardinal formatters rather than @@ -124,56 +124,70 @@ # NOTE: Will raise error when you try to use degree-minute-second # formatters with cartopy < 0.18. FORMATTERS = { # note default LogFormatter uses ugly e+00 notation - 'none': mticker.NullFormatter, - 'null': mticker.NullFormatter, - 'auto': pticker.AutoFormatter, - 'date': mdates.AutoDateFormatter, - 'scalar': mticker.ScalarFormatter, - 'simple': pticker.SimpleFormatter, - 'fixed': mticker.FixedLocator, - 'index': pticker.IndexFormatter, - 'sci': pticker.SciFormatter, - 'sigfig': pticker.SigFigFormatter, - 'frac': pticker.FracFormatter, - 'func': mticker.FuncFormatter, - 'strmethod': mticker.StrMethodFormatter, - 'formatstr': mticker.FormatStrFormatter, - 'datestr': mdates.DateFormatter, - 'log': mticker.LogFormatterSciNotation, # NOTE: this is subclass of Mathtext class - 'logit': mticker.LogitFormatter, - 'eng': mticker.EngFormatter, - 'percent': mticker.PercentFormatter, - 'e': partial(pticker.FracFormatter, symbol=r'$e$', number=np.e), - 'pi': partial(pticker.FracFormatter, symbol=r'$\pi$', number=np.pi), - 'tau': partial(pticker.FracFormatter, symbol=r'$\tau$', number=2 * np.pi), - 'lat': partial(pticker.SimpleFormatter, negpos='SN'), - 'lon': partial(pticker.SimpleFormatter, negpos='WE', wraprange=(-180, 180)), - 'deg': partial(pticker.SimpleFormatter, suffix='\N{DEGREE SIGN}'), - 'deglat': partial(pticker.SimpleFormatter, suffix='\N{DEGREE SIGN}', negpos='SN'), - 'deglon': partial(pticker.SimpleFormatter, suffix='\N{DEGREE SIGN}', negpos='WE', wraprange=(-180, 180)), # noqa: E501 - 'math': mticker.LogFormatterMathtext, # deprecated (use SciNotation subclass) + "none": mticker.NullFormatter, + "null": mticker.NullFormatter, + "auto": pticker.AutoFormatter, + "date": mdates.AutoDateFormatter, + "scalar": mticker.ScalarFormatter, + "simple": pticker.SimpleFormatter, + "fixed": mticker.FixedLocator, + "index": pticker.IndexFormatter, + "sci": pticker.SciFormatter, + "sigfig": pticker.SigFigFormatter, + "frac": pticker.FracFormatter, + "func": mticker.FuncFormatter, + "strmethod": mticker.StrMethodFormatter, + "formatstr": mticker.FormatStrFormatter, + "datestr": mdates.DateFormatter, + "log": mticker.LogFormatterSciNotation, # NOTE: this is subclass of Mathtext class + "logit": mticker.LogitFormatter, + "eng": mticker.EngFormatter, + "percent": mticker.PercentFormatter, + "e": partial(pticker.FracFormatter, symbol=r"$e$", number=np.e), + "pi": partial(pticker.FracFormatter, symbol=r"$\pi$", number=np.pi), + "tau": partial(pticker.FracFormatter, symbol=r"$\tau$", number=2 * np.pi), + "lat": partial(pticker.SimpleFormatter, negpos="SN"), + "lon": partial(pticker.SimpleFormatter, negpos="WE", wraprange=(-180, 180)), + "deg": partial(pticker.SimpleFormatter, suffix="\N{DEGREE SIGN}"), + "deglat": partial(pticker.SimpleFormatter, suffix="\N{DEGREE SIGN}", negpos="SN"), + "deglon": partial( + pticker.SimpleFormatter, + suffix="\N{DEGREE SIGN}", + negpos="WE", + wraprange=(-180, 180), + ), # noqa: E501 + "math": mticker.LogFormatterMathtext, # deprecated (use SciNotation subclass) } -if hasattr(mpolar, 'ThetaFormatter'): - FORMATTERS['theta'] = mpolar.ThetaFormatter -if hasattr(mdates, 'ConciseDateFormatter'): - FORMATTERS['concise'] = mdates.ConciseDateFormatter -if _version_cartopy >= '0.18': - FORMATTERS['dms'] = partial(pticker.DegreeFormatter, dms=True) - FORMATTERS['dmslon'] = partial(pticker.LongitudeFormatter, dms=True) - FORMATTERS['dmslat'] = partial(pticker.LatitudeFormatter, dms=True) +if hasattr(mpolar, "ThetaFormatter"): + FORMATTERS["theta"] = mpolar.ThetaFormatter +if hasattr(mdates, "ConciseDateFormatter"): + FORMATTERS["concise"] = mdates.ConciseDateFormatter +if _version_cartopy >= "0.18": + FORMATTERS["dms"] = partial(pticker.DegreeFormatter, dms=True) + FORMATTERS["dmslon"] = partial(pticker.LongitudeFormatter, dms=True) + FORMATTERS["dmslat"] = partial(pticker.LatitudeFormatter, dms=True) # Scale registry and presets SCALES = mscale._scale_mapping SCALES_PRESETS = { - 'quadratic': ('power', 2,), - 'cubic': ('power', 3,), - 'quartic': ('power', 4,), - 'height': ('exp', np.e, -1 / 7, 1013.25, True), - 'pressure': ('exp', np.e, -1 / 7, 1013.25, False), - 'db': ('exp', 10, 1, 0.1, True), - 'idb': ('exp', 10, 1, 0.1, False), - 'np': ('exp', np.e, 1, 1, True), - 'inp': ('exp', np.e, 1, 1, False), + "quadratic": ( + "power", + 2, + ), + "cubic": ( + "power", + 3, + ), + "quartic": ( + "power", + 4, + ), + "height": ("exp", np.e, -1 / 7, 1013.25, True), + "pressure": ("exp", np.e, -1 / 7, 1013.25, False), + "db": ("exp", 10, 1, 0.1, True), + "idb": ("exp", 10, 1, 0.1, False), + "np": ("exp", np.e, 1, 1, True), + "inp": ("exp", np.e, 1, 1, False), } mscale.register_scale(pscale.CutoffScale) mscale.register_scale(pscale.ExpScale) @@ -190,94 +204,100 @@ # Cartopy projection registry and basemap default keyword args # NOTE: Normally basemap raises error if you omit keyword args PROJ_DEFAULTS = { - 'geos': {'lon_0': 0}, - 'eck4': {'lon_0': 0}, - 'moll': {'lon_0': 0}, - 'hammer': {'lon_0': 0}, - 'kav7': {'lon_0': 0}, - 'sinu': {'lon_0': 0}, - 'vandg': {'lon_0': 0}, - 'mbtfpq': {'lon_0': 0}, - 'robin': {'lon_0': 0}, - 'ortho': {'lon_0': 0, 'lat_0': 0}, - 'nsper': {'lon_0': 0, 'lat_0': 0}, - 'aea': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3}, - 'eqdc': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3}, - 'cass': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3}, - 'gnom': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3}, - 'poly': {'lon_0': 0, 'lat_0': 0, 'width': 10000e3, 'height': 10000e3}, - 'npaeqd': {'lon_0': 0, 'boundinglat': 10}, # NOTE: everything breaks if you - 'nplaea': {'lon_0': 0, 'boundinglat': 10}, # try to set boundinglat to zero - 'npstere': {'lon_0': 0, 'boundinglat': 10}, - 'spaeqd': {'lon_0': 0, 'boundinglat': -10}, - 'splaea': {'lon_0': 0, 'boundinglat': -10}, - 'spstere': {'lon_0': 0, 'boundinglat': -10}, - 'lcc': { - 'lon_0': 0, 'lat_0': 40, 'lat_1': 35, 'lat_2': 45, # use cartopy defaults - 'width': 20000e3, 'height': 15000e3 + "geos": {"lon_0": 0}, + "eck4": {"lon_0": 0}, + "moll": {"lon_0": 0}, + "hammer": {"lon_0": 0}, + "kav7": {"lon_0": 0}, + "sinu": {"lon_0": 0}, + "vandg": {"lon_0": 0}, + "mbtfpq": {"lon_0": 0}, + "robin": {"lon_0": 0}, + "ortho": {"lon_0": 0, "lat_0": 0}, + "nsper": {"lon_0": 0, "lat_0": 0}, + "aea": {"lon_0": 0, "lat_0": 90, "width": 15000e3, "height": 15000e3}, + "eqdc": {"lon_0": 0, "lat_0": 90, "width": 15000e3, "height": 15000e3}, + "cass": {"lon_0": 0, "lat_0": 90, "width": 15000e3, "height": 15000e3}, + "gnom": {"lon_0": 0, "lat_0": 90, "width": 15000e3, "height": 15000e3}, + "poly": {"lon_0": 0, "lat_0": 0, "width": 10000e3, "height": 10000e3}, + "npaeqd": {"lon_0": 0, "boundinglat": 10}, # NOTE: everything breaks if you + "nplaea": {"lon_0": 0, "boundinglat": 10}, # try to set boundinglat to zero + "npstere": {"lon_0": 0, "boundinglat": 10}, + "spaeqd": {"lon_0": 0, "boundinglat": -10}, + "splaea": {"lon_0": 0, "boundinglat": -10}, + "spstere": {"lon_0": 0, "boundinglat": -10}, + "lcc": { + "lon_0": 0, + "lat_0": 40, + "lat_1": 35, + "lat_2": 45, # use cartopy defaults + "width": 20000e3, + "height": 15000e3, }, - 'tmerc': { - 'lon_0': 0, 'lat_0': 0, 'width': 10000e3, 'height': 10000e3 - }, - 'merc': { - 'llcrnrlat': -80, 'urcrnrlat': 84, 'llcrnrlon': -180, 'urcrnrlon': 180 - }, - 'omerc': { - 'lat_0': 0, 'lon_0': 0, 'lat_1': -10, 'lat_2': 10, - 'lon_1': 0, 'lon_2': 0, 'width': 10000e3, 'height': 10000e3 + "tmerc": {"lon_0": 0, "lat_0": 0, "width": 10000e3, "height": 10000e3}, + "merc": {"llcrnrlat": -80, "urcrnrlat": 84, "llcrnrlon": -180, "urcrnrlon": 180}, + "omerc": { + "lat_0": 0, + "lon_0": 0, + "lat_1": -10, + "lat_2": 10, + "lon_1": 0, + "lon_2": 0, + "width": 10000e3, + "height": 10000e3, }, } if ccrs is None: PROJS = {} else: PROJS = { - 'aitoff': pproj.Aitoff, - 'hammer': pproj.Hammer, - 'kav7': pproj.KavrayskiyVII, - 'wintri': pproj.WinkelTripel, - 'npgnom': pproj.NorthPolarGnomonic, - 'spgnom': pproj.SouthPolarGnomonic, - 'npaeqd': pproj.NorthPolarAzimuthalEquidistant, - 'spaeqd': pproj.SouthPolarAzimuthalEquidistant, - 'nplaea': pproj.NorthPolarLambertAzimuthalEqualArea, - 'splaea': pproj.SouthPolarLambertAzimuthalEqualArea, + "aitoff": pproj.Aitoff, + "hammer": pproj.Hammer, + "kav7": pproj.KavrayskiyVII, + "wintri": pproj.WinkelTripel, + "npgnom": pproj.NorthPolarGnomonic, + "spgnom": pproj.SouthPolarGnomonic, + "npaeqd": pproj.NorthPolarAzimuthalEquidistant, + "spaeqd": pproj.SouthPolarAzimuthalEquidistant, + "nplaea": pproj.NorthPolarLambertAzimuthalEqualArea, + "splaea": pproj.SouthPolarLambertAzimuthalEqualArea, } PROJS_MISSING = { - 'aea': 'AlbersEqualArea', - 'aeqd': 'AzimuthalEquidistant', - 'cyl': 'PlateCarree', # only basemap name not matching PROJ - 'eck1': 'EckertI', - 'eck2': 'EckertII', - 'eck3': 'EckertIII', - 'eck4': 'EckertIV', - 'eck5': 'EckertV', - 'eck6': 'EckertVI', - 'eqc': 'PlateCarree', # actual PROJ name - 'eqdc': 'EquidistantConic', - 'eqearth': 'EqualEarth', # better looking Robinson; not in basemap - 'euro': 'EuroPP', # Europe; not in basemap or PROJ - 'geos': 'Geostationary', - 'gnom': 'Gnomonic', - 'igh': 'InterruptedGoodeHomolosine', # not in basemap - 'laea': 'LambertAzimuthalEqualArea', - 'lcc': 'LambertConformal', - 'lcyl': 'LambertCylindrical', # not in basemap or PROJ - 'merc': 'Mercator', - 'mill': 'Miller', - 'moll': 'Mollweide', - 'npstere': 'NorthPolarStereo', # np/sp stuff not in PROJ - 'nsper': 'NearsidePerspective', - 'ortho': 'Orthographic', - 'osgb': 'OSGB', # UK; not in basemap or PROJ - 'osni': 'OSNI', # Ireland; not in basemap or PROJ - 'pcarree': 'PlateCarree', # common alternate name - 'robin': 'Robinson', - 'rotpole': 'RotatedPole', - 'sinu': 'Sinusoidal', - 'spstere': 'SouthPolarStereo', - 'stere': 'Stereographic', - 'tmerc': 'TransverseMercator', - 'utm': 'UTM', # not in basemap + "aea": "AlbersEqualArea", + "aeqd": "AzimuthalEquidistant", + "cyl": "PlateCarree", # only basemap name not matching PROJ + "eck1": "EckertI", + "eck2": "EckertII", + "eck3": "EckertIII", + "eck4": "EckertIV", + "eck5": "EckertV", + "eck6": "EckertVI", + "eqc": "PlateCarree", # actual PROJ name + "eqdc": "EquidistantConic", + "eqearth": "EqualEarth", # better looking Robinson; not in basemap + "euro": "EuroPP", # Europe; not in basemap or PROJ + "geos": "Geostationary", + "gnom": "Gnomonic", + "igh": "InterruptedGoodeHomolosine", # not in basemap + "laea": "LambertAzimuthalEqualArea", + "lcc": "LambertConformal", + "lcyl": "LambertCylindrical", # not in basemap or PROJ + "merc": "Mercator", + "mill": "Miller", + "moll": "Mollweide", + "npstere": "NorthPolarStereo", # np/sp stuff not in PROJ + "nsper": "NearsidePerspective", + "ortho": "Orthographic", + "osgb": "OSGB", # UK; not in basemap or PROJ + "osni": "OSNI", # Ireland; not in basemap or PROJ + "pcarree": "PlateCarree", # common alternate name + "robin": "Robinson", + "rotpole": "RotatedPole", + "sinu": "Sinusoidal", + "spstere": "SouthPolarStereo", + "stere": "Stereographic", + "tmerc": "TransverseMercator", + "utm": "UTM", # not in basemap } for _key, _cls in tuple(PROJS_MISSING.items()): if hasattr(ccrs, _cls): @@ -285,51 +305,48 @@ del PROJS_MISSING[_key] if PROJS_MISSING: warnings._warn_proplot( - 'The following cartopy projection(s) are unavailable: ' - + ', '.join(map(repr, PROJS_MISSING)) - + ' . Please consider updating cartopy.' - ) - PROJS_TABLE = ( - 'The known cartopy projection classes are:\n' - + '\n'.join( - ' ' + key + ' ' * (max(map(len, PROJS)) - len(key) + 10) + cls.__name__ - for key, cls in PROJS.items() + "The following cartopy projection(s) are unavailable: " + + ", ".join(map(repr, PROJS_MISSING)) + + " . Please consider updating cartopy." ) + PROJS_TABLE = "The known cartopy projection classes are:\n" + "\n".join( + " " + key + " " * (max(map(len, PROJS)) - len(key) + 10) + cls.__name__ + for key, cls in PROJS.items() ) # Geographic feature properties FEATURES_CARTOPY = { # positional arguments passed to NaturalEarthFeature - 'land': ('physical', 'land'), - 'ocean': ('physical', 'ocean'), - 'lakes': ('physical', 'lakes'), - 'coast': ('physical', 'coastline'), - 'rivers': ('physical', 'rivers_lake_centerlines'), - 'borders': ('cultural', 'admin_0_boundary_lines_land'), - 'innerborders': ('cultural', 'admin_1_states_provinces_lakes'), + "land": ("physical", "land"), + "ocean": ("physical", "ocean"), + "lakes": ("physical", "lakes"), + "coast": ("physical", "coastline"), + "rivers": ("physical", "rivers_lake_centerlines"), + "borders": ("cultural", "admin_0_boundary_lines_land"), + "innerborders": ("cultural", "admin_1_states_provinces_lakes"), } FEATURES_BASEMAP = { # names of relevant basemap methods - 'land': 'fillcontinents', - 'coast': 'drawcoastlines', - 'rivers': 'drawrivers', - 'borders': 'drawcountries', - 'innerborders': 'drawstates', + "land": "fillcontinents", + "coast": "drawcoastlines", + "rivers": "drawrivers", + "borders": "drawcountries", + "innerborders": "drawstates", } # Resolution names # NOTE: Maximum basemap resolutions are much finer than cartopy RESOS_CARTOPY = { - 'lo': '110m', - 'med': '50m', - 'hi': '10m', - 'x-hi': '10m', # extra high - 'xx-hi': '10m', # extra extra high + "lo": "110m", + "med": "50m", + "hi": "10m", + "x-hi": "10m", # extra high + "xx-hi": "10m", # extra extra high } RESOS_BASEMAP = { - 'lo': 'c', # coarse - 'med': 'l', - 'hi': 'i', # intermediate - 'x-hi': 'h', - 'xx-hi': 'f', # fine + "lo": "c", # coarse + "med": "l", + "hi": "i", # intermediate + "x-hi": "h", + "xx-hi": "f", # fine } @@ -361,11 +378,18 @@ def _modify_colormap(cmap, *, cut, left, right, reverse, shift, alpha, samples): @warnings._rename_kwargs( - '0.8.0', fade='saturation', shade='luminance', to_listed='discrete' + "0.8.0", fade="saturation", shade="luminance", to_listed="discrete" ) def Colormap( - *args, name=None, listmode='perceptual', filemode='continuous', discrete=False, - cycle=None, save=False, save_kw=None, **kwargs + *args, + name=None, + listmode="perceptual", + filemode="continuous", + discrete=False, + cycle=None, + save=False, + save_kw=None, + **kwargs, ): """ Generate, retrieve, modify, and/or merge instances of @@ -533,6 +557,7 @@ def Colormap( proplot.constructor.Cycle proplot.utils.get_colors """ + # Helper function # NOTE: Very careful here! Try to support common use cases. For example # adding opacity gradations to colormaps with Colormap('cmap', alpha=(0.5, 1)) @@ -549,27 +574,27 @@ def _pop_modification(key): values = (None,) else: raise ValueError( - f'Got {len(args)} colormap-specs ' - f'but {len(value)} values for {key!r}.' + f"Got {len(args)} colormap-specs " + f"but {len(value)} values for {key!r}." ) return value, values # Parse keyword args that can apply to the merged colormap or each one - hsla = _pop_props(kwargs, 'hsla') - if not args and hsla.keys() - {'alpha'}: + hsla = _pop_props(kwargs, "hsla") + if not args and hsla.keys() - {"alpha"}: args = (hsla,) else: kwargs.update(hsla) - default_luminance = kwargs.pop('default_luminance', None) # used internally - cut, cuts = _pop_modification('cut') - left, lefts = _pop_modification('left') - right, rights = _pop_modification('right') - shift, shifts = _pop_modification('shift') - reverse, reverses = _pop_modification('reverse') - samples, sampless = _pop_modification('samples') - alpha, alphas = _pop_modification('alpha') - luminance, luminances = _pop_modification('luminance') - saturation, saturations = _pop_modification('saturation') + default_luminance = kwargs.pop("default_luminance", None) # used internally + cut, cuts = _pop_modification("cut") + left, lefts = _pop_modification("left") + right, rights = _pop_modification("right") + shift, shifts = _pop_modification("shift") + reverse, reverses = _pop_modification("reverse") + samples, sampless = _pop_modification("samples") + alpha, alphas = _pop_modification("alpha") + luminance, luminances = _pop_modification("luminance") + saturation, saturations = _pop_modification("saturation") if luminance is not None: luminances = (luminance,) * len(args) if saturation is not None: @@ -578,41 +603,63 @@ def _pop_modification(key): # Issue warnings and errors if not args: raise ValueError( - 'Colormap() requires either positional arguments or ' + "Colormap() requires either positional arguments or " "'hue', 'chroma', 'saturation', and/or 'luminance' keywords." ) - deprecated = {'listed': 'discrete', 'linear': 'continuous'} + deprecated = {"listed": "discrete", "linear": "continuous"} if listmode in deprecated: oldmode, listmode = listmode, deprecated[listmode] warnings._warn_proplot( - f'Please use listmode={listmode!r} instead of listmode={oldmode!r}.' - 'Option was renamed in v0.8 and will be removed in a future relase.' + f"Please use listmode={listmode!r} instead of listmode={oldmode!r}." + "Option was renamed in v0.8 and will be removed in a future relase." ) - options = {'discrete', 'continuous', 'perceptual'} - for key, mode in zip(('listmode', 'filemode'), (listmode, filemode)): + options = {"discrete", "continuous", "perceptual"} + for key, mode in zip(("listmode", "filemode"), (listmode, filemode)): if mode not in options: raise ValueError( - f'Invalid {key}={mode!r}. Options are: ' - + ', '.join(map(repr, options)) - + '.' + f"Invalid {key}={mode!r}. Options are: " + + ", ".join(map(repr, options)) + + "." ) # Loop through colormaps cmaps = [] - for arg, icut, ileft, iright, ireverse, ishift, isamples, iluminance, isaturation, ialpha in zip( # noqa: E501 - args, cuts, lefts, rights, reverses, shifts, sampless, luminances, saturations, alphas # noqa: E501 + for ( + arg, + icut, + ileft, + iright, + ireverse, + ishift, + isamples, + iluminance, + isaturation, + ialpha, + ) in zip( # noqa: E501 + args, + cuts, + lefts, + rights, + reverses, + shifts, + sampless, + luminances, + saturations, + alphas, # noqa: E501 ): # Load registered colormaps and maps on file # TODO: Document how 'listmode' also affects loaded files if isinstance(arg, str): - if '.' in arg and os.path.isfile(arg): - if filemode == 'discrete': + if "." in arg and os.path.isfile(arg): + if filemode == "discrete": arg = pcolors.DiscreteColormap.from_file(arg) else: arg = pcolors.ContinuousColormap.from_file(arg) else: + # FIXME: This error is baffling too me. Colors and colormaps + # are used interchangeable here try: - arg = pcolors._cmap_database[arg] + arg = pcolors._cmap_database.get_cmap(arg) except KeyError: pass @@ -626,12 +673,13 @@ def _pop_modification(key): # List of color tuples or color strings, i.e. iterable of iterables elif ( - not isinstance(arg, str) and np.iterable(arg) + not isinstance(arg, str) + and np.iterable(arg) and all(np.iterable(color) for color in arg) ): - if listmode == 'discrete': + if listmode == "discrete": cmap = pcolors.DiscreteColormap(arg) - elif listmode == 'continuous': + elif listmode == "continuous": cmap = pcolors.ContinuousColormap.from_list(arg) else: cmap = pcolors.PerceptualColormap.from_list(arg) @@ -639,18 +687,18 @@ def _pop_modification(key): # Monochrome colormap from input color # NOTE: Do not print color names in error message. Too long to be useful. else: - jreverse = isinstance(arg, str) and arg[-2:] == '_r' + jreverse = isinstance(arg, str) and arg[-2:] == "_r" if jreverse: arg = arg[:-2] try: color = to_rgba(arg, cycle=cycle) except (ValueError, TypeError): - message = f'Invalid colormap, color cycle, or color {arg!r}.' - if isinstance(arg, str) and arg[:1] != '#': + message = f"Invalid colormap, color cycle, or color {arg!r}." + if isinstance(arg, str) and arg[:1] != "#": message += ( - ' Options include: ' - + ', '.join(sorted(map(repr, pcolors._cmap_database))) - + '.' + " Options include: " + + ", ".join(sorted(map(repr, pcolors._cmap_database))) + + "." ) raise ValueError(message) from None iluminance = _not_none(iluminance, default_luminance) @@ -662,8 +710,14 @@ def _pop_modification(key): # Modify the colormap cmap = _modify_colormap( - cmap, cut=icut, left=ileft, right=iright, - reverse=ireverse, shift=ishift, alpha=ialpha, samples=isamples, + cmap, + cut=icut, + left=ileft, + right=iright, + reverse=ireverse, + shift=ishift, + alpha=ialpha, + samples=isamples, ) cmaps.append(cmap) @@ -677,8 +731,14 @@ def _pop_modification(key): if discrete and isinstance(cmap, pcolors.ContinuousColormap): # noqa: E501 samples = _not_none(samples, DEFAULT_CYCLE_SAMPLES) cmap = _modify_colormap( - cmap, cut=cut, left=left, right=right, - reverse=reverse, shift=shift, alpha=alpha, samples=samples + cmap, + cut=cut, + left=left, + right=right, + reverse=reverse, + shift=shift, + alpha=alpha, + samples=samples, ) # Initialize @@ -691,8 +751,8 @@ def _pop_modification(key): else: cmap.name = name if not isinstance(name, str): - raise ValueError('The colormap name must be a string.') - pcolors._cmap_database[name] = cmap + raise ValueError("The colormap name must be a string.") + pcolors._cmap_database.register(cmap, name=name) # Save the colormap if save: @@ -787,9 +847,9 @@ def Cycle(*args, N=None, samples=None, name=None, **kwargs): """ # Parse keyword arguments that rotate through other properties # besides color cycles. - props = _pop_props(kwargs, 'line') - if 'sizes' in kwargs: # special case, gets translated back by scatter() - props.setdefault('markersize', kwargs.pop('sizes')) + props = _pop_props(kwargs, "line") + if "sizes" in kwargs: # special case, gets translated back by scatter() + props.setdefault("markersize", kwargs.pop("sizes")) samples = _not_none(samples=samples, N=N) # trigger Colormap default for key, value in tuple(props.items()): # permit in-place modification if value is None: @@ -800,15 +860,15 @@ def Cycle(*args, N=None, samples=None, name=None, **kwargs): # If args is non-empty, means we want color cycle; otherwise is black if not args: - props.setdefault('color', ['black']) + props.setdefault("color", ["black"]) if kwargs: - warnings._warn_proplot(f'Ignoring Cycle() keyword arg(s) {kwargs}.') + warnings._warn_proplot(f"Ignoring Cycle() keyword arg(s) {kwargs}.") dicts = () # Merge cycler objects and/or update cycler objects with input kwargs elif all(isinstance(arg, cycler.Cycler) for arg in args): if kwargs: - warnings._warn_proplot(f'Ignoring Cycle() keyword arg(s) {kwargs}.') + warnings._warn_proplot(f"Ignoring Cycle() keyword arg(s) {kwargs}.") if len(args) == 1 and not props: return args[0] dicts = tuple(arg.by_key() for arg in args) @@ -818,14 +878,16 @@ def Cycle(*args, N=None, samples=None, name=None, **kwargs): # someone might be trying to make qualitative colormap for use in 2D plot else: if isinstance(args[-1], Number): - args, samples = args[:-1], _not_none(samples_positional=args[-1], samples=samples) # noqa: #501 - kwargs.setdefault('listmode', 'discrete') - kwargs.setdefault('filemode', 'discrete') - kwargs['discrete'] = True # triggers application of default 'samples' - kwargs['default_luminance'] = DEFAULT_CYCLE_LUMINANCE + args, samples = args[:-1], _not_none( + samples_positional=args[-1], samples=samples + ) # noqa: #501 + kwargs.setdefault("listmode", "discrete") + kwargs.setdefault("filemode", "discrete") + kwargs["discrete"] = True # triggers application of default 'samples' + kwargs["default_luminance"] = DEFAULT_CYCLE_LUMINANCE cmap = Colormap(*args, name=name, samples=samples, **kwargs) name = _not_none(name, cmap.name) - dict_ = {'color': [c if isinstance(c, str) else to_hex(c) for c in cmap.colors]} + dict_ = {"color": [c if isinstance(c, str) else to_hex(c) for c in cmap.colors]} dicts = (dict_,) # Update the cyler property @@ -839,7 +901,7 @@ def Cycle(*args, N=None, samples=None, name=None, **kwargs): maxlen = np.lcm.reduce([len(value) for value in props.values()]) props = {key: value * (maxlen // len(value)) for key, value in props.items()} cycle = cycler.cycler(**props) - cycle.name = _not_none(name, '_no_name') + cycle.name = _not_none(name, "_no_name") return cycle @@ -897,15 +959,15 @@ def Norm(norm, *args, **kwargs): if isinstance(norm, mcolors.Normalize): return copy.copy(norm) if not isinstance(norm, str): - raise ValueError(f'Invalid norm name {norm!r}. Must be string.') + raise ValueError(f"Invalid norm name {norm!r}. Must be string.") if norm not in NORMS: raise ValueError( - f'Unknown normalizer {norm!r}. Options are: ' - + ', '.join(map(repr, NORMS)) - + '.' + f"Unknown normalizer {norm!r}. Options are: " + + ", ".join(map(repr, NORMS)) + + "." ) - if norm == 'symlog' and not args and 'linthresh' not in kwargs: - kwargs['linthresh'] = 1 # special case, needs argument + if norm == "symlog" and not args and "linthresh" not in kwargs: + kwargs["linthresh"] = 1 # special case, needs argument return NORMS[norm](*args, **kwargs) @@ -992,30 +1054,32 @@ def Locator(locator, *args, discrete=False, **kwargs): proplot.axes.Axes.colorbar proplot.constructor.Formatter """ # noqa: E501 - if np.iterable(locator) and not isinstance(locator, str) and not all( - isinstance(num, Number) for num in locator + if ( + np.iterable(locator) + and not isinstance(locator, str) + and not all(isinstance(num, Number) for num in locator) ): locator, *args = *locator, *args if isinstance(locator, mticker.Locator): return copy.copy(locator) if isinstance(locator, str): - if locator == 'index': # defaults + if locator == "index": # defaults args = args or (1,) if len(args) == 1: args = (*args, 0) - elif locator in ('logminor', 'logitminor', 'symlogminor'): # presets - locator, _ = locator.split('minor') - if locator == 'logit': - kwargs.setdefault('minor', True) + elif locator in ("logminor", "logitminor", "symlogminor"): # presets + locator, _ = locator.split("minor") + if locator == "logit": + kwargs.setdefault("minor", True) else: - kwargs.setdefault('subs', np.arange(1, 10)) + kwargs.setdefault("subs", np.arange(1, 10)) if locator in LOCATORS: locator = LOCATORS[locator](*args, **kwargs) else: raise ValueError( - f'Unknown locator {locator!r}. Options are: ' - + ', '.join(map(repr, LOCATORS)) - + '.' + f"Unknown locator {locator!r}. Options are: " + + ", ".join(map(repr, LOCATORS)) + + "." ) elif locator is True: locator = mticker.AutoLocator(*args, **kwargs) @@ -1030,7 +1094,7 @@ def Locator(locator, *args, discrete=False, **kwargs): else: locator = mticker.FixedLocator(locator, *args, **kwargs) else: - raise ValueError(f'Invalid locator {locator!r}.') + raise ValueError(f"Invalid locator {locator!r}.") return locator @@ -1138,25 +1202,27 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs): proplot.axes.Axes.colorbar proplot.constructor.Locator """ # noqa: E501 - if np.iterable(formatter) and not isinstance(formatter, str) and not all( - isinstance(item, str) for item in formatter + if ( + np.iterable(formatter) + and not isinstance(formatter, str) + and not all(isinstance(item, str) for item in formatter) ): formatter, *args = *formatter, *args if isinstance(formatter, mticker.Formatter): return copy.copy(formatter) if isinstance(formatter, str): - if re.search(r'{x(:.+)?}', formatter): # str.format + if re.search(r"{x(:.+)?}", formatter): # str.format formatter = mticker.StrMethodFormatter(formatter, *args, **kwargs) - elif '%' in formatter: # str % format + elif "%" in formatter: # str % format cls = mdates.DateFormatter if date else mticker.FormatStrFormatter formatter = cls(formatter, *args, **kwargs) elif formatter in FORMATTERS: formatter = FORMATTERS[formatter](*args, **kwargs) else: raise ValueError( - f'Unknown formatter {formatter!r}. Options are: ' - + ', '.join(map(repr, FORMATTERS)) - + '.' + f"Unknown formatter {formatter!r}. Options are: " + + ", ".join(map(repr, FORMATTERS)) + + "." ) elif formatter is True: formatter = pticker.AutoFormatter(*args, **kwargs) @@ -1167,7 +1233,7 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs): elif callable(formatter): formatter = mticker.FuncFormatter(formatter, *args, **kwargs) else: - raise ValueError(f'Invalid formatter {formatter!r}.') + raise ValueError(f"Invalid formatter {formatter!r}.") return formatter @@ -1244,29 +1310,36 @@ def Scale(scale, *args, **kwargs): if isinstance(scale, mscale.ScaleBase): return copy.copy(scale) if not isinstance(scale, str): - raise ValueError(f'Invalid scale name {scale!r}. Must be string.') + raise ValueError(f"Invalid scale name {scale!r}. Must be string.") scale = scale.lower() if scale in SCALES_PRESETS: if args or kwargs: warnings._warn_proplot( - f'Scale {scale!r} is a scale *preset*. Ignoring positional ' - 'argument(s): {args} and keyword argument(s): {kwargs}. ' + f"Scale {scale!r} is a scale *preset*. Ignoring positional " + "argument(s): {args} and keyword argument(s): {kwargs}. " ) scale, *args = SCALES_PRESETS[scale] if scale in SCALES: scale = SCALES[scale] else: raise ValueError( - f'Unknown scale or preset {scale!r}. Options are: ' - + ', '.join(map(repr, (*SCALES, *SCALES_PRESETS))) - + '.' + f"Unknown scale or preset {scale!r}. Options are: " + + ", ".join(map(repr, (*SCALES, *SCALES_PRESETS))) + + "." ) return scale(*args, **kwargs) def Proj( - name, backend=None, - lon0=None, lon_0=None, lat0=None, lat_0=None, lonlim=None, latlim=None, **kwargs + name, + backend=None, + lon0=None, + lon_0=None, + lat0=None, + lat_0=None, + lonlim=None, + latlim=None, + **kwargs, ): """ Return a `cartopy.crs.Projection` or `~mpl_toolkits.basemap.Basemap` instance. @@ -1431,71 +1504,73 @@ def Proj( latlim = _not_none(latlim, default=(None, None)) is_crs = Projection is not object and isinstance(name, Projection) is_basemap = Basemap is not object and isinstance(name, Basemap) - include_axes = kwargs.pop('include_axes', False) # for error message - if backend is not None and backend not in ('cartopy', 'basemap'): + include_axes = kwargs.pop("include_axes", False) # for error message + if backend is not None and backend not in ("cartopy", "basemap"): raise ValueError( f"Invalid backend={backend!r}. Options are 'cartopy' or 'basemap'." ) if not is_crs and not is_basemap: - backend = _not_none(backend, rc['geo.backend']) + backend = _not_none(backend, rc["geo.backend"]) if not isinstance(name, str): raise ValueError( - f'Unexpected projection {name!r}. Must be PROJ string name, ' - 'cartopy.crs.Projection, or mpl_toolkits.basemap.Basemap.' + f"Unexpected projection {name!r}. Must be PROJ string name, " + "cartopy.crs.Projection, or mpl_toolkits.basemap.Basemap." ) for key_proj, key_cartopy, value in ( - ('lon_0', 'central_longitude', lon0), - ('lat_0', 'central_latitude', lat0), - ('llcrnrlon', 'min_longitude', lonlim[0]), - ('urcrnrlon', 'max_longitude', lonlim[1]), - ('llcrnrlat', 'min_latitude', latlim[0]), - ('urcrnrlat', 'max_latitude', latlim[1]), + ("lon_0", "central_longitude", lon0), + ("lat_0", "central_latitude", lat0), + ("llcrnrlon", "min_longitude", lonlim[0]), + ("urcrnrlon", "max_longitude", lonlim[1]), + ("llcrnrlat", "min_latitude", latlim[0]), + ("urcrnrlat", "max_latitude", latlim[1]), ): if value is None: continue - if backend == 'basemap' and key_proj == 'lon_0' and value > 0: + if backend == "basemap" and key_proj == "lon_0" and value > 0: value -= 360 # see above comment - kwargs[key_proj if backend == 'basemap' else key_cartopy] = value + kwargs[key_proj if backend == "basemap" else key_cartopy] = value # Projection instances if is_crs or is_basemap: if backend is not None: - kwargs['backend'] = backend + kwargs["backend"] = backend if kwargs: - warnings._warn_proplot(f'Ignoring Proj() keyword arg(s): {kwargs!r}.') + warnings._warn_proplot(f"Ignoring Proj() keyword arg(s): {kwargs!r}.") proj = name - backend = 'cartopy' if is_crs else 'basemap' + backend = "cartopy" if is_crs else "basemap" # Cartopy name # NOTE: Error message matches basemap invalid projection message - elif backend == 'cartopy': + elif backend == "cartopy": # Parse keywoard arguments import cartopy # ensure present # noqa: F401 - for key in ('round', 'boundinglat'): + + for key in ("round", "boundinglat"): value = kwargs.pop(key, None) if value is not None: raise ValueError( - 'Ignoring Proj() keyword {key}={value!r}. Must be passed ' - 'to GeoAxes.format() when cartopy is the backend.' + "Ignoring Proj() keyword {key}={value!r}. Must be passed " + "to GeoAxes.format() when cartopy is the backend." ) # Retrieve projection and initialize with nice error message try: crs = PROJS[name] except KeyError: - message = f'{name!r} is an unknown cartopy projection class.\n' - message += 'The known cartopy projection classes are:\n' - message += '\n'.join( - ' ' + key + ' ' * (max(map(len, PROJS)) - len(key) + 10) + cls.__name__ + message = f"{name!r} is an unknown cartopy projection class.\n" + message += "The known cartopy projection classes are:\n" + message += "\n".join( + " " + key + " " * (max(map(len, PROJS)) - len(key) + 10) + cls.__name__ for key, cls in PROJS.items() ) if include_axes: from . import axes as paxes # avoid circular imports - message = message.replace('class.', 'class or axes subclass.') - message += '\nThe known axes subclasses are:\n' + paxes._cls_table + + message = message.replace("class.", "class or axes subclass.") + message += "\nThe known axes subclasses are:\n" + paxes._cls_table raise ValueError(message) from None - if name == 'geos': # fix common mistake - kwargs.pop('central_latitude', None) + if name == "geos": # fix common mistake + kwargs.pop("central_latitude", None) proj = crs(**kwargs) # Basemap name @@ -1510,49 +1585,44 @@ def Proj( else: # Parse input arguments from mpl_toolkits import basemap # ensure present # noqa: F401 - if name in ('eqc', 'pcarree'): - name = 'cyl' # PROJ package aliases - defaults = {'fix_aspect': True, **PROJ_DEFAULTS.get(name, {})} - if name[:2] in ('np', 'sp'): - defaults['round'] = rc['geo.round'] - if name == 'geos': - defaults['rsphere'] = (6378137.00, 6356752.3142) + + if name in ("eqc", "pcarree"): + name = "cyl" # PROJ package aliases + defaults = {"fix_aspect": True, **PROJ_DEFAULTS.get(name, {})} + if name[:2] in ("np", "sp"): + defaults["round"] = rc["geo.round"] + if name == "geos": + defaults["rsphere"] = (6378137.00, 6356752.3142) for key, value in defaults.items(): if kwargs.get(key, None) is None: # allow e.g. boundinglat=None kwargs[key] = value - # Initialize - if _version_mpl >= '3.3': - raise RuntimeError( - 'Basemap is no longer maintained and is incompatible with ' - 'matplotlib >= 3.3. Please use cartopy as your geographic ' - 'plotting backend or downgrade to matplotlib < 3.3.' - ) reso = _not_none( - reso=kwargs.pop('reso', None), - resolution=kwargs.pop('resolution', None), - default=rc['reso'] + reso=kwargs.pop("reso", None), + resolution=kwargs.pop("resolution", None), + default=rc["reso"], ) if reso in RESOS_BASEMAP: reso = RESOS_BASEMAP[reso] else: raise ValueError( - f'Invalid resolution {reso!r}. Options are: ' - + ', '.join(map(repr, RESOS_BASEMAP)) - + '.' + f"Invalid resolution {reso!r}. Options are: " + + ", ".join(map(repr, RESOS_BASEMAP)) + + "." ) - kwargs.update({'resolution': reso, 'projection': name}) + kwargs.update({"resolution": reso, "projection": name}) try: proj = Basemap(**kwargs) # will raise helpful warning except ValueError as err: message = str(err) message = message.strip() - message = message.replace('projection', 'basemap projection') - message = message.replace('supported', 'known') + message = message.replace("projection", "basemap projection") + message = message.replace("supported", "known") if include_axes: from . import axes as paxes # avoid circular imports - message = message.replace('projection.', 'projection or axes subclass.') - message += '\nThe known axes subclasses are:\n' + paxes._cls_table + + message = message.replace("projection.", "projection or axes subclass.") + message += "\nThe known axes subclasses are:\n" + paxes._cls_table raise ValueError(message) from None proj._proj_backend = backend @@ -1560,6 +1630,4 @@ def Proj( # Deprecated -Colors = warnings._rename_objs( - '0.8.0', Colors=get_colors -) +Colors = warnings._rename_objs("0.8.0", Colors=get_colors) diff --git a/proplot/demos.py b/proplot/demos.py index 7011838de..b307f4850 100644 --- a/proplot/demos.py +++ b/proplot/demos.py @@ -18,166 +18,314 @@ from .utils import to_rgb, to_xyz __all__ = [ - 'show_cmaps', - 'show_channels', - 'show_colors', - 'show_colorspaces', - 'show_cycles', - 'show_fonts', + "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 + "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, + "base": mcolors.BASE_COLORS, + "css4": mcolors.CSS4_COLORS, + "opencolor": pcolors.COLORS_OPEN, + "xkcd": pcolors.COLORS_XKCD, } CYCLE_TABLE = { - 'Matplotlib defaults': ( - 'default', 'classic', + "Matplotlib defaults": ( + "default", + "classic", ), - 'Matplotlib stylesheets': ( + "Matplotlib stylesheets": ( # NOTE: Do not include 'solarized' because colors are terrible for # colorblind folks. - 'colorblind', 'colorblind10', 'tableau', 'ggplot', '538', 'seaborn', 'bmh', + "colorblind", + "colorblind10", + "tableau", + "ggplot", + "538", + "seaborn", + "bmh", ), - 'ColorBrewer2.0 qualitative': ( - 'Accent', 'Dark2', - 'Paired', 'Pastel1', 'Pastel2', - 'Set1', 'Set2', 'Set3', - 'tab10', 'tab20', 'tab20b', 'tab20c', + "ColorBrewer2.0 qualitative": ( + "Accent", + "Dark2", + "Paired", + "Pastel1", + "Pastel2", + "Set1", + "Set2", + "Set3", + "tab10", + "tab20", + "tab20b", + "tab20c", ), - 'Other qualitative': ( - 'FlatUI', 'Qual1', 'Qual2', + "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', + "Grayscale": ( # assorted origin, but they belong together + "Greys", + "Mono", + "MonoCycle", ), - 'Matplotlib sequential': ( - 'viridis', 'plasma', 'inferno', 'magma', 'cividis', + "Matplotlib sequential": ( + "viridis", + "plasma", + "inferno", + "magma", + "cividis", ), - 'Matplotlib cyclic': ( - 'twilight', + "Matplotlib cyclic": ("twilight",), + "Seaborn sequential": ( + "Rocket", + "Flare", + "Mako", + "Crest", ), - 'Seaborn sequential': ( - 'Rocket', 'Flare', 'Mako', 'Crest', + "Seaborn diverging": ( + "IceFire", + "Vlag", ), - 'Seaborn diverging': ( - 'IceFire', 'Vlag', + "Proplot sequential": ( + "Fire", + "Stellar", + "Glacial", + "Dusk", + "Marine", + "Boreal", + "Sunrise", + "Sunset", ), - 'Proplot sequential': ( - 'Fire', - 'Stellar', - 'Glacial', - 'Dusk', - 'Marine', - 'Boreal', - 'Sunrise', - 'Sunset', + "Proplot diverging": ( + "Div", + "NegPos", + "DryWet", ), - 'Proplot diverging': ( - 'Div', 'NegPos', 'DryWet', + "Other sequential": ("cubehelix", "turbo"), + "Other diverging": ( + "BR", + "ColdHot", + "CoolWarm", ), - 'Other sequential': ( - 'cubehelix', 'turbo' + "cmOcean sequential": ( + "Oxy", + "Thermal", + "Dense", + "Ice", + "Haline", + "Deep", + "Algae", + "Tempo", + "Speed", + "Turbid", + "Solar", + "Matter", + "Amp", ), - 'Other diverging': ( - 'BR', 'ColdHot', 'CoolWarm', + "cmOcean diverging": ( + "Balance", + "Delta", + "Curl", ), - 'cmOcean sequential': ( - 'Oxy', 'Thermal', 'Dense', 'Ice', 'Haline', - 'Deep', 'Algae', 'Tempo', 'Speed', 'Turbid', 'Solar', 'Matter', - 'Amp', + "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", ), - 'cmOcean diverging': ( - 'Balance', 'Delta', 'Curl', + "Scientific colour maps diverging": ( + "roma", + "broc", + "cork", + "vik", + "bam", + "lisbon", + "tofino", + "berlin", + "vanimo", ), - 'cmOcean cyclic': ( - 'Phase', + "Scientific colour maps cyclic": ( + "romaO", + "brocO", + "corkO", + "vikO", + "bamO", ), - 'Scientific colour maps sequential': ( - 'batlow', 'batlowK', 'batlowW', - 'devon', 'davos', 'oslo', 'lapaz', 'acton', - 'lajolla', 'bilbao', 'tokyo', 'turku', 'bamako', 'nuuk', - 'hawaii', 'buda', 'imola', - 'oleron', 'bukavu', 'fes', + "ColorBrewer2.0 sequential": ( + "Purples", + "Blues", + "Greens", + "Oranges", + "Reds", + "YlOrBr", + "YlOrRd", + "OrRd", + "PuRd", + "RdPu", + "BuPu", + "PuBu", + "PuBuGn", + "BuGn", + "GnBu", + "YlGnBu", + "YlGn", ), - 'Scientific colour maps diverging': ( - 'roma', 'broc', 'cork', 'vik', 'bam', 'lisbon', 'tofino', 'berlin', 'vanimo', + "ColorBrewer2.0 diverging": ( + "Spectral", + "PiYG", + "PRGn", + "BrBG", + "PuOr", + "RdGY", + "RdBu", + "RdYlBu", + "RdYlGn", ), - 'Scientific colour maps cyclic': ( - 'romaO', 'brocO', 'corkO', 'vikO', 'bamO', + "SciVisColor blues": ( + "Blues1", + "Blues2", + "Blues3", + "Blues4", + "Blues5", + "Blues6", + "Blues7", + "Blues8", + "Blues9", + "Blues10", + "Blues11", ), - 'ColorBrewer2.0 sequential': ( - 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds', - 'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu', - 'PuBu', 'PuBuGn', 'BuGn', 'GnBu', 'YlGnBu', 'YlGn' + "SciVisColor greens": ( + "Greens1", + "Greens2", + "Greens3", + "Greens4", + "Greens5", + "Greens6", + "Greens7", + "Greens8", ), - 'ColorBrewer2.0 diverging': ( - 'Spectral', 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGY', - 'RdBu', 'RdYlBu', 'RdYlGn', + "SciVisColor yellows": ( + "Yellows1", + "Yellows2", + "Yellows3", + "Yellows4", ), - 'SciVisColor blues': ( - 'Blues1', 'Blues2', 'Blues3', 'Blues4', 'Blues5', - 'Blues6', 'Blues7', 'Blues8', 'Blues9', 'Blues10', 'Blues11', + "SciVisColor oranges": ( + "Oranges1", + "Oranges2", + "Oranges3", + "Oranges4", ), - 'SciVisColor greens': ( - 'Greens1', 'Greens2', 'Greens3', 'Greens4', 'Greens5', - 'Greens6', 'Greens7', 'Greens8', + "SciVisColor browns": ( + "Browns1", + "Browns2", + "Browns3", + "Browns4", + "Browns5", + "Browns6", + "Browns7", + "Browns8", + "Browns9", ), - 'SciVisColor yellows': ( - 'Yellows1', 'Yellows2', 'Yellows3', 'Yellows4', + "SciVisColor reds": ( + "Reds1", + "Reds2", + "Reds3", + "Reds4", + "Reds5", ), - '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', + "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', + "MATLAB": ( + "bone", + "cool", + "copper", + "autumn", + "flag", + "prism", + "jet", + "hsv", + "hot", + "spring", + "summer", + "winter", + "pink", + "gray", ), - 'GNUplot': ( - 'gnuplot', 'gnuplot2', 'ocean', 'afmhot', 'rainbow', + "GNUplot": ( + "gnuplot", + "gnuplot2", + "ocean", + "afmhot", + "rainbow", ), - 'GIST': ( - 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', - 'gist_rainbow', 'gist_stern', 'gist_yarg', + "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 ), - '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 @@ -192,15 +340,25 @@ 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 +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 def show_channels( - *args, N=100, rgb=False, saturation=True, - minhue=0, maxsat=500, width=100, refwidth=1.7 + *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, @@ -241,25 +399,29 @@ def show_channels( """ # Figure and plot if not args: - raise ValueError('At least one positional argument required.') + raise ValueError("At least one positional argument required.") array = [[1, 1, 2, 2, 3, 3]] - labels = ('Hue', 'Chroma', 'Luminance') + labels = ("Hue", "Chroma", "Luminance") if saturation: array += [[0, 4, 4, 5, 5, 0]] - labels += ('HSL saturation', 'HPL saturation') + labels += ("HSL saturation", "HPL saturation") if rgb: array += [np.array([4, 4, 5, 5, 6, 6]) + 2 * int(saturation)] - labels += ('Red', 'Green', 'Blue') + 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, + 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) + 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 @@ -270,9 +432,9 @@ def show_channels( 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] + 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 @@ -283,47 +445,54 @@ def show_channels( data += tuple(rgb_data) for ax, y, label in zip(axs, data, labels): ylim, ylocator = None, None - if label in ('Red', 'Green', 'Blue'): + if label in ("Red", "Green", "Blue"): ylim = (0, 1) ylocator = 0.2 - elif label == 'Luminance': + elif label == "Luminance": ylim = (0, 100) ylocator = 20 - elif label == 'Hue': + 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: + if "HSL" in label: m = ms = max(min(max(ms, max(y)), maxsat), 100) - elif 'HPL' in label: + 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) + 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 '') + ", ".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, + 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' + cmap, + loc="b", + span=(2, 5), + locator="null", + label=cmap.name, + labelweight="bold", ) return fig, axs @@ -368,37 +537,46 @@ def show_colorspaces(*, luminance=None, saturation=None, hue=None, refwidth=2): 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' + 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' + 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' + 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')): + 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]): @@ -407,21 +585,35 @@ def show_colorspaces(*, luminance=None, saturation=None, hue=None, refwidth=2): rgba[k, j, 3] = 0 # black cell else: rgba[k, j, :3] = rgb_jk - ax.imshow(rgba, origin='lower', aspect='auto') + 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', + 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') +@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, + 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 @@ -432,7 +624,7 @@ def _draw_bars( table.update({cat: [None] * len(names) for cat, names in source.items()}) for cmap in cmaps: cat = None - name = cmap.name or '_no_name' + name = cmap.name or "_no_name" name = name.lower() for opt, names in source.items(): names = list(map(str.lower, names)) @@ -446,7 +638,7 @@ def _draw_bars( # Filter out certain categories options = set(map(str.lower, source)) if ignore is None: - ignore = ('matlab', 'gnuplot', 'gist', 'other') + ignore = ("matlab", "gnuplot", "gist", "other") if isinstance(include, str): include = (include,) if isinstance(ignore, str): @@ -457,8 +649,9 @@ def _draw_bars( 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)) + '.' + 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] @@ -469,8 +662,12 @@ def _draw_bars( # 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', + refwidth=length, + refheight=width, + nrows=naxs, + share=False, + hspace="2pt", + top="-1em", ) i = -1 nheads = nbars = 0 # for deciding which axes to plot in @@ -482,24 +679,32 @@ def _draw_bars( break if j == 0: # allocate this axes for title i += 2 - for ax in axs[i - 2:i]: + 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) + 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, + 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', + 0 - (rc["axes.labelpad"] / 72) / length, + 0.45, + label, + ha="right", + va="center", + transform="axes", ) if j == 0: - ax.set_title(cat, weight='bold') + ax.set_title(cat, weight="bold") nbars += len(cmaps) return fig, axs @@ -550,21 +755,26 @@ def show_cmaps(*args, **kwargs): if args: cmaps = list(map(constructor.Colormap, args)) cmaps = [ - cmap if isinstance(cmap, mcolors.LinearSegmentedColormap) - else pcolors._get_cmap_subtype(cmap, 'continuous') for cmap in args + ( + cmap + if isinstance(cmap, mcolors.LinearSegmentedColormap) + else pcolors._get_cmap_subtype(cmap, "continuous") + ) + for cmap in args ] ignore = () else: cmaps = [ - cmap for cmap in pcolors._cmap_database.values() + cmap + for cmap in pcolors._cmap_database.values() if isinstance(cmap, pcolors.ContinuousColormap) - and not (cmap.name or '_')[:1] == '_' + and not (cmap.name or "_")[:1] == "_" ] ignore = None # Return figure of colorbars - kwargs.setdefault('source', CMAP_TABLE) - kwargs.setdefault('ignore', ignore) + kwargs.setdefault("source", CMAP_TABLE) + kwargs.setdefault("ignore", ignore) return _draw_bars(cmaps, **kwargs) @@ -607,25 +817,32 @@ def show_cycles(*args, **kwargs): # Get the list of cycles if args: cycles = [ - pcolors.DiscreteColormap( - cmap.by_key().get('color', ['k']), name=getattr(cmap, 'name', None) + ( + pcolors.DiscreteColormap( + cmap.by_key().get("color", ["k"]), name=getattr(cmap, "name", None) + ) + if isinstance(cmap, cycler.Cycler) + else ( + cmap + if isinstance(cmap, mcolors.ListedColormap) + else pcolors._get_cmap_subtype(cmap, "discrete") + ) ) - if isinstance(cmap, cycler.Cycler) - else cmap if isinstance(cmap, mcolors.ListedColormap) - else pcolors._get_cmap_subtype(cmap, 'discrete') for cmap in args + for cmap in args ] ignore = () else: cycles = [ - cmap for cmap in pcolors._cmap_database.values() + cmap + for cmap in pcolors._cmap_database.values() if isinstance(cmap, pcolors.DiscreteColormap) - and not (cmap.name or '_')[:1] == '_' + and not (cmap.name or "_")[:1] == "_" ] ignore = None # Return figure of colorbars - kwargs.setdefault('source', CYCLE_TABLE) - kwargs.setdefault('ignore', ignore) + kwargs.setdefault("source", CYCLE_TABLE) + kwargs.setdefault("ignore", ignore) return _draw_bars(cycles, **kwargs) @@ -655,7 +872,7 @@ def _filter_colors(hcl, ihue, nhues, minsat): @docstring._snippet_manager -def show_colors(*, nhues=17, minsat=10, unknown='User', include=None, ignore=None): +def show_colors(*, nhues=17, minsat=10, unknown="User", include=None, ignore=None): """ Generate tables of the registered color names. Adapted from `this example `__. @@ -689,7 +906,7 @@ def show_colors(*, nhues=17, minsat=10, unknown='User', include=None, ignore=Non # Tables of known colors to be plotted colordict = {} if ignore is None: - ignore = 'css4' + ignore = "css4" if isinstance(include, str): include = (include.lower(),) if isinstance(ignore, str): @@ -700,16 +917,19 @@ def show_colors(*, nhues=17, minsat=10, unknown='User', include=None, ignore=Non 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)) + '.' + 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 + 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: @@ -721,21 +941,22 @@ def show_colors(*, nhues=17, minsat=10, unknown='User', include=None, ignore=Non # them by hue in descending order of luminance. namess = {} for cat in sorted(include): - if cat == 'base': + if cat == "base": names = np.asarray(colordict[cat]) ncols, nrows = len(names), 1 - elif cat == 'opencolor': + 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 = [(name, to_xyz(name, "hcl")) for name in colordict[cat]] hclpairs = [ sorted( [ - pair for pair in hclpairs + pair + for pair in hclpairs if _filter_colors(pair[1], ihue, nhues, minsat) ], - key=lambda x: x[1][2] # sort by luminance + key=lambda x: x[1][2], # sort by luminance ) for ihue in range(nhues) ] @@ -761,19 +982,21 @@ def show_colors(*, nhues=17, minsat=10, unknown='User', include=None, ignore=Non hratios=hratios, ) title_dict = { - 'css4': 'CSS4 colors', - 'base': 'Base colors', - 'opencolor': 'Open color', - 'xkcd': 'XKCD colors', + "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', + titleweight="bold", xlim=(0, maxcols - 1), ylim=(0, names.shape[1]), - grid=False, yloc='neither', xloc='neither', + grid=False, + yloc="neither", + xloc="neither", alpha=0, ) @@ -790,13 +1013,20 @@ def show_colors(*, nhues=17, minsat=10, unknown='User', include=None, ignore=Non 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, + 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 + [x1, x2], + [y, y], + color=name, + lw=lw, + solid_capstyle="butt", # do not stick out clip_on=False, ) @@ -871,35 +1101,38 @@ def show_fonts( 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_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)}.') + 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_proplot(f'Input font name {opts[:1]!r} not found. Skipping.') + warnings._warn_proplot(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') + family = _not_none(family, None if args else "sans-serif") if user: - paths = _get_data_folders('fonts', default=False) + 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') + 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)) + '.' + f"Invalid font family {family!r}. Options are: " + + ", ".join(map(repr, options)) + + "." ) - names = FAMILY_TEXGYRE if family == 'tex-gyre' else rc['font.' + family] + 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)) @@ -909,51 +1142,60 @@ def show_fonts( 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 : ! ? & # % $ [] {}' + "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$' + "\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'} + ctx = {"mathtext.fontset": "custom"} if not fallback: - if _version_mpl < '3.4': - ctx['mathtext.fallback_to_cm'] = False + if _version_mpl < "3.4": + ctx["mathtext.fallback_to_cm"] = False else: - ctx['mathtext.fallback'] = None - if 'size' not in kwargs: + ctx["mathtext.fallback"] = None + if "size" not in kwargs: for prop in props: - if prop.get_size() == rc['font.size']: + 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 + 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, + 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 + 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 + 0, + 0.5, + f"{name}:\n{text} ", + ha="left", + va="center", + linespacing=linespacing, + fontproperties=prop, ) return fig, axs diff --git a/proplot/externals/hsluv.py b/proplot/externals/hsluv.py index 73dfdf4a0..be917e84e 100644 --- a/proplot/externals/hsluv.py +++ b/proplot/externals/hsluv.py @@ -29,16 +29,8 @@ from colorsys import hls_to_rgb, rgb_to_hls # Coefficients or something -m = [ - [3.2406, -1.5372, -0.4986], - [-0.9689, 1.8758, 0.0415], - [0.0557, -0.2040, 1.0570] -] -m_inv = [ - [0.4124, 0.3576, 0.1805], - [0.2126, 0.7152, 0.0722], - [0.0193, 0.1192, 0.9505] -] +m = [[3.2406, -1.5372, -0.4986], [-0.9689, 1.8758, 0.0415], [0.0557, -0.2040, 1.0570]] +m_inv = [[0.4124, 0.3576, 0.1805], [0.2126, 0.7152, 0.0722], [0.0193, 0.1192, 0.9505]] # Hard-coded D65 illuminant (has to do with expected light intensity and # white balance that falls upon the generated color) # See: https://en.wikipedia.org/wiki/Illuminant_D65 @@ -121,7 +113,7 @@ def rgb_prepare(triple): for ch in triple: ch = round(ch, 3) if ch < -0.0001 or ch > 1.0001: - raise Exception(f'Illegal RGB value {ch:f}.') + raise Exception(f"Illegal RGB value {ch:f}.") if ch < 0: ch = 0 if ch > 1: @@ -133,11 +125,11 @@ def rgb_prepare(triple): def rgb_to_hex(triple): [r, g, b] = triple - return '#%02x%02x%02x' % tuple(rgb_prepare([r, g, b])) + return "#%02x%02x%02x" % tuple(rgb_prepare([r, g, b])) def hex_to_rgb(color): - if color.startswith('#'): + if color.startswith("#"): color = color[1:] r = int(color[0:2], 16) / 255.0 g = int(color[2:4], 16) / 255.0 @@ -147,38 +139,38 @@ def hex_to_rgb(color): def max_chroma(L, H): hrad = math.radians(H) - sinH = (math.sin(hrad)) - cosH = (math.cos(hrad)) - sub1 = (math.pow(L + 16, 3.0) / 1560896.0) + sinH = math.sin(hrad) + cosH = math.cos(hrad) + sub1 = math.pow(L + 16, 3.0) / 1560896.0 sub2 = sub1 if sub1 > 0.008856 else (L / 903.3) - result = float('inf') + result = float("inf") for row in m: m1 = row[0] m2 = row[1] m3 = row[2] - top = ((0.99915 * m1 + 1.05122 * m2 + 1.14460 * m3) * sub2) - rbottom = (0.86330 * m3 - 0.17266 * m2) - lbottom = (0.12949 * m3 - 0.38848 * m1) + top = (0.99915 * m1 + 1.05122 * m2 + 1.14460 * m3) * sub2 + rbottom = 0.86330 * m3 - 0.17266 * m2 + lbottom = 0.12949 * m3 - 0.38848 * m1 bottom = (rbottom * sinH + lbottom * cosH) * sub2 for t in (0.0, 1.0): - C = (L * (top - 1.05122 * t) / (bottom + 0.17266 * sinH * t)) + C = L * (top - 1.05122 * t) / (bottom + 0.17266 * sinH * t) if C > 0.0 and C < result: result = C return result def hrad_extremum(L): - lhs = (math.pow(L, 3.0) + 48.0 * math.pow(L, 2.0) - + 768.0 * L + 4096.0) / 1560896.0 + lhs = (math.pow(L, 3.0) + 48.0 * math.pow(L, 2.0) + 768.0 * L + 4096.0) / 1560896.0 rhs = 1107.0 / 125000.0 sub = lhs if lhs > rhs else 10.0 * L / 9033.0 - chroma = float('inf') + chroma = float("inf") result = None for row in m: for limit in (0.0, 1.0): [m1, m2, m3] = row - top = -3015466475.0 * m3 * sub + 603093295.0 * m2 * sub \ - - 603093295.0 * limit + top = ( + -3015466475.0 * m3 * sub + 603093295.0 * m2 * sub - 603093295.0 * limit + ) bottom = 1356959916.0 * m1 * sub - 452319972.0 * m3 * sub hrad = math.atan2(top, bottom) if limit == 0.0: @@ -252,15 +244,15 @@ def from_linear(c): if c <= 0.0031308: return 12.92 * c else: - return (1.055 * math.pow(c, 1.0 / 2.4) - 0.055) + return 1.055 * math.pow(c, 1.0 / 2.4) - 0.055 def to_linear(c): a = 0.055 if c > 0.04045: - return (math.pow((c + a) / (1.0 + a), 2.4)) + return math.pow((c + a) / (1.0 + a), 2.4) else: - return (c / 12.92) + return c / 12.92 def CIExyz_to_rgb(triple): @@ -275,8 +267,8 @@ def rgb_to_CIExyz(triple): def CIEluv_to_lchuv(triple): L, U, V = triple - C = (math.pow(math.pow(U, 2) + math.pow(V, 2), (1.0 / 2.0))) - hrad = (math.atan2(V, U)) + C = math.pow(math.pow(U, 2) + math.pow(V, 2), (1.0 / 2.0)) + hrad = math.atan2(V, U) H = math.degrees(hrad) if H < 0.0: H = 360.0 + H @@ -286,8 +278,8 @@ def CIEluv_to_lchuv(triple): def lchuv_to_CIEluv(triple): L, C, H = triple Hrad = math.radians(H) - U = (math.cos(Hrad) * C) - V = (math.sin(Hrad) * C) + U = math.cos(Hrad) * C + V = math.sin(Hrad) * C return [L, U, V] @@ -298,14 +290,14 @@ def lchuv_to_CIEluv(triple): def CIEfunc(t): if t > lab_e: - return (math.pow(t, 1.0 / gamma)) + return math.pow(t, 1.0 / gamma) else: - return (7.787 * t + 16.0 / 116.0) + return 7.787 * t + 16.0 / 116.0 def CIEfunc_inverse(t): if math.pow(t, 3.0) > lab_e: - return (math.pow(t, gamma)) + return math.pow(t, gamma) else: return (116.0 * t - 16.0) / lab_k diff --git a/proplot/figure.py b/proplot/figure.py index bc0ff6f0d..28003d0d9 100644 --- a/proplot/figure.py +++ b/proplot/figure.py @@ -33,28 +33,28 @@ from .utils import units __all__ = [ - 'Figure', + "Figure", ] # Preset figure widths or sizes based on academic journal recommendations # NOTE: Please feel free to add to this! JOURNAL_SIZES = { - 'aaas1': '5.5cm', - 'aaas2': '12cm', - 'agu1': ('95mm', '115mm'), - 'agu2': ('190mm', '115mm'), - 'agu3': ('95mm', '230mm'), - 'agu4': ('190mm', '230mm'), - 'ams1': 3.2, - 'ams2': 4.5, - 'ams3': 5.5, - 'ams4': 6.5, - 'nat1': '89mm', - 'nat2': '183mm', - 'pnas1': '8.7cm', - 'pnas2': '11.4cm', - 'pnas3': '17.8cm', + "aaas1": "5.5cm", + "aaas2": "12cm", + "agu1": ("95mm", "115mm"), + "agu2": ("190mm", "115mm"), + "agu3": ("95mm", "230mm"), + "agu4": ("190mm", "230mm"), + "ams1": 3.2, + "ams2": 4.5, + "ams3": 5.5, + "ams4": 6.5, + "nat1": "89mm", + "nat2": "183mm", + "pnas1": "8.7cm", + "pnas2": "11.4cm", + "pnas3": "17.8cm", } @@ -170,7 +170,7 @@ .. _pnas: \ https://www.pnas.org/page/authors/format """ -docstring._snippet_manager['figure.figure'] = _figure_docstring +docstring._snippet_manager["figure.figure"] = _figure_docstring # Multiple subplots @@ -221,7 +221,7 @@ %(gridspec.vector)s %(gridspec.tight)s """ -docstring._snippet_manager['figure.subplots_params'] = _subplots_params_docstring +docstring._snippet_manager["figure.subplots_params"] = _subplots_params_docstring # Extra args docstring @@ -231,7 +231,7 @@ `proplot.axes.GeoAxes`, or `proplot.axes.ThreeAxes`. This can include keyword arguments for projection-specific ``format`` commands. """ -docstring._snippet_manager['figure.axes_params'] = _axes_params_docstring +docstring._snippet_manager["figure.axes_params"] = _axes_params_docstring # Multiple subplots docstring @@ -261,7 +261,7 @@ proplot.gridspec.SubplotGrid proplot.axes.Axes """ -docstring._snippet_manager['figure.subplots'] = _subplots_docstring +docstring._snippet_manager["figure.subplots"] = _subplots_docstring # Single subplot docstring @@ -314,7 +314,7 @@ proplot.figure.Figure.subplots proplot.figure.Figure.add_subplots """ -docstring._snippet_manager['figure.subplot'] = _subplot_docstring +docstring._snippet_manager["figure.subplot"] = _subplot_docstring # Single axes @@ -341,7 +341,7 @@ proplot.figure.Figure.subplots proplot.figure.Figure.add_subplots """ -docstring._snippet_manager['figure.axes'] = _axes_docstring +docstring._snippet_manager["figure.axes"] = _axes_docstring # Colorbar or legend panel docstring @@ -378,8 +378,12 @@ right {name}s and ``'left'`` and ``'right'`` are valid for top and bottom {name}s. The default is always ``'center'``. """ -docstring._snippet_manager['figure.legend_space'] = _space_docstring.format(name='legend') # noqa: E501 -docstring._snippet_manager['figure.colorbar_space'] = _space_docstring.format(name='colorbar') # noqa: E501 +docstring._snippet_manager["figure.legend_space"] = _space_docstring.format( + name="legend" +) # noqa: E501 +docstring._snippet_manager["figure.colorbar_space"] = _space_docstring.format( + name="colorbar" +) # noqa: E501 # Save docstring @@ -399,7 +403,7 @@ Figure.savefig matplotlib.figure.Figure.savefig """ -docstring._snippet_manager['figure.save'] = _save_docstring +docstring._snippet_manager["figure.save"] = _save_docstring def _get_journal_size(preset): @@ -409,9 +413,8 @@ def _get_journal_size(preset): value = JOURNAL_SIZES.get(preset, None) if value is None: raise ValueError( - f'Unknown preset figure size specifier {preset!r}. ' - 'Current options are: ' - + ', '.join(map(repr, JOURNAL_SIZES.keys())) + f"Unknown preset figure size specifier {preset!r}. " + "Current options are: " + ", ".join(map(repr, JOURNAL_SIZES.keys())) ) figwidth = figheight = None try: @@ -428,6 +431,7 @@ def _add_canvas_preprocessor(canvas, method, cache=False): and aspect ratio-conserving adjustments and aligns labels. Required so canvas methods instantiate renderers with the correct dimensions. """ + # NOTE: Renderer must be (1) initialized with the correct figure size or # (2) changed inplace during draw, but vector graphic renderers *cannot* # be changed inplace. So options include (1) monkey patch @@ -448,7 +452,7 @@ def _canvas_preprocess(self, *args, **kwargs): # nothing, and for print_figure is some figure object, but this block # has never been invoked when calling print_figure. if fig._is_adjusting: - if method == '_draw': # macosx backend + if method == "_draw": # macosx backend return fig._get_renderer() else: return @@ -472,62 +476,95 @@ class Figure(mfigure.Figure): """ The `~matplotlib.figure.Figure` subclass used by proplot. """ + # Shared error and warning messages _share_message = ( - 'Axis sharing level can be 0 or False (share nothing), ' + "Axis sharing level can be 0 or False (share nothing), " "1 or 'labels' or 'labs' (share axis labels), " "2 or 'limits' or 'lims' (share axis limits and axis labels), " - '3 or True (share axis limits, axis labels, and tick labels), ' + "3 or True (share axis limits, axis labels, and tick labels), " "or 4 or 'all' (share axis labels and tick labels in the same gridspec " - 'rows and columns and share axis limits across all subplots).' + "rows and columns and share axis limits across all subplots)." ) _space_message = ( - 'To set the left, right, bottom, top, wspace, or hspace gridspec values, ' - 'pass them as keyword arguments to pplt.figure() or pplt.subplots(). Please ' - 'note they are now specified in physical units, with strings interpreted by ' - 'pplt.units() and floats interpreted as font size-widths.' + "To set the left, right, bottom, top, wspace, or hspace gridspec values, " + "pass them as keyword arguments to pplt.figure() or pplt.subplots(). Please " + "note they are now specified in physical units, with strings interpreted by " + "pplt.units() and floats interpreted as font size-widths." ) _tight_message = ( - 'Proplot uses its own tight layout algorithm that is activated by default. ' + "Proplot uses its own tight layout algorithm that is activated by default. " "To disable it, set pplt.rc['subplots.tight'] to False or pass tight=False " - 'to pplt.subplots(). For details, see fig.auto_layout().' + "to pplt.subplots(). For details, see fig.auto_layout()." ) _warn_interactive = True # disabled after first warning def __repr__(self): opts = {} - for attr in ('refaspect', 'refwidth', 'refheight', 'figwidth', 'figheight'): - value = getattr(self, '_' + attr) + for attr in ("refaspect", "refwidth", "refheight", "figwidth", "figheight"): + value = getattr(self, "_" + attr) if value is not None: opts[attr] = np.round(value, 2) - geom = '' + geom = "" if self.gridspec: nrows, ncols = self.gridspec.get_geometry() - geom = f'nrows={nrows}, ncols={ncols}, ' - opts = ', '.join(f'{key}={value!r}' for key, value in opts.items()) - return f'Figure({geom}{opts})' + geom = f"nrows={nrows}, ncols={ncols}, " + opts = ", ".join(f"{key}={value!r}" for key, value in opts.items()) + return f"Figure({geom}{opts})" # NOTE: If _rename_kwargs argument is an invalid identifier, it is # simply used in the warning message. @docstring._obfuscate_kwargs @docstring._snippet_manager @warnings._rename_kwargs( - '0.7.0', axpad='innerpad', autoformat='pplt.rc.autoformat = {}' + "0.7.0", axpad="innerpad", autoformat="pplt.rc.autoformat = {}" ) def __init__( - self, *, refnum=None, ref=None, refaspect=None, aspect=None, - refwidth=None, refheight=None, axwidth=None, axheight=None, - figwidth=None, figheight=None, width=None, height=None, journal=None, - sharex=None, sharey=None, share=None, # used for default spaces - spanx=None, spany=None, span=None, - alignx=None, aligny=None, align=None, - left=None, right=None, top=None, bottom=None, - wspace=None, hspace=None, space=None, - tight=None, outerpad=None, innerpad=None, panelpad=None, - wpad=None, hpad=None, pad=None, - wequal=None, hequal=None, equal=None, - wgroup=None, hgroup=None, group=None, - **kwargs + self, + *, + refnum=None, + ref=None, + refaspect=None, + aspect=None, + refwidth=None, + refheight=None, + axwidth=None, + axheight=None, + figwidth=None, + figheight=None, + width=None, + height=None, + journal=None, + sharex=None, + sharey=None, + share=None, # used for default spaces + spanx=None, + spany=None, + span=None, + alignx=None, + aligny=None, + align=None, + left=None, + right=None, + top=None, + bottom=None, + wspace=None, + hspace=None, + space=None, + tight=None, + outerpad=None, + innerpad=None, + panelpad=None, + wpad=None, + hpad=None, + pad=None, + wequal=None, + hequal=None, + equal=None, + wgroup=None, + hgroup=None, + group=None, + **kwargs, ): """ Parameters @@ -563,54 +600,59 @@ def __init__( if journal is not None: jwidth, jheight = _get_journal_size(journal) if jwidth is not None and figwidth is not None: - messages.append(('journal', journal, 'figwidth', figwidth)) + messages.append(("journal", journal, "figwidth", figwidth)) if jheight is not None and figheight is not None: - messages.append(('journal', journal, 'figheight', figheight)) + messages.append(("journal", journal, "figheight", figheight)) figwidth = _not_none(jwidth, figwidth) figheight = _not_none(jheight, figheight) if figwidth is not None and refwidth is not None: - messages.append(('figwidth', figwidth, 'refwidth', refwidth)) + messages.append(("figwidth", figwidth, "refwidth", refwidth)) refwidth = None if figheight is not None and refheight is not None: - messages.append(('figheight', figheight, 'refheight', refheight)) + messages.append(("figheight", figheight, "refheight", refheight)) refheight = None - if figwidth is None and figheight is None and refwidth is None and refheight is None: # noqa: E501 - refwidth = rc['subplots.refwidth'] # always inches + if ( + figwidth is None + and figheight is None + and refwidth is None + and refheight is None + ): # noqa: E501 + refwidth = rc["subplots.refwidth"] # always inches if np.iterable(refaspect): refaspect = refaspect[0] / refaspect[1] for key1, val1, key2, val2 in messages: warnings._warn_proplot( - f'Got conflicting figure size arguments {key1}={val1!r} and ' - f'{key2}={val2!r}. Ignoring {key2!r}.' + f"Got conflicting figure size arguments {key1}={val1!r} and " + f"{key2}={val2!r}. Ignoring {key2!r}." ) self._refnum = refnum self._refaspect = refaspect self._refaspect_default = 1 # updated for imshow and geographic plots - self._refwidth = units(refwidth, 'in') - self._refheight = units(refheight, 'in') - self._figwidth = figwidth = units(figwidth, 'in') - self._figheight = figheight = units(figheight, 'in') + self._refwidth = units(refwidth, "in") + self._refheight = units(refheight, "in") + self._figwidth = figwidth = units(figwidth, "in") + self._figheight = figheight = units(figheight, "in") # Add special consideration for interactive backends - backend = _not_none(rc.backend, '') + backend = _not_none(rc.backend, "") backend = backend.lower() - interactive = 'nbagg' in backend or 'ipympl' in backend + interactive = "nbagg" in backend or "ipympl" in backend if not interactive: pass elif figwidth is None or figheight is None: - figsize = rc['figure.figsize'] # modified by proplot + figsize = rc["figure.figsize"] # modified by proplot self._figwidth = figwidth = _not_none(figwidth, figsize[0]) self._figheight = figheight = _not_none(figheight, figsize[1]) self._refwidth = self._refheight = None # critical! if self._warn_interactive: Figure._warn_interactive = False # set class attribute warnings._warn_proplot( - 'Auto-sized proplot figures are not compatible with interactive ' + "Auto-sized proplot figures are not compatible with interactive " "backends like '%matplotlib widget' and '%matplotlib notebook'. " - f'Reverting to the figure size ({figwidth}, {figheight}). To make ' - 'auto-sized figures, please consider using the non-interactive ' - '(default) backend. This warning message is shown the first time ' - 'you create a figure without explicitly specifying the size.' + f"Reverting to the figure size ({figwidth}, {figheight}). To make " + "auto-sized figures, please consider using the non-interactive " + "(default) backend. This warning message is shown the first time " + "you create a figure without explicitly specifying the size." ) # Add space settings @@ -620,85 +662,101 @@ def __init__( # 'subplots_adjust' would be confusing since we switch to absolute # units and that function is heavily used outside of proplot. params = { - 'left': left, 'right': right, 'top': top, 'bottom': bottom, - 'wspace': wspace, 'hspace': hspace, 'space': space, - 'wequal': wequal, 'hequal': hequal, 'equal': equal, - 'wgroup': wgroup, 'hgroup': hgroup, 'group': group, - 'wpad': wpad, 'hpad': hpad, 'pad': pad, - 'outerpad': outerpad, 'innerpad': innerpad, 'panelpad': panelpad, + "left": left, + "right": right, + "top": top, + "bottom": bottom, + "wspace": wspace, + "hspace": hspace, + "space": space, + "wequal": wequal, + "hequal": hequal, + "equal": equal, + "wgroup": wgroup, + "hgroup": hgroup, + "group": group, + "wpad": wpad, + "hpad": hpad, + "pad": pad, + "outerpad": outerpad, + "innerpad": innerpad, + "panelpad": panelpad, } self._gridspec_params = params # used to initialize the gridspec for key, value in tuple(params.items()): if not isinstance(value, str) and np.iterable(value) and len(value) > 1: raise ValueError( - f'Invalid gridspec parameter {key}={value!r}. Space parameters ' - 'passed to Figure() must be scalar. For vector spaces use ' - 'GridSpec() or pass space parameters to subplots().' + f"Invalid gridspec parameter {key}={value!r}. Space parameters " + "passed to Figure() must be scalar. For vector spaces use " + "GridSpec() or pass space parameters to subplots()." ) # Add tight layout setting and ignore native settings - pars = kwargs.pop('subplotpars', None) + pars = kwargs.pop("subplotpars", None) if pars is not None: warnings._warn_proplot( - f'Ignoring subplotpars={pars!r}. ' + self._space_message + f"Ignoring subplotpars={pars!r}. " + self._space_message ) - if kwargs.pop('tight_layout', None): + if kwargs.pop("tight_layout", None): + warnings._warn_proplot("Ignoring tight_layout=True. " + self._tight_message) + if kwargs.pop("constrained_layout", None): warnings._warn_proplot( - 'Ignoring tight_layout=True. ' + self._tight_message + "Ignoring constrained_layout=True. " + self._tight_message ) - if kwargs.pop('constrained_layout', None): - warnings._warn_proplot( - 'Ignoring constrained_layout=True. ' + self._tight_message - ) - if rc_matplotlib.get('figure.autolayout', False): + if rc_matplotlib.get("figure.autolayout", False): warnings._warn_proplot( "Setting rc['figure.autolayout'] to False. " + self._tight_message ) - if rc_matplotlib.get('figure.constrained_layout.use', False): + if rc_matplotlib.get("figure.constrained_layout.use", False): warnings._warn_proplot( - "Setting rc['figure.constrained_layout.use'] to False. " + self._tight_message # noqa: E501 + "Setting rc['figure.constrained_layout.use'] to False. " + + self._tight_message # noqa: E501 ) try: - rc_matplotlib['figure.autolayout'] = False # this is rcParams + rc_matplotlib["figure.autolayout"] = False # this is rcParams except KeyError: pass try: - rc_matplotlib['figure.constrained_layout.use'] = False # this is rcParams + rc_matplotlib["figure.constrained_layout.use"] = False # this is rcParams except KeyError: pass - self._tight_active = _not_none(tight, rc['subplots.tight']) + self._tight_active = _not_none(tight, rc["subplots.tight"]) # Translate share settings - translate = {'labels': 1, 'labs': 1, 'limits': 2, 'lims': 2, 'all': 4} - sharex = _not_none(sharex, share, rc['subplots.share']) - sharey = _not_none(sharey, share, rc['subplots.share']) + translate = {"labels": 1, "labs": 1, "limits": 2, "lims": 2, "all": 4} + sharex = _not_none(sharex, share, rc["subplots.share"]) + sharey = _not_none(sharey, share, rc["subplots.share"]) sharex = 3 if sharex is True else translate.get(sharex, sharex) sharey = 3 if sharey is True else translate.get(sharey, sharey) if sharex not in range(5): - raise ValueError(f'Invalid sharex={sharex!r}. ' + self._share_message) + raise ValueError(f"Invalid sharex={sharex!r}. " + self._share_message) if sharey not in range(5): - raise ValueError(f'Invalid sharey={sharey!r}. ' + self._share_message) + raise ValueError(f"Invalid sharey={sharey!r}. " + self._share_message) self._sharex = int(sharex) self._sharey = int(sharey) # Translate span and align settings - spanx = _not_none(spanx, span, False if not sharex else None, rc['subplots.span']) # noqa: E501 - spany = _not_none(spany, span, False if not sharey else None, rc['subplots.span']) # noqa: E501 + spanx = _not_none( + spanx, span, False if not sharex else None, rc["subplots.span"] + ) # noqa: E501 + spany = _not_none( + spany, span, False if not sharey else None, rc["subplots.span"] + ) # noqa: E501 if spanx and (alignx or align): # only warn when explicitly requested warnings._warn_proplot('"alignx" has no effect when spanx=True.') if spany and (aligny or align): warnings._warn_proplot('"aligny" has no effect when spany=True.') self._spanx = bool(spanx) self._spany = bool(spany) - alignx = _not_none(alignx, align, rc['subplots.align']) - aligny = _not_none(aligny, align, rc['subplots.align']) + alignx = _not_none(alignx, align, rc["subplots.align"]) + aligny = _not_none(aligny, align, rc["subplots.align"]) self._alignx = bool(alignx) self._aligny = bool(aligny) # Initialize the figure # NOTE: Super labels are stored inside {axes: text} dictionaries self._gridspec = None - self._panel_dict = {'left': [], 'right': [], 'bottom': [], 'top': []} + self._panel_dict = {"left": [], "right": [], "bottom": [], "top": []} self._subplot_dict = {} # subplots indexed by number self._subplot_counter = 0 # avoid add_subplot() returning an existing subplot self._is_adjusting = False @@ -708,7 +766,7 @@ def __init__( rc_kw, rc_mode = _pop_rc(kwargs) kw_format = _pop_params(kwargs, self._format_signature) if figwidth is not None and figheight is not None: - kwargs['figsize'] = (figwidth, figheight) + kwargs["figsize"] = (figwidth, figheight) with self._context_authorized(): super().__init__(**kwargs) @@ -716,21 +774,21 @@ def __init__( # _align_axis_labels supports arbitrary spanning labels for subplot groups. # NOTE: Don't use 'anchor' rotation mode otherwise switching to horizontal # left and right super labels causes overlap. Current method is fine. - self._suptitle = self.text(0.5, 0.95, '', ha='center', va='bottom') + self._suptitle = self.text(0.5, 0.95, "", ha="center", va="bottom") self._supxlabel_dict = {} # an axes: label mapping self._supylabel_dict = {} # an axes: label mapping - self._suplabel_dict = {'left': {}, 'right': {}, 'bottom': {}, 'top': {}} - self._suptitle_pad = rc['suptitle.pad'] + self._suplabel_dict = {"left": {}, "right": {}, "bottom": {}, "top": {}} + self._suptitle_pad = rc["suptitle.pad"] d = self._suplabel_props = {} # store the super label props - d['left'] = {'va': 'center', 'ha': 'right'} - d['right'] = {'va': 'center', 'ha': 'left'} - d['bottom'] = {'va': 'top', 'ha': 'center'} - d['top'] = {'va': 'bottom', 'ha': 'center'} + d["left"] = {"va": "center", "ha": "right"} + d["right"] = {"va": "center", "ha": "left"} + d["bottom"] = {"va": "top", "ha": "center"} + d["top"] = {"va": "bottom", "ha": "center"} d = self._suplabel_pad = {} # store the super label padding - d['left'] = rc['leftlabel.pad'] - d['right'] = rc['rightlabel.pad'] - d['bottom'] = rc['bottomlabel.pad'] - d['top'] = rc['toplabel.pad'] + d["left"] = rc["leftlabel.pad"] + d["right"] = rc["rightlabel.pad"] + d["bottom"] = rc["bottomlabel.pad"] + d["top"] = rc["toplabel.pad"] # Format figure # NOTE: This ignores user-input rc_mode. @@ -741,9 +799,9 @@ def _context_adjusting(self, cache=True): Prevent re-running auto layout steps due to draws triggered by figure resizes. Otherwise can get infinite loops. """ - kw = {'_is_adjusting': True} + kw = {"_is_adjusting": True} if not cache: - kw['_cachedRenderer'] = None # temporarily ignore it + kw["_cachedRenderer"] = None # temporarily ignore it return context._state_context(self, **kw) def _context_authorized(self): @@ -759,16 +817,22 @@ def _parse_backend(backend=None, basemap=None): Handle deprication of basemap and cartopy package. """ if basemap is not None: - backend = ('cartopy', 'basemap')[bool(basemap)] + backend = ("cartopy", "basemap")[bool(basemap)] warnings._warn_proplot( f"The 'basemap' keyword was deprecated in version 0.10.0 and will be " - f'removed in a future release. Please use backend={backend!r} instead.' + f"removed in a future release. Please use backend={backend!r} instead." ) return backend def _parse_proj( - self, proj=None, projection=None, - proj_kw=None, projection_kw=None, backend=None, basemap=None, **kwargs + self, + proj=None, + projection=None, + proj_kw=None, + projection_kw=None, + backend=None, + basemap=None, + **kwargs, ): """ Translate the user-input projection into a registered matplotlib @@ -776,7 +840,7 @@ def _parse_proj( `cartopy.crs.Projection`, or `mpl_toolkits.basemap.Basemap`. """ # Parse arguments - proj = _not_none(proj=proj, projection=projection, default='cartesian') + proj = _not_none(proj=proj, projection=projection, default="cartesian") proj_kw = _not_none(proj_kw=proj_kw, projection_kw=projection_kw, default={}) backend = self._parse_backend(backend, basemap) if isinstance(proj, str): @@ -784,13 +848,13 @@ def _parse_proj( if isinstance(self, paxes.Axes): proj = self._name elif isinstance(self, maxes.Axes): - raise ValueError('Matplotlib axes cannot be added to proplot figures.') + raise ValueError("Matplotlib axes cannot be added to proplot figures.") # Search axes projections name = None if isinstance(proj, str): try: - mproj.get_projection_class('proplot_' + proj) + mproj.get_projection_class("proplot_" + proj) except (KeyError, ValueError): pass else: @@ -804,32 +868,32 @@ def _parse_proj( and constructor.Basemap is object ): raise ValueError( - f'Invalid projection name {proj!r}. If you are trying to generate a ' - 'GeoAxes with a cartopy.crs.Projection or mpl_toolkits.basemap.Basemap ' - 'then cartopy or basemap must be installed. Otherwise the known axes ' - f'subclasses are:\n{paxes._cls_table}' + f"Invalid projection name {proj!r}. If you are trying to generate a " + "GeoAxes with a cartopy.crs.Projection or mpl_toolkits.basemap.Basemap " + "then cartopy or basemap must be installed. Otherwise the known axes " + f"subclasses are:\n{paxes._cls_table}" ) # Search geographic projections # NOTE: Also raises errors due to unexpected projection type if name is None: proj = constructor.Proj(proj, backend=backend, include_axes=True, **proj_kw) name = proj._proj_backend - kwargs['map_projection'] = proj + kwargs["map_projection"] = proj - kwargs['projection'] = 'proplot_' + name + kwargs["projection"] = "proplot_" + name return kwargs def _get_align_axes(self, side): """ Return the main axes along the edge of the figure. """ - x, y = 'xy' if side in ('left', 'right') else 'yx' + x, y = "xy" if side in ("left", "right") else "yx" axs = self._subplot_dict.values() if not axs: return [] ranges = np.array([ax._range_subplotspec(x) for ax in axs]) - edge = ranges[:, 0].min() if side in ('left', 'top') else ranges[:, 1].max() - idx = 0 if side in ('left', 'top') else 1 + edge = ranges[:, 0].min() if side in ("left", "top") else ranges[:, 1].max() + idx = 0 if side in ("left", "top") else 1 axs = [ax for ax in axs if ax._range_subplotspec(x)[idx] == edge] axs = [ax for ax in sorted(axs, key=lambda ax: ax._range_subplotspec(y)[0])] axs = [ax for ax in axs if ax.get_visible()] @@ -841,10 +905,10 @@ def _get_align_coord(self, side, axs, includepanels=False): """ # Get position in figure relative coordinates if not all(isinstance(ax, paxes.Axes) for ax in axs): - raise RuntimeError('Axes must be proplot axes.') + raise RuntimeError("Axes must be proplot axes.") if not all(isinstance(ax, maxes.SubplotBase) for ax in axs): - raise RuntimeError('Axes must be subplots.') - s = 'y' if side in ('left', 'right') else 'x' + raise RuntimeError("Axes must be subplots.") + s = "y" if side in ("left", "right") else "x" axs = [ax._panel_parent or ax for ax in axs] # deflect to main axes if includepanels: # include panel short axes? axs = [_ for ax in axs for _ in ax._iter_axes(panels=True, children=False)] @@ -854,7 +918,7 @@ def _get_align_coord(self, side, axs, includepanels=False): ax_hi = axs[np.where(ranges[:, 1] == max_)[0][0]] box_lo = ax_lo.get_subplotspec().get_position(self) box_hi = ax_hi.get_subplotspec().get_position(self) - if s == 'x': + if s == "x": pos = 0.5 * (box_lo.x0 + box_hi.x1) else: pos = 0.5 * (box_lo.y1 + box_hi.y0) # 'lo' is actually on top of figure @@ -866,36 +930,41 @@ def _get_offset_coord(self, side, axs, renderer, *, pad=None, extra=None): """ Return the figure coordinate for offsetting super labels and super titles. """ - s = 'x' if side in ('left', 'right') else 'y' + s = "x" if side in ("left", "right") else "y" cs = [] - objs = tuple(_ for ax in axs for _ in ax._iter_axes(panels=True, children=True, hidden=True)) # noqa: E501 + objs = tuple( + _ + for ax in axs + for _ in ax._iter_axes(panels=True, children=True, hidden=True) + ) # noqa: E501 objs = objs + (extra or ()) # e.g. top super labels for obj in objs: bbox = obj.get_tightbbox(renderer) # cannot use cached bbox - attr = s + 'max' if side in ('top', 'right') else s + 'min' + attr = s + "max" if side in ("top", "right") else s + "min" c = getattr(bbox, attr) - c = (c, 0) if side in ('left', 'right') else (0, c) + c = (c, 0) if side in ("left", "right") else (0, c) c = self.transFigure.inverted().transform(c) - c = c[0] if side in ('left', 'right') else c[1] + c = c[0] if side in ("left", "right") else c[1] cs.append(c) width, height = self.get_size_inches() if pad is None: pad = self._suplabel_pad[side] / 72 - pad = pad / width if side in ('left', 'right') else pad / height - return min(cs) - pad if side in ('left', 'bottom') else max(cs) + pad + pad = pad / width if side in ("left", "right") else pad / height + return min(cs) - pad if side in ("left", "bottom") else max(cs) + pad def _get_renderer(self): """ Get a renderer at all costs. See matplotlib's tight_layout.py. """ - if self._cachedRenderer: + if hasattr(self, "_cached_render"): renderer = self._cachedRenderer else: canvas = self.canvas - if canvas and hasattr(canvas, 'get_renderer'): + if canvas and hasattr(canvas, "get_renderer"): renderer = canvas.get_renderer() else: from matplotlib.backends.backend_agg import FigureCanvasAgg + canvas = FigureCanvasAgg(self) renderer = canvas.get_renderer() return renderer @@ -911,36 +980,36 @@ def _add_axes_panel(self, ax, side=None, **kwargs): ax = ax._altx_parent or ax ax = ax._alty_parent or ax if not isinstance(ax, paxes.Axes): - raise RuntimeError('Cannot add panels to non-proplot axes.') + raise RuntimeError("Cannot add panels to non-proplot axes.") if not isinstance(ax, maxes.SubplotBase): - raise RuntimeError('Cannot add panels to non-subplot axes.') + raise RuntimeError("Cannot add panels to non-subplot axes.") orig = ax._panel_side if orig is None: pass elif side is None or side == orig: ax, side = ax._panel_parent, orig else: - raise RuntimeError(f'Cannot add {side!r} panel to existing {orig!r} panel.') - side = _translate_loc(side, 'panel', default=_not_none(orig, 'right')) + raise RuntimeError(f"Cannot add {side!r} panel to existing {orig!r} panel.") + side = _translate_loc(side, "panel", default=_not_none(orig, "right")) # Add and setup the panel accounting for index changes # NOTE: Always put tick labels on the 'outside' and permit arbitrary # keyword arguments passed from the user. gs = self.gridspec if not gs: - raise RuntimeError('The gridspec must be active.') + raise RuntimeError("The gridspec must be active.") kw = _pop_params(kwargs, gs._insert_panel_slot) ss, share = gs._insert_panel_slot(side, ax, **kw) - kwargs['autoshare'] = False - kwargs.setdefault('number', False) # power users might number panels + kwargs["autoshare"] = False + kwargs.setdefault("number", False) # power users might number panels pax = self.add_subplot(ss, **kwargs) pax._panel_side = side pax._panel_share = share pax._panel_parent = ax ax._panel_dict[side].append(pax) ax._apply_auto_share() - axis = pax.yaxis if side in ('left', 'right') else pax.xaxis - getattr(axis, 'tick_' + side)() # set tick and tick label position + axis = pax.yaxis if side in ("left", "right") else pax.xaxis + getattr(axis, "tick_" + side)() # set tick and tick label position axis.set_label_position(side) # set label position return pax @@ -951,16 +1020,16 @@ def _add_figure_panel( Add a figure panel. """ # Interpret args and enforce sensible keyword args - side = _translate_loc(side, 'panel', default='right') - if side in ('left', 'right'): - for key, value in (('col', col), ('cols', cols)): + side = _translate_loc(side, "panel", default="right") + if side in ("left", "right"): + for key, value in (("col", col), ("cols", cols)): if value is not None: - raise ValueError(f'Invalid keyword {key!r} for {side!r} panel.') + raise ValueError(f"Invalid keyword {key!r} for {side!r} panel.") span = _not_none(span=span, row=row, rows=rows) else: - for key, value in (('row', row), ('rows', rows)): + for key, value in (("row", row), ("rows", rows)): if value is not None: - raise ValueError(f'Invalid keyword {key!r} for {side!r} panel.') + raise ValueError(f"Invalid keyword {key!r} for {side!r} panel.") span = _not_none(span=span, col=col, cols=cols) # Add and setup panel @@ -968,7 +1037,7 @@ def _add_figure_panel( # do not need to pass aribtrary axes keyword arguments. gs = self.gridspec if not gs: - raise RuntimeError('The gridspec must be active.') + raise RuntimeError("The gridspec must be active.") ss, _ = gs._insert_panel_slot(side, span, filled=True, **kwargs) pax = self.add_subplot(ss, autoshare=False, number=False) plist = self._panel_dict[side] @@ -990,13 +1059,12 @@ def _add_subplot(self, *args, **kwargs): # Integer arg if len(args) == 1 and isinstance(args[0], Integral): if not 111 <= args[0] <= 999: - raise ValueError(f'Input {args[0]} must fall between 111 and 999.') + raise ValueError(f"Input {args[0]} must fall between 111 and 999.") args = tuple(map(int, str(args[0]))) # Subplot spec - if ( - len(args) == 1 - and isinstance(args[0], (maxes.SubplotBase, mgridspec.SubplotSpec)) + if len(args) == 1 and isinstance( + args[0], (maxes.SubplotBase, mgridspec.SubplotSpec) ): ss = args[0] if isinstance(ss, maxes.SubplotBase): @@ -1005,11 +1073,11 @@ def _add_subplot(self, *args, **kwargs): gs = ss.get_topmost_subplotspec().get_gridspec() if not isinstance(gs, pgridspec.GridSpec): raise ValueError( - 'Input subplotspec must be derived from a proplot.GridSpec.' + "Input subplotspec must be derived from a proplot.GridSpec." ) if ss.get_topmost_subplotspec().get_gridspec() is not gs: raise ValueError( - 'Input subplotspec must be derived from the active figure gridspec.' + "Input subplotspec must be derived from the active figure gridspec." ) # Row and column spec @@ -1027,29 +1095,29 @@ def _add_subplot(self, *args, **kwargs): orows, ocols = gs.get_geometry() if orows % nrows: raise ValueError( - f'The input number of rows {nrows} does not divide the ' - f'figure gridspec number of rows {orows}.' + f"The input number of rows {nrows} does not divide the " + f"figure gridspec number of rows {orows}." ) if ocols % ncols: raise ValueError( - f'The input number of columns {ncols} does not divide the ' - f'figure gridspec number of columns {ocols}.' + f"The input number of columns {ncols} does not divide the " + f"figure gridspec number of columns {ocols}." ) if any(_ < 1 or _ > nrows * ncols for _ in (i, j)): raise ValueError( - 'The input subplot indices must fall between ' - f'1 and {nrows * ncols}. Instead got {i} and {j}.' + "The input subplot indices must fall between " + f"1 and {nrows * ncols}. Instead got {i} and {j}." ) rowfact, colfact = orows // nrows, ocols // ncols irow, icol = divmod(i - 1, ncols) # convert to zero-based jrow, jcol = divmod(j - 1, ncols) irow, icol = irow * rowfact, icol * colfact jrow, jcol = (jrow + 1) * rowfact - 1, (jcol + 1) * colfact - 1 - ss = gs[irow:jrow + 1, icol:jcol + 1] + ss = gs[irow : jrow + 1, icol : jcol + 1] # Otherwise else: - raise ValueError(f'Invalid add_subplot positional arguments {args!r}.') + raise ValueError(f"Invalid add_subplot positional arguments {args!r}.") # Add the subplot # NOTE: Pass subplotspec as keyword arg for mpl >= 3.4 workaround @@ -1058,20 +1126,32 @@ def _add_subplot(self, *args, **kwargs): # wrong location due to gridspec override. Is against OO package design. self.gridspec = gs # trigger layout adjustment self._subplot_counter += 1 # unique label for each subplot - kwargs.setdefault('label', f'subplot_{self._subplot_counter}') - kwargs.setdefault('number', 1 + max(self._subplot_dict, default=0)) + kwargs.setdefault("label", f"subplot_{self._subplot_counter}") + kwargs.setdefault("number", 1 + max(self._subplot_dict, default=0)) + kwargs.pop("refwidth", None) # TODO: remove this ax = super().add_subplot(ss, _subplot_spec=ss, **kwargs) if ax.number: self._subplot_dict[ax.number] = ax return ax def _add_subplots( - self, array=None, nrows=1, ncols=1, order='C', proj=None, projection=None, - proj_kw=None, projection_kw=None, backend=None, basemap=None, **kwargs + self, + array=None, + nrows=1, + ncols=1, + order="C", + proj=None, + projection=None, + proj_kw=None, + projection_kw=None, + backend=None, + basemap=None, + **kwargs, ): """ The driver function for adding multiple subplots. """ + # Clunky helper function # TODO: Consider deprecating and asking users to use add_subplot() def _axes_dict(naxs, input, kw=False, default=None): @@ -1086,7 +1166,7 @@ def _axes_dict(naxs, input, kw=False, default=None): if not any(nested): # any([]) == False input = {range(1, naxs + 1): input.copy()} elif not all(nested): - raise ValueError(f'Invalid input {input!r}.') + raise ValueError(f"Invalid input {input!r}.") # Unfurl keys that contain multiple axes numbers output = {} for nums, item in input.items(): @@ -1099,14 +1179,15 @@ def _axes_dict(naxs, input, kw=False, default=None): output[num] = {} if kw else default if output.keys() != set(range(1, naxs + 1)): raise ValueError( - f'Have {naxs} axes, but {input!r} includes props for the axes: ' - + ', '.join(map(repr, sorted(output))) + '.' + f"Have {naxs} axes, but {input!r} includes props for the axes: " + + ", ".join(map(repr, sorted(output))) + + "." ) return output # Build the subplot array # NOTE: Currently this may ignore user-input nrows/ncols without warning - if order not in ('C', 'F'): # better error message + if order not in ("C", "F"): # better error message raise ValueError(f"Invalid order={order!r}. Options are 'C' or 'F'.") gs = None if array is None or isinstance(array, mgridspec.GridSpec): @@ -1119,33 +1200,33 @@ def _axes_dict(naxs, input, kw=False, default=None): array[array == None] = 0 # None or 0 both valid placeholders # noqa: E711 array = array.astype(int) if array.ndim == 1: # interpret as single row or column - array = array[None, :] if order == 'C' else array[:, None] + array = array[None, :] if order == "C" else array[:, None] elif array.ndim != 2: - raise ValueError(f'Expected 1D or 2D array of integers. Got {array}.') + raise ValueError(f"Expected 1D or 2D array of integers. Got {array}.") # Parse input format, gridspec, and projection arguments # NOTE: Permit figure format keywords for e.g. 'collabels' (more intuitive) nums = np.unique(array[array != 0]) naxs = len(nums) if any(num < 0 or not isinstance(num, Integral) for num in nums.flat): - raise ValueError(f'Expected array of positive integers. Got {array}.') + raise ValueError(f"Expected array of positive integers. Got {array}.") proj = _not_none(projection=projection, proj=proj) - proj = _axes_dict(naxs, proj, kw=False, default='cartesian') + proj = _axes_dict(naxs, proj, kw=False, default="cartesian") proj_kw = _not_none(projection_kw=projection_kw, proj_kw=proj_kw) or {} proj_kw = _axes_dict(naxs, proj_kw, kw=True) backend = self._parse_backend(backend, basemap) backend = _axes_dict(naxs, backend, kw=False) axes_kw = { - num: {'proj': proj[num], 'proj_kw': proj_kw[num], 'backend': backend[num]} + num: {"proj": proj[num], "proj_kw": proj_kw[num], "backend": backend[num]} for num in proj } - for key in ('gridspec_kw', 'subplot_kw'): + for key in ("gridspec_kw", "subplot_kw"): kw = kwargs.pop(key, None) if not kw: continue warnings._warn_proplot( - f'{key!r} is not necessary in proplot. Pass the ' - 'parameters as keyword arguments instead.' + f"{key!r} is not necessary in proplot. Pass the " + "parameters as keyword arguments instead." ) kwargs.update(kw or {}) figure_kw = _pop_params(kwargs, self._format_signature) @@ -1165,8 +1246,8 @@ def _axes_dict(naxs, input, kw=False, default=None): num = idx + 1 x0, x1 = axcols[idx, 0], axcols[idx, 1] y0, y1 = axrows[idx, 0], axrows[idx, 1] - ss = gs[y0:y1 + 1, x0:x1 + 1] - kw = {**kwargs, **axes_kw[num], 'number': num} + ss = gs[y0 : y1 + 1, x0 : x1 + 1] + kw = {**kwargs, **axes_kw[num], "number": num} axs[idx] = self.add_subplot(ss, **kw) self.format(skip_axes=True, **figure_kw) @@ -1180,25 +1261,25 @@ def _align_axis_label(self, x): # NOTE: Must trigger axis sharing here so that super label alignment # with tight=False is valid. Kind of kludgey but oh well. seen = set() - span = getattr(self, '_span' + x) - align = getattr(self, '_align' + x) + span = getattr(self, "_span" + x) + align = getattr(self, "_align" + x) for ax in self._subplot_dict.values(): if isinstance(ax, paxes.CartesianAxes): ax._apply_axis_sharing() # always! else: continue - pos = getattr(ax, x + 'axis').get_label_position() - if ax in seen or pos not in ('bottom', 'left'): + pos = getattr(ax, x + "axis").get_label_position() + if ax in seen or pos not in ("bottom", "left"): continue # already aligned or cannot align axs = ax._get_span_axes(pos, panels=False) # returns panel or main axes - if any(getattr(ax, '_share' + x) for ax in axs): + if any(getattr(ax, "_share" + x) for ax in axs): continue # nothing to align or axes have parents seen.update(axs) if span or align: - if hasattr(self, '_align_label_groups'): + if hasattr(self, "_align_label_groups"): group = self._align_label_groups[x] else: - group = getattr(self, '_align_' + x + 'label_grp', None) + group = getattr(self, "_align_" + x + "label_grp", None) if group is not None: # fail silently to avoid fragile API changes for ax in axs[1:]: group.join(axs[0], ax) # add to grouper @@ -1212,15 +1293,15 @@ def _align_super_labels(self, side, renderer): # NOTE: Ensure title is offset only here. for ax in self._subplot_dict.values(): ax._apply_title_above() - if side not in ('left', 'right', 'bottom', 'top'): - raise ValueError(f'Invalid side {side!r}.') + if side not in ("left", "right", "bottom", "top"): + raise ValueError(f"Invalid side {side!r}.") labs = self._suplabel_dict[side] axs = tuple(ax for ax, lab in labs.items() if lab.get_text()) if not axs: return c = self._get_offset_coord(side, axs, renderer) for lab in labs.values(): - s = 'x' if side in ('left', 'right') else 'y' + s = "x" if side in ("left", "right") else "y" lab.update({s: c}) def _align_super_title(self, renderer): @@ -1229,15 +1310,15 @@ def _align_super_title(self, renderer): """ if not self._suptitle.get_text(): return - axs = self._get_align_axes('top') # returns outermost panels + axs = self._get_align_axes("top") # returns outermost panels if not axs: return - labs = tuple(t for t in self._suplabel_dict['top'].values() if t.get_text()) + labs = tuple(t for t in self._suplabel_dict["top"].values() if t.get_text()) pad = (self._suptitle_pad / 72) / self.get_size_inches()[1] - x, _ = self._get_align_coord('top', axs, includepanels=self._includepanels) - y = self._get_offset_coord('top', axs, renderer, pad=pad, extra=labs) - self._suptitle.set_ha('center') - self._suptitle.set_va('bottom') + x, _ = self._get_align_coord("top", axs, includepanels=self._includepanels) + y = self._get_offset_coord("top", axs, renderer, pad=pad, extra=labs) + self._suptitle.set_ha("center") + self._suptitle.set_va("bottom") self._suptitle.set_position((x, y)) def _update_axis_label(self, side, axs): @@ -1249,28 +1330,28 @@ def _update_axis_label(self, side, axs): # offsetting them between two subplots if necessary. Now we track designated # 'super' labels and replace the actual labels with spaces so they still impact # the tight bounding box and thus allocate space for the spanning label. - x, y = 'xy' if side in ('bottom', 'top') else 'yx' + x, y = "xy" if side in ("bottom", "top") else "yx" c, ax = self._get_align_coord(side, axs, includepanels=self._includepanels) - axlab = getattr(ax, x + 'axis').label # the central label - suplabs = getattr(self, '_sup' + x + 'label_dict') # dict of spanning labels + axlab = getattr(ax, x + "axis").label # the central label + suplabs = getattr(self, "_sup" + x + "label_dict") # dict of spanning labels suplab = suplabs.get(ax, None) if suplab is None and not axlab.get_text().strip(): return # nothing to transfer from the normal label if suplab is not None and not suplab.get_text().strip(): return # nothing to update on the super label if suplab is None: - props = ('ha', 'va', 'rotation', 'rotation_mode') - suplab = suplabs[ax] = self.text(0, 0, '') - suplab.update({prop: getattr(axlab, 'get_' + prop)() for prop in props}) + props = ("ha", "va", "rotation", "rotation_mode") + suplab = suplabs[ax] = self.text(0, 0, "") + suplab.update({prop: getattr(axlab, "get_" + prop)() for prop in props}) # Copy text from the central label to the spanning label # NOTE: Must use spaces rather than newlines, otherwise tight layout # won't make room. Reason is Text implementation (see Text._get_layout()) labels._transfer_label(axlab, suplab) # text, color, and font properties - count = 1 + suplab.get_text().count('\n') - space = '\n'.join(' ' * count) + count = 1 + suplab.get_text().count("\n") + space = "\n".join(" " * count) for ax in axs: # includes original 'axis' - axis = getattr(ax, x + 'axis') + axis = getattr(ax, x + "axis") axis.label.set_text(space) # Update spanning label position then add simple monkey patch @@ -1278,7 +1359,7 @@ def _update_axis_label(self, side, axs): # called is not sufficient. Fails with e.g. inline backend. t = mtransforms.IdentityTransform() # set in pixels cx, cy = axlab.get_position() - if x == 'x': + if x == "x": trans = mtransforms.blended_transform_factory(self.transFigure, t) coord = (c, cy) else: @@ -1286,26 +1367,28 @@ def _update_axis_label(self, side, axs): coord = (cx, c) suplab.set_transform(trans) suplab.set_position(coord) - setpos = getattr(mtext.Text, 'set_' + y) + setpos = getattr(mtext.Text, "set_" + y) + def _set_coord(self, *args, **kwargs): # noqa: E306 setpos(self, *args, **kwargs) setpos(suplab, *args, **kwargs) - setattr(axlab, 'set_' + y, _set_coord.__get__(axlab)) + + setattr(axlab, "set_" + y, _set_coord.__get__(axlab)) def _update_super_labels(self, side, labels, **kwargs): """ Assign the figure super labels and update settings. """ # Update the label parameters - if side not in ('left', 'right', 'bottom', 'top'): - raise ValueError(f'Invalid side {side!r}.') + if side not in ("left", "right", "bottom", "top"): + raise ValueError(f"Invalid side {side!r}.") kw = rc.fill( { - 'color': side + 'label.color', - 'rotation': side + 'label.rotation', - 'size': side + 'label.size', - 'weight': side + 'label.weight', - 'family': 'font.family', + "color": side + "label.color", + "rotation": side + "label.rotation", + "size": side + "label.size", + "weight": side + "label.weight", + "family": "font.family", }, context=True, ) @@ -1325,8 +1408,8 @@ def _update_super_labels(self, side, labels, **kwargs): return # nothing to update if len(labels) != len(axs): raise ValueError( - f'Got {len(labels)} {side} labels but found {len(axs)} axes ' - f'along the {side} side of the figure.' + f"Got {len(labels)} {side} labels but found {len(axs)} axes " + f"along the {side} side of the figure." ) src = self._suplabel_dict[side] extra = src.keys() - set(axs) @@ -1334,7 +1417,7 @@ def _update_super_labels(self, side, labels, **kwargs): text = src[ax].get_text() if text: warnings._warn_proplot( - f'Removing {side} label with text {text!r} from axes {ax.number}.' + f"Removing {side} label with text {text!r} from axes {ax.number}." ) src[ax].remove() # remove from the figure @@ -1343,13 +1426,13 @@ def _update_super_labels(self, side, labels, **kwargs): for ax, label in zip(axs, labels): if ax in src: obj = src[ax] - elif side in ('left', 'right'): + elif side in ("left", "right"): trans = mtransforms.blended_transform_factory(tf, ax.transAxes) - obj = src[ax] = self.text(0, 0.5, '', transform=trans) + obj = src[ax] = self.text(0, 0.5, "", transform=trans) obj.update(props) else: trans = mtransforms.blended_transform_factory(ax.transAxes, tf) - obj = src[ax] = self.text(0.5, 0, '', transform=trans) + obj = src[ax] = self.text(0.5, 0, "", transform=trans) obj.update(props) if kw: obj.update(kw) @@ -1362,10 +1445,10 @@ def _update_super_title(self, title, **kwargs): """ kw = rc.fill( { - 'size': 'suptitle.size', - 'weight': 'suptitle.weight', - 'color': 'suptitle.color', - 'family': 'font.family' + "size": "suptitle.size", + "weight": "suptitle.weight", + "color": "suptitle.color", + "family": "font.family", }, context=True, ) @@ -1456,10 +1539,11 @@ def auto_layout(self, renderer=None, aspect=None, tight=None, resize=None): def _draw_content(): for ax in self._iter_axes(hidden=False, children=True): ax._add_queued_guides() # may trigger resizes if panels are added + def _align_content(): # noqa: E306 - for axis in 'xy': + for axis in "xy": self._align_axis_label(axis) - for side in ('left', 'right', 'top', 'bottom'): + for side in ("left", "right", "top", "bottom"): self._align_super_labels(side, renderer) self._align_super_title(renderer) @@ -1477,18 +1561,32 @@ def _align_content(): # noqa: E306 _align_content() @warnings._rename_kwargs( - '0.10.0', mathtext_fallback='pplt.rc.mathtext_fallback = {}' + "0.10.0", mathtext_fallback="pplt.rc.mathtext_fallback = {}" ) @docstring._snippet_manager def format( - self, axs=None, *, - figtitle=None, suptitle=None, suptitle_kw=None, - llabels=None, leftlabels=None, leftlabels_kw=None, - rlabels=None, rightlabels=None, rightlabels_kw=None, - blabels=None, bottomlabels=None, bottomlabels_kw=None, - tlabels=None, toplabels=None, toplabels_kw=None, - rowlabels=None, collabels=None, # aliases - includepanels=None, **kwargs, + self, + axs=None, + *, + figtitle=None, + suptitle=None, + suptitle_kw=None, + llabels=None, + leftlabels=None, + leftlabels_kw=None, + rlabels=None, + rightlabels=None, + rightlabels_kw=None, + blabels=None, + bottomlabels=None, + bottomlabels_kw=None, + tlabels=None, + toplabels=None, + toplabels_kw=None, + rowlabels=None, + collabels=None, # aliases + includepanels=None, + **kwargs, ): """ Modify figure-wide labels and call ``format`` for the @@ -1527,19 +1625,19 @@ def format( """ # Initiate context block axs = axs or self._subplot_dict.values() - skip_axes = kwargs.pop('skip_axes', False) # internal keyword arg + skip_axes = kwargs.pop("skip_axes", False) # internal keyword arg rc_kw, rc_mode = _pop_rc(kwargs) with rc.context(rc_kw, mode=rc_mode): # Update background patch - kw = rc.fill({'facecolor': 'figure.facecolor'}, context=True) + kw = rc.fill({"facecolor": "figure.facecolor"}, context=True) self.patch.update(kw) # Update super title and label spacing - pad = rc.find('suptitle.pad', context=True) # super title + pad = rc.find("suptitle.pad", context=True) # super title if pad is not None: self._suptitle_pad = pad for side in tuple(self._suplabel_pad): # super labels - pad = rc.find(side + 'label.pad', context=True) + pad = rc.find(side + "label.pad", context=True) if pad is not None: self._suplabel_pad[side] = pad if includepanels is not None: @@ -1556,22 +1654,22 @@ def format( **suptitle_kw, ) self._update_super_labels( - 'left', + "left", _not_none(rowlabels=rowlabels, leftlabels=leftlabels, llabels=llabels), **leftlabels_kw, ) self._update_super_labels( - 'right', + "right", _not_none(rightlabels=rightlabels, rlabels=rlabels), **rightlabels_kw, ) self._update_super_labels( - 'bottom', + "bottom", _not_none(bottomlabels=bottomlabels, blabels=blabels), **bottomlabels_kw, ) self._update_super_labels( - 'top', + "top", _not_none(collabels=collabels, toplabels=toplabels, tlabels=tlabels), **toplabels_kw, ) @@ -1586,7 +1684,8 @@ def format( classes = set() # track used dictionaries for ax in axs: kw = { - key: value for cls, kw in kws.items() + key: value + for cls, kw in kws.items() for key, value in kw.items() if isinstance(ax, cls) and not classes.add(cls) } @@ -1594,20 +1693,32 @@ def format( # Warn unused keyword argument(s) kw = { - key: value for name in kws.keys() - classes + key: value + for name in kws.keys() - classes for key, value in kws[name].items() } if kw: warnings._warn_proplot( - f'Ignoring unused projection-specific format() keyword argument(s): {kw}' # noqa: E501 + f"Ignoring unused projection-specific format() keyword argument(s): {kw}" # noqa: E501 ) @docstring._concatenate_inherited @docstring._snippet_manager def colorbar( - self, mappable, values=None, loc=None, location=None, - row=None, col=None, rows=None, cols=None, span=None, - space=None, pad=None, width=None, **kwargs + self, + mappable, + values=None, + loc=None, + location=None, + row=None, + col=None, + rows=None, + cols=None, + span=None, + space=None, + pad=None, + width=None, + **kwargs, ): """ Add a colorbar along the side of the figure. @@ -1637,8 +1748,8 @@ def colorbar( matplotlib.figure.Figure.colorbar """ # Backwards compatibility - ax = kwargs.pop('ax', None) - cax = kwargs.pop('cax', None) + ax = kwargs.pop("ax", None) + cax = kwargs.pop("cax", None) if isinstance(values, maxes.Axes): cax = _not_none(cax_positional=values, cax=cax) values = None @@ -1646,10 +1757,10 @@ def colorbar( ax = _not_none(ax_positional=loc, ax=ax) loc = None # Helpful warning - if kwargs.pop('use_gridspec', None) is not None: + if kwargs.pop("use_gridspec", None) is not None: warnings._warn_proplot( "Ignoring the 'use_gridspec' keyword. Proplot always allocates " - 'additional space for colorbars using the figure gridspec ' + "additional space for colorbars using the figure gridspec " "rather than 'stealing space' from the parent subplot." ) # Fill this axes @@ -1663,20 +1774,38 @@ def colorbar( ) # Figure panel colorbar else: - loc = _not_none(loc=loc, location=location, default='r') + loc = _not_none(loc=loc, location=location, default="r") ax = self._add_figure_panel( - loc, row=row, col=col, rows=rows, cols=cols, span=span, - width=width, space=space, pad=pad, + loc, + row=row, + col=col, + rows=rows, + cols=cols, + span=span, + width=width, + space=space, + pad=pad, ) - cb = ax.colorbar(mappable, values, loc='fill', **kwargs) + cb = ax.colorbar(mappable, values, loc="fill", **kwargs) return cb @docstring._concatenate_inherited @docstring._snippet_manager def legend( - self, handles=None, labels=None, loc=None, location=None, - row=None, col=None, rows=None, cols=None, span=None, - space=None, pad=None, width=None, **kwargs + self, + handles=None, + labels=None, + loc=None, + location=None, + row=None, + col=None, + rows=None, + cols=None, + span=None, + space=None, + pad=None, + width=None, + **kwargs, ): """ Add a legend along the side of the figure. @@ -1699,7 +1828,7 @@ def legend( proplot.axes.Axes.legend matplotlib.axes.Axes.legend """ - ax = kwargs.pop('ax', None) + ax = kwargs.pop("ax", None) # Axes panel legend if ax is not None: leg = ax.legend( @@ -1707,12 +1836,19 @@ def legend( ) # Figure panel legend else: - loc = _not_none(loc=loc, location=location, default='r') + loc = _not_none(loc=loc, location=location, default="r") ax = self._add_figure_panel( - loc, row=row, col=col, rows=rows, cols=cols, span=span, - width=width, space=space, pad=pad, + loc, + row=row, + col=col, + rows=rows, + cols=cols, + span=span, + width=width, + space=space, + pad=pad, ) - leg = ax.legend(handles, labels, loc='fill', **kwargs) + leg = ax.legend(handles, labels, loc="fill", **kwargs) return leg @docstring._snippet_manager @@ -1761,8 +1897,11 @@ def set_canvas(self, canvas): # around this by forcing additional draw() call in this function before # proceeding with print_figure). Set the canvas and add monkey patches # to the instance-level draw and print_figure methods. - method = '_draw' if callable(getattr(canvas, '_draw', None)) else 'draw' - _add_canvas_preprocessor(canvas, 'print_figure', cache=False) # saves, inlines + method = "draw" + # if getattr(canvas, "_draw", None): + # method = "_draw" + # method = '_draw' if callable(getattr(canvas, '_draw', None)) else 'draw' + _add_canvas_preprocessor(canvas, "print_figure", cache=False) # saves, inlines _add_canvas_preprocessor(canvas, method, cache=True) # renderer displays super().set_canvas(canvas) @@ -1803,7 +1942,7 @@ def set_size_inches(self, w, h=None, *, forward=True, internal=False, eps=None): # Parse input args figsize = w if h is None else (w, h) if not np.all(np.isfinite(figsize)): - raise ValueError(f'Figure size must be finite, not {figsize}.') + raise ValueError(f"Figure size must be finite, not {figsize}.") # Fix the figure size if this is a user action from an interactive backend # NOTE: If we fail to detect 'user' resize from the user, not only will @@ -1813,7 +1952,7 @@ def set_size_inches(self, w, h=None, *, forward=True, internal=False, eps=None): # int(Figure.bbox.[width|height]) which rounds to whole pixels. When # renderer calls set_size_inches, size may be effectively the same, but # slightly changed due to roundoff error! Therefore only compare approx size. - attrs = ('_is_idle_drawing', '_is_drawing', '_draw_pending') + attrs = ("_is_idle_drawing", "_is_drawing", "_draw_pending") backend = any(getattr(self.canvas, attr, None) for attr in attrs) internal = internal or self._is_adjusting samesize = self._is_same_size(figsize, eps) @@ -1849,11 +1988,11 @@ def _iter_axes(self, hidden=False, children=False, panels=True): if panels is False: panels = () elif panels is True or panels is None: - panels = ('left', 'right', 'bottom', 'top') + panels = ("left", "right", "bottom", "top") elif isinstance(panels, str): panels = (panels,) - if not set(panels) <= {'left', 'right', 'bottom', 'top'}: - raise ValueError(f'Invalid sides {panels!r}.') + if not set(panels) <= {"left", "right", "bottom", "top"}: + raise ValueError(f"Invalid sides {panels!r}.") # Iterate axs = ( *self._subplot_dict.values(), @@ -1881,7 +2020,7 @@ def gridspec(self): @gridspec.setter def gridspec(self, gs): if not isinstance(gs, pgridspec.GridSpec): - raise ValueError('Gridspec must be a proplot.GridSpec instance.') + raise ValueError("Gridspec must be a proplot.GridSpec instance.") self._gridspec = gs gs.figure = self # trigger copying settings from the figure @@ -1924,13 +2063,15 @@ def tight(self, b): # Add deprecated properties. There are *lots* of properties we pass to Figure # and do not like idea of publicly tracking every single one of them. If we # want to improve user introspection consider modifying Figure.__repr__. -for _attr in ('alignx', 'aligny', 'sharex', 'sharey', 'spanx', 'spany', 'tight', 'ref'): +for _attr in ("alignx", "aligny", "sharex", "sharey", "spanx", "spany", "tight", "ref"): + def _get_deprecated(self, attr=_attr): warnings._warn_proplot( - f'The property {attr!r} is no longer public as of v0.8. It will be ' - 'removed in a future release.' + f"The property {attr!r} is no longer public as of v0.8. It will be " + "removed in a future release." ) - return getattr(self, '_' + attr) + return getattr(self, "_" + attr) + _getter = property(_get_deprecated) setattr(Figure, _attr, property(_get_deprecated)) @@ -1938,22 +2079,24 @@ def _get_deprecated(self, attr=_attr): # Disable native matplotlib layout and spacing functions when called # manually and emit warning message to help new users. for _attr, _msg in ( - ('set_tight_layout', Figure._tight_message), - ('set_constrained_layout', Figure._tight_message), - ('tight_layout', Figure._tight_message), - ('init_layoutbox', Figure._tight_message), - ('execute_constrained_layout', Figure._tight_message), - ('subplots_adjust', Figure._space_message), + ("set_tight_layout", Figure._tight_message), + ("set_constrained_layout", Figure._tight_message), + ("tight_layout", Figure._tight_message), + ("init_layoutbox", Figure._tight_message), + ("execute_constrained_layout", Figure._tight_message), + ("subplots_adjust", Figure._space_message), ): _func = getattr(Figure, _attr, None) if _func is None: continue + @functools.wraps(_func) # noqa: E301 def _disable_method(self, *args, func=_func, message=_msg, **kwargs): - message = f'fig.{func.__name__}() has no effect on proplot figures. ' + message + message = f"fig.{func.__name__}() has no effect on proplot figures. " + message if self._is_authorized: return func(self, *args, **kwargs) else: warnings._warn_proplot(message) # noqa: E501, U100 + _disable_method.__doc__ = None # remove docs setattr(Figure, _attr, _disable_method) diff --git a/proplot/gridspec.py b/proplot/gridspec.py index 717d6bb79..bc28c8622 100644 --- a/proplot/gridspec.py +++ b/proplot/gridspec.py @@ -19,11 +19,7 @@ from .internals import _not_none, docstring, warnings from .utils import _fontsize_to_pt, units -__all__ = [ - 'GridSpec', - 'SubplotGrid', - 'SubplotsContainer' # deprecated -] +__all__ = ["GridSpec", "SubplotGrid", "SubplotsContainer"] # deprecated # Gridspec vector arguments @@ -91,18 +87,20 @@ colorbars, and legends and between "stacks" of these objects. %(units.em)s """ -docstring._snippet_manager['gridspec.shared'] = _shared_docstring -docstring._snippet_manager['gridspec.scalar'] = _scalar_docstring -docstring._snippet_manager['gridspec.vector'] = _vector_docstring -docstring._snippet_manager['gridspec.tight'] = _tight_docstring +docstring._snippet_manager["gridspec.shared"] = _shared_docstring +docstring._snippet_manager["gridspec.scalar"] = _scalar_docstring +docstring._snippet_manager["gridspec.vector"] = _vector_docstring +docstring._snippet_manager["gridspec.tight"] = _tight_docstring def _disable_method(attr): """ Disable the inherited method. """ + def _dummy_method(*args): - raise RuntimeError(f'Method {attr}() is disabled on proplot gridspecs.') + raise RuntimeError(f"Method {attr}() is disabled on proplot gridspecs.") + _dummy_method.__name__ = attr return _dummy_method @@ -112,15 +110,16 @@ class _SubplotSpec(mgridspec.SubplotSpec): A thin `~matplotlib.gridspec.SubplotSpec` subclass with a nice string representation and a few helper methods. """ + def __repr__(self): # NOTE: Also include panel obfuscation here to avoid confusion. If this # is a panel slot generated internally then show zero info. try: nrows, ncols, num1, num2 = self._get_geometry() except (IndexError, ValueError, AttributeError): - return 'SubplotSpec(unknown)' + return "SubplotSpec(unknown)" else: - return f'SubplotSpec(nrows={nrows}, ncols={ncols}, index=({num1}, {num2}))' + return f"SubplotSpec(nrows={nrows}, ncols={ncols}, index=({num1}, {num2}))" def _get_geometry(self): """ @@ -178,22 +177,23 @@ class GridSpec(mgridspec.GridSpec): A `~matplotlib.gridspec.GridSpec` subclass that permits variable spacing between successive rows and columns and hides "panel slots" from indexing. """ + def __repr__(self): nrows, ncols = self.get_geometry() prows, pcols = self.get_panel_geometry() - params = {'nrows': nrows, 'ncols': ncols} + params = {"nrows": nrows, "ncols": ncols} if prows: - params['nrows_panel'] = prows + params["nrows_panel"] = prows if pcols: - params['ncols_panel'] = pcols - params = ', '.join(f'{key}={value!r}' for key, value in params.items()) - return f'GridSpec({params})' + params["ncols_panel"] = pcols + params = ", ".join(f"{key}={value!r}" for key, value in params.items()) + return f"GridSpec({params})" def __getattr__(self, attr): # Redirect to private 'layout' attributes that are fragile w.r.t. # matplotlib version. Cannot set these by calling super().__init__() # because we make spacing arguments non-settable properties. - if 'layout' in attr: + if "layout" in attr: return None super().__getattribute__(attr) # native error message @@ -259,37 +259,41 @@ def __init__(self, nrows=1, ncols=1, **kwargs): # instantiation. In general it seems strange for future changes to rc settings # to magically update an existing gridspec layout. This also may improve draw # time as manual or auto figure resizes repeatedly call get_grid_positions(). - scales = {'in': 0, 'inout': 0.5, 'out': 1, None: 1} - self._xtickspace = scales[rc['xtick.direction']] * rc['xtick.major.size'] - self._ytickspace = scales[rc['ytick.direction']] * rc['ytick.major.size'] - self._xticklabelspace = _fontsize_to_pt(rc['xtick.labelsize']) + rc['xtick.major.pad'] # noqa: E501 - self._yticklabelspace = 2 * _fontsize_to_pt(rc['ytick.labelsize']) + rc['ytick.major.pad'] # noqa: E501 - self._labelspace = _fontsize_to_pt(rc['axes.labelsize']) + rc['axes.labelpad'] - self._titlespace = _fontsize_to_pt(rc['axes.titlesize']) + rc['axes.titlepad'] + scales = {"in": 0, "inout": 0.5, "out": 1, None: 1} + self._xtickspace = scales[rc["xtick.direction"]] * rc["xtick.major.size"] + self._ytickspace = scales[rc["ytick.direction"]] * rc["ytick.major.size"] + self._xticklabelspace = ( + _fontsize_to_pt(rc["xtick.labelsize"]) + rc["xtick.major.pad"] + ) # noqa: E501 + self._yticklabelspace = ( + 2 * _fontsize_to_pt(rc["ytick.labelsize"]) + rc["ytick.major.pad"] + ) # noqa: E501 + self._labelspace = _fontsize_to_pt(rc["axes.labelsize"]) + rc["axes.labelpad"] + self._titlespace = _fontsize_to_pt(rc["axes.titlesize"]) + rc["axes.titlepad"] # Tight layout and panel-related properties # NOTE: The wpanels and hpanels contain empty strings '' (indicating main axes), # or one of 'l', 'r', 'b', 't' (indicating axes panels) or 'f' (figure panels) - outerpad = _not_none(kwargs.pop('outerpad', None), rc['subplots.outerpad']) - innerpad = _not_none(kwargs.pop('innerpad', None), rc['subplots.innerpad']) - panelpad = _not_none(kwargs.pop('panelpad', None), rc['subplots.panelpad']) - pad = _not_none(kwargs.pop('pad', None), innerpad) # alias of innerpad - self._outerpad = units(outerpad, 'em', 'in') - self._innerpad = units(innerpad, 'em', 'in') - self._panelpad = units(panelpad, 'em', 'in') - self._hpad_total = [units(pad, 'em', 'in')] * (nrows - 1) - self._wpad_total = [units(pad, 'em', 'in')] * (ncols - 1) - self._hequal = rc['subplots.equalspace'] - self._wequal = rc['subplots.equalspace'] - self._hgroup = rc['subplots.groupspace'] - self._wgroup = rc['subplots.groupspace'] - self._hpanels = [''] * nrows # axes and figure panel identification - self._wpanels = [''] * ncols + outerpad = _not_none(kwargs.pop("outerpad", None), rc["subplots.outerpad"]) + innerpad = _not_none(kwargs.pop("innerpad", None), rc["subplots.innerpad"]) + panelpad = _not_none(kwargs.pop("panelpad", None), rc["subplots.panelpad"]) + pad = _not_none(kwargs.pop("pad", None), innerpad) # alias of innerpad + self._outerpad = units(outerpad, "em", "in") + self._innerpad = units(innerpad, "em", "in") + self._panelpad = units(panelpad, "em", "in") + self._hpad_total = [units(pad, "em", "in")] * (nrows - 1) + self._wpad_total = [units(pad, "em", "in")] * (ncols - 1) + self._hequal = rc["subplots.equalspace"] + self._wequal = rc["subplots.equalspace"] + self._hgroup = rc["subplots.groupspace"] + self._wgroup = rc["subplots.groupspace"] + self._hpanels = [""] * nrows # axes and figure panel identification + self._wpanels = [""] * ncols self._fpanels = { # array representation of figure panel spans - 'left': np.empty((0, nrows), dtype=bool), - 'right': np.empty((0, nrows), dtype=bool), - 'bottom': np.empty((0, ncols), dtype=bool), - 'top': np.empty((0, ncols), dtype=bool), + "left": np.empty((0, nrows), dtype=bool), + "right": np.empty((0, nrows), dtype=bool), + "bottom": np.empty((0, ncols), dtype=bool), + "top": np.empty((0, ncols), dtype=bool), } self._update_params(pad=pad, **kwargs) @@ -307,6 +311,7 @@ def _make_subplot_spec(self, key, includepanels=False): """ Generate a subplotspec either ignoring panels or including panels. """ + # Convert the indices into endpoint-inclusive (start, stop) def _normalize_index(key, size, axis=None): # noqa: E306 if isinstance(key, slice): @@ -318,8 +323,8 @@ def _normalize_index(key, size, axis=None): # noqa: E306 key += size if 0 <= key < size: return key, key # endpoing inclusive - extra = 'for gridspec' if axis is None else f'along axis {axis}' - raise IndexError(f'Invalid index {key} {extra} with size {size}.') + extra = "for gridspec" if axis is None else f"along axis {axis}" + raise IndexError(f"Invalid index {key} {extra} with size {size}.") # Normalize the indices if includepanels: @@ -334,7 +339,7 @@ def _normalize_index(key, size, axis=None): # noqa: E306 num2 = _normalize_index(k2, ncols, axis=1) num1, num2 = np.ravel_multi_index((num1, num2), (nrows, ncols)) else: - raise ValueError(f'Invalid index {key!r}.') + raise ValueError(f"Invalid index {key!r}.") # Return the subplotspec if not includepanels: @@ -352,7 +357,7 @@ def _encode_indices(self, *args, which=None): try: nums.append(idxs[arg]) except (IndexError, TypeError): - raise ValueError(f'Invalid gridspec index {arg}.') + raise ValueError(f"Invalid gridspec index {arg}.") return nums[0] if len(nums) == 1 else nums def _decode_indices(self, *args, which=None): @@ -366,7 +371,7 @@ def _decode_indices(self, *args, which=None): try: nums.append(idxs.index(arg)) except ValueError: - raise ValueError(f'Invalid gridspec index {arg}.') + raise ValueError(f"Invalid gridspec index {arg}.") return nums[0] if len(nums) == 1 else nums def _filter_indices(self, key, panel=False): @@ -377,9 +382,9 @@ def _filter_indices(self, key, panel=False): # defined for consistency with the properties ending in "total". # These may be made public in a future version. which = key[0] - space = 'space' in key or 'pad' in key + space = "space" in key or "pad" in key idxs = self._get_indices(which=which, space=space, panel=panel) - vector = getattr(self, key + '_total') + vector = getattr(self, key + "_total") return [vector[i] for i in idxs] def _get_indices(self, which=None, space=False, panel=False): @@ -387,19 +392,20 @@ def _get_indices(self, which=None, space=False, panel=False): Get the indices associated with "unhidden" or "hidden" slots. """ if which: - panels = getattr(self, f'_{which}panels') + panels = getattr(self, f"_{which}panels") else: panels = [h + w for h, w in itertools.product(self._hpanels, self._wpanels)] if not space: - idxs = [ - i for i, p in enumerate(panels) if p - ] + idxs = [i for i, p in enumerate(panels) if p] else: idxs = [ - i for i, (p1, p2) in enumerate(zip(panels[:-1], panels[1:])) - if p1 == p2 == 'f' - or p1 in ('l', 't') and p2 in ('l', 't', '') - or p1 in ('r', 'b', '') and p2 in ('r', 'b') + i + for i, (p1, p2) in enumerate(zip(panels[:-1], panels[1:])) + if p1 == p2 == "f" + or p1 in ("l", "t") + and p2 in ("l", "t", "") + or p1 in ("r", "b", "") + and p2 in ("r", "b") ] if not panel: length = len(panels) - 1 if space else len(panels) @@ -434,10 +440,10 @@ def _modify_subplot_geometry(self, newrow=None, newcol=None): # nesting -- from making side colorbars with length less than 1. if ss is ax.get_subplotspec(): ax.set_subplotspec(ss_new) - elif ss is getattr(gs, '_subplot_spec', None): + elif ss is getattr(gs, "_subplot_spec", None): gs._subplot_spec = ss_new else: - raise RuntimeError('Unexpected GridSpecFromSubplotSpec nesting.') + raise RuntimeError("Unexpected GridSpecFromSubplotSpec nesting.") ax._reposition_subplot() def _parse_panel_arg(self, side, arg): @@ -452,11 +458,11 @@ def _parse_panel_arg(self, side, arg): ss = arg.get_subplotspec().get_topmost_subplotspec() offset = len(arg._panel_dict[side]) + 1 row1, row2, col1, col2 = ss._get_rows_columns() - if side in ('left', 'right'): - iratio = col1 - offset if side == 'left' else col2 + offset + if side in ("left", "right"): + iratio = col1 - offset if side == "left" else col2 + offset start, stop = row1, row2 else: - iratio = row1 - offset if side == 'top' else row2 + offset + iratio = row1 - offset if side == "top" else row2 + offset start, stop = col1, col2 # Add a figure panel. Index depends on the side and the input 'span' @@ -466,24 +472,32 @@ def _parse_panel_arg(self, side, arg): # tracked with figure panel array (a boolean mask where each row corresponds # to a panel, moving toward the outside, and True indicates a slot is filled). elif ( - arg is None or isinstance(arg, Integral) - or np.iterable(arg) and all(isinstance(_, Integral) for _ in arg) + arg is None + or isinstance(arg, Integral) + or np.iterable(arg) + and all(isinstance(_, Integral) for _ in arg) ): - slot = 'f' + slot = "f" array = self._fpanels[side] - nacross = self._ncols_total if side in ('left', 'right') else self._nrows_total # noqa: E501 + nacross = ( + self._ncols_total if side in ("left", "right") else self._nrows_total + ) # noqa: E501 npanels, nalong = array.shape arg = np.atleast_1d(_not_none(arg, (1, nalong))) if arg.size not in (1, 2): - raise ValueError(f'Invalid span={arg!r}. Must be scalar or 2-tuple of coordinates.') # noqa: E501 + raise ValueError( + f"Invalid span={arg!r}. Must be scalar or 2-tuple of coordinates." + ) # noqa: E501 if any(s < 1 or s > nalong for s in arg): - raise ValueError(f'Invalid span={arg!r}. Coordinates must satisfy 1 <= c <= {nalong}.') # noqa: E501 + raise ValueError( + f"Invalid span={arg!r}. Coordinates must satisfy 1 <= c <= {nalong}." + ) # noqa: E501 start, stop = arg[0] - 1, arg[-1] # non-inclusive starting at zero - iratio = -1 if side in ('left', 'top') else nacross # default values + iratio = -1 if side in ("left", "top") else nacross # default values for i in range(npanels): # possibly use existing panel slot if not any(array[i, start:stop]): array[i, start:stop] = True - if side in ('left', 'top'): # descending moves us closer to 0 + if side in ("left", "top"): # descending moves us closer to 0 iratio = npanels - 1 - i # index in ratios array else: # descending array moves us closer to nacross - 1 iratio = nacross - (npanels - i) # index in ratios array @@ -493,18 +507,26 @@ def _parse_panel_arg(self, side, arg): iarray[0, start:stop] = True array = np.concatenate((array, iarray), axis=0) self._fpanels[side] = array # replace array - which = 'h' if side in ('left', 'right') else 'w' + which = "h" if side in ("left", "right") else "w" start, stop = self._encode_indices(start, stop - 1, which=which) else: - raise ValueError(f'Invalid panel argument {arg!r}.') + raise ValueError(f"Invalid panel argument {arg!r}.") # Return subplotspec indices # NOTE: Convert using the lengthwise indices return slot, iratio, slice(start, stop + 1) def _insert_panel_slot( - self, side, arg, *, share=None, width=None, space=None, pad=None, filled=False, + self, + side, + arg, + *, + share=None, + width=None, + space=None, + pad=None, + filled=False, ): """ Insert a panel slot into the existing gridspec. The `side` is the panel side @@ -513,20 +535,20 @@ def _insert_panel_slot( # Parse input args and get user-input properties, default properties fig = self.figure if fig is None: - raise RuntimeError('Figure must be assigned to gridspec.') - if side not in ('left', 'right', 'bottom', 'top'): - raise ValueError(f'Invalid side {side}.') + raise RuntimeError("Figure must be assigned to gridspec.") + if side not in ("left", "right", "bottom", "top"): + raise ValueError(f"Invalid side {side}.") slot, idx, span = self._parse_panel_arg(side, arg) - pad = units(pad, 'em', 'in') - space = units(space, 'em', 'in') - width = units(width, 'in') + pad = units(pad, "em", "in") + space = units(space, "em", "in") + width = units(width, "in") share = False if filled else share if share is not None else True - which = 'w' if side in ('left', 'right') else 'h' - panels = getattr(self, f'_{which}panels') - pads = getattr(self, f'_{which}pad_total') # no copies! - ratios = getattr(self, f'_{which}ratios_total') - spaces = getattr(self, f'_{which}space_total') - spaces_default = getattr(self, f'_{which}space_total_default') + which = "w" if side in ("left", "right") else "h" + panels = getattr(self, f"_{which}panels") + pads = getattr(self, f"_{which}pad_total") # no copies! + ratios = getattr(self, f"_{which}ratios_total") + spaces = getattr(self, f"_{which}space_total") + spaces_default = getattr(self, f"_{which}space_total_default") new_outer_slot = idx in (-1, len(panels)) new_inner_slot = not new_outer_slot and panels[idx] != slot @@ -535,51 +557,55 @@ def _insert_panel_slot( # that adds an unnecessary tick space. So bypass _get_default_space totally. pad_default = ( self._panelpad - if slot != 'f' - or side in ('left', 'top') and panels[0] == 'f' - or side in ('right', 'bottom') and panels[-1] == 'f' + if slot != "f" + or side in ("left", "top") + and panels[0] == "f" + or side in ("right", "bottom") + and panels[-1] == "f" else self._innerpad ) inner_space_default = ( _not_none(pad, pad_default) - if side in ('top', 'right') + if side in ("top", "right") else self._get_default_space( - 'hspace_total' if side == 'bottom' else 'wspace_total', + "hspace_total" if side == "bottom" else "wspace_total", title=False, # no title between subplot and panel share=3 if share else 0, # space for main subplot labels pad=_not_none(pad, pad_default), ) ) outer_space_default = self._get_default_space( - 'bottom' if not share and side == 'top' - else 'left' if not share and side == 'right' - else side, + ( + "bottom" + if not share and side == "top" + else "left" if not share and side == "right" else side + ), title=True, # room for titles deflected above panels pad=self._outerpad if new_outer_slot else self._innerpad, ) if new_inner_slot: outer_space_default += self._get_default_space( - 'hspace_total' if side in ('bottom', 'top') else 'wspace_total', + "hspace_total" if side in ("bottom", "top") else "wspace_total", share=None, # use external share setting pad=0, # use no additional padding ) width_default = units( - rc['colorbar.width' if filled else 'subplots.panelwidth'], 'in' + rc["colorbar.width" if filled else "subplots.panelwidth"], "in" ) # Adjust space, ratio, and panel indicator arrays # If slot exists, overwrite width, pad, space if they were provided by the user # If slot does not exist, modify gemoetry and add insert new spaces - attr = 'ncols' if side in ('left', 'right') else 'nrows' - idx_offset = int(side in ('top', 'left')) - idx_inner_space = idx - int(side in ('bottom', 'right')) # inner colorbar space - idx_outer_space = idx - int(side in ('top', 'left')) # outer colorbar space + attr = "ncols" if side in ("left", "right") else "nrows" + idx_offset = int(side in ("top", "left")) + idx_inner_space = idx - int(side in ("bottom", "right")) # inner colorbar space + idx_outer_space = idx - int(side in ("top", "left")) # outer colorbar space if new_outer_slot or new_inner_slot: idx += idx_offset idx_inner_space += idx_offset idx_outer_space += idx_offset - newcol, newrow = (idx, None) if attr == 'ncols' else (None, idx) - setattr(self, f'_{attr}_total', 1 + getattr(self, f'_{attr}_total')) + newcol, newrow = (idx, None) if attr == "ncols" else (None, idx) + setattr(self, f"_{attr}_total", 1 + getattr(self, f"_{attr}_total")) panels.insert(idx, slot) ratios.insert(idx, _not_none(width, width_default)) pads.insert(idx_inner_space, _not_none(pad, pad_default)) @@ -588,7 +614,7 @@ def _insert_panel_slot( if new_inner_slot: spaces_default.insert(idx_outer_space, outer_space_default) else: - setattr(self, f'_{side}_default', outer_space_default) + setattr(self, f"_{side}_default", outer_space_default) else: newrow = newcol = None spaces_default[idx_inner_space] = inner_space_default @@ -607,7 +633,7 @@ def _insert_panel_slot( fig.set_size_inches(figsize, internal=True, forward=False) else: self.update() - key = (span, idx) if side in ('left', 'right') else (idx, span) + key = (span, idx) if side in ("left", "right") else (idx, span) ss = self._make_subplot_spec(key, includepanels=True) # bypass obfuscation return ss, share @@ -621,17 +647,17 @@ def _get_space(self, key): # instead fills spaces between subplots depending on sharing setting. fig = self.figure if not fig: - raise ValueError('Figure must be assigned to get grid positions.') - attr = f'_{key}' # user-specified - attr_default = f'_{key}_default' # default values + raise ValueError("Figure must be assigned to get grid positions.") + attr = f"_{key}" # user-specified + attr_default = f"_{key}_default" # default values value = getattr(self, attr) value_default = getattr(self, attr_default) - if key in ('left', 'right', 'bottom', 'top'): + if key in ("left", "right", "bottom", "top"): if value_default is None: value_default = self._get_default_space(key) setattr(self, attr_default, value_default) return _not_none(value, value_default) - elif key in ('wspace_total', 'hspace_total'): + elif key in ("wspace_total", "hspace_total"): result = [] for i, (val, val_default) in enumerate(zip(value, value_default)): if val_default is None: @@ -640,7 +666,7 @@ def _get_space(self, key): result.append(_not_none(val, val_default)) return result else: - raise ValueError(f'Unknown space parameter {key!r}.') + raise ValueError(f"Unknown space parameter {key!r}.") def _get_default_space(self, key, pad=None, share=None, title=True): """ @@ -651,20 +677,20 @@ def _get_default_space(self, key, pad=None, share=None, title=True): # get_grid_positions() calculations. fig = self.figure if fig is None: - raise RuntimeError('Figure must be assigned.') - if key == 'right': + raise RuntimeError("Figure must be assigned.") + if key == "right": pad = _not_none(pad, self._outerpad) space = 0 - elif key == 'top': + elif key == "top": pad = _not_none(pad, self._outerpad) space = self._titlespace if title else 0 - elif key == 'left': + elif key == "left": pad = _not_none(pad, self._outerpad) space = self._labelspace + self._yticklabelspace + self._ytickspace - elif key == 'bottom': + elif key == "bottom": pad = _not_none(pad, self._outerpad) space = self._labelspace + self._xticklabelspace + self._xtickspace - elif key == 'wspace_total': + elif key == "wspace_total": pad = _not_none(pad, self._innerpad) share = _not_none(share, fig._sharey, 0) space = self._ytickspace @@ -672,7 +698,7 @@ def _get_default_space(self, key, pad=None, share=None, title=True): space += self._yticklabelspace if share < 1: space += self._labelspace - elif key == 'hspace_total': + elif key == "hspace_total": pad = _not_none(pad, self._innerpad) share = _not_none(share, fig._sharex, 0) space = self._xtickspace @@ -683,7 +709,7 @@ def _get_default_space(self, key, pad=None, share=None, title=True): if share < 1: space += self._labelspace else: - raise ValueError(f'Invalid space key {key!r}.') + raise ValueError(f"Invalid space key {key!r}.") return pad + space / 72 def _get_tight_space(self, w): @@ -694,14 +720,14 @@ def _get_tight_space(self, w): fig = self.figure if not fig: return - if w == 'w': - x, y = 'xy' + if w == "w": + x, y = "xy" group = self._wgroup nacross = self.nrows_total space = self.wspace_total pad = self.wpad_total else: - x, y = 'yx' + x, y = "yx" group = self._hgroup nacross = self.ncols_total space = self.hspace_total @@ -725,18 +751,18 @@ def _get_tight_space(self, w): idx1 = idx2 = np.array(()) while ii >= 0 and idx1.size == 0: filt1 = ralong[:, 1] == ii # i.e. r / b edge abutts against this - idx1, = np.where(filt & filt1) + (idx1,) = np.where(filt & filt1) ii -= 1 ii = i + 1 while ii <= len(space) and idx2.size == 0: filt2 = ralong[:, 0] == ii # i.e. l / t edge abutts against this - idx2, = np.where(filt & filt2) + (idx2,) = np.where(filt & filt2) ii += 1 # Put axes into unique groups and store as (l, r) or (b, t) pairs. axs1, axs2 = [axs[_] for _ in idx1], [axs[_] for _ in idx2] - if x != 'x': # order bottom-to-top + if x != "x": # order bottom-to-top axs1, axs2 = axs2, axs1 - for (group1, group2) in groups: + for group1, group2 in groups: if any(_ in group1 for _ in axs1) or any(_ in group2 for _ in axs2): group1.update(axs1) group2.update(axs2) @@ -747,12 +773,14 @@ def _get_tight_space(self, w): # Determing the spaces using cached tight bounding boxes # NOTE: Set gridspec space to zero if there are no adjacent edges if not group: - groups = [( - set(ax for (group1, _) in groups for ax in group1), - set(ax for (_, group2) in groups for ax in group2), - )] + groups = [ + ( + set(ax for (group1, _) in groups for ax in group1), + set(ax for (_, group2) in groups for ax in group2), + ) + ] margins = [] - for (group1, group2) in groups: + for group1, group2 in groups: x1 = max(ax._range_tightbbox(x)[1] for ax in group1) x2 = min(ax._range_tightbbox(x)[0] for ax in group2) margins.append((x2 - x1) / self.figure.dpi) @@ -775,12 +803,12 @@ def _auto_layout_aspect(self): # Get aspect ratio ratio = ax.get_aspect() # the aspect ratio in *data units* - if ratio == 'auto': + if ratio == "auto": return - elif ratio == 'equal': + elif ratio == "equal": ratio = 1 elif isinstance(ratio, str): - raise RuntimeError(f'Unknown aspect ratio mode {ratio!r}.') + raise RuntimeError(f"Unknown aspect ratio mode {ratio!r}.") else: ratio = 1 / ratio @@ -835,15 +863,15 @@ def _auto_layout_tight(self, renderer): # Calculate new subplot row and column spaces. Enforce equal # default spaces between main subplot edges if requested. - hspace = self._get_tight_space('h') - wspace = self._get_tight_space('w') + hspace = self._get_tight_space("h") + wspace = self._get_tight_space("w") if self._hequal: - idxs = self._get_indices('h', space=True) + idxs = self._get_indices("h", space=True) space = max(hspace[i] for i in idxs) for i in idxs: hspace[i] = space if self._wequal: - idxs = self._get_indices('w', space=True) + idxs = self._get_indices("w", space=True) space = max(wspace[i] for i in idxs) for i in idxs: wspace[i] = space @@ -875,10 +903,18 @@ def _update_figsize(self): y1, y2, x1, x2 = ss._get_rows_columns() refhspace = sum(self.hspace_total[y1:y2]) refwspace = sum(self.wspace_total[x1:x2]) - refhpanel = sum(self.hratios_total[i] for i in range(y1, y2 + 1) if self._hpanels[i]) # noqa: E501 - refwpanel = sum(self.wratios_total[i] for i in range(x1, x2 + 1) if self._wpanels[i]) # noqa: E501 - refhsubplot = sum(self.hratios_total[i] for i in range(y1, y2 + 1) if not self._hpanels[i]) # noqa: E501 - refwsubplot = sum(self.wratios_total[i] for i in range(x1, x2 + 1) if not self._wpanels[i]) # noqa: E501 + refhpanel = sum( + self.hratios_total[i] for i in range(y1, y2 + 1) if self._hpanels[i] + ) # noqa: E501 + refwpanel = sum( + self.wratios_total[i] for i in range(x1, x2 + 1) if self._wpanels[i] + ) # noqa: E501 + refhsubplot = sum( + self.hratios_total[i] for i in range(y1, y2 + 1) if not self._hpanels[i] + ) # noqa: E501 + refwsubplot = sum( + self.wratios_total[i] for i in range(x1, x2 + 1) if not self._wpanels[i] + ) # noqa: E501 # Get the reference sizes # NOTE: The sizing arguments should have been normalized already @@ -892,7 +928,7 @@ def _update_figsize(self): if refwidth is not None: # WARNING: do not change to elif! refheight = refwidth / refaspect else: - raise RuntimeError('Figure size arguments are all missing.') + raise RuntimeError("Figure size arguments are all missing.") if refwidth is None and figwidth is None: if figheight is not None: gridheight = figheight - self.spaceheight - self.panelheight @@ -900,7 +936,7 @@ def _update_figsize(self): if refheight is not None: refwidth = refheight * refaspect else: - raise RuntimeError('Figure size arguments are all missing.') + raise RuntimeError("Figure size arguments are all missing.") # Get the auto figure size. Might trigger 'not enough room' error later # NOTE: For e.g. [[1, 1, 2, 2], [0, 3, 3, 0]] we make sure to still scale the @@ -919,21 +955,39 @@ def _update_figsize(self): if all(np.isfinite(figsize)): return figsize else: - warnings._warn_proplot(f'Auto resize failed. Invalid figsize {figsize}.') + warnings._warn_proplot(f"Auto resize failed. Invalid figsize {figsize}.") def _update_params( - self, *, - left=None, bottom=None, right=None, top=None, - wspace=None, hspace=None, space=None, - wpad=None, hpad=None, pad=None, - wequal=None, hequal=None, equal=None, - wgroup=None, hgroup=None, group=None, - outerpad=None, innerpad=None, panelpad=None, - hratios=None, wratios=None, width_ratios=None, height_ratios=None, + self, + *, + left=None, + bottom=None, + right=None, + top=None, + wspace=None, + hspace=None, + space=None, + wpad=None, + hpad=None, + pad=None, + wequal=None, + hequal=None, + equal=None, + wgroup=None, + hgroup=None, + group=None, + outerpad=None, + innerpad=None, + panelpad=None, + hratios=None, + wratios=None, + width_ratios=None, + height_ratios=None, ): """ Update the user-specified properties. """ + # Assign scalar args # WARNING: The key signature here is critical! Used in ui.py to # separate out figure keywords and gridspec keywords. @@ -941,25 +995,26 @@ def _assign_scalar(key, value, convert=True): if value is None: return if not np.isscalar(value): - raise ValueError(f'Unexpected {key}={value!r}. Must be scalar.') + raise ValueError(f"Unexpected {key}={value!r}. Must be scalar.") if convert: - value = units(value, 'em', 'in') - setattr(self, f'_{key}', value) + value = units(value, "em", "in") + setattr(self, f"_{key}", value) + hequal = _not_none(hequal, equal) wequal = _not_none(wequal, equal) hgroup = _not_none(hgroup, group) wgroup = _not_none(wgroup, group) - _assign_scalar('left', left) - _assign_scalar('right', right) - _assign_scalar('bottom', bottom) - _assign_scalar('top', top) - _assign_scalar('panelpad', panelpad) - _assign_scalar('outerpad', outerpad) - _assign_scalar('innerpad', innerpad) - _assign_scalar('hequal', hequal, convert=False) - _assign_scalar('wequal', wequal, convert=False) - _assign_scalar('hgroup', hgroup, convert=False) - _assign_scalar('wgroup', wgroup, convert=False) + _assign_scalar("left", left) + _assign_scalar("right", right) + _assign_scalar("bottom", bottom) + _assign_scalar("top", top) + _assign_scalar("panelpad", panelpad) + _assign_scalar("outerpad", outerpad) + _assign_scalar("innerpad", innerpad) + _assign_scalar("hequal", hequal, convert=False) + _assign_scalar("wequal", wequal, convert=False) + _assign_scalar("hgroup", hgroup, convert=False) + _assign_scalar("wgroup", wgroup, convert=False) # Assign vector args # NOTE: Here we employ obfuscation that skips 'panel' indices. So users could @@ -975,32 +1030,33 @@ def _assign_vector(key, values, space): if values.size == 1: values = np.repeat(values, nidxs) if values.size != nidxs: - raise ValueError(f'Expected len({key}) == {nidxs}. Got {values.size}.') - list_ = getattr(self, f'_{key}_total') + raise ValueError(f"Expected len({key}) == {nidxs}. Got {values.size}.") + list_ = getattr(self, f"_{key}_total") for i, value in enumerate(values): if value is None: continue list_[idxs[i]] = value + if pad is not None and not np.isscalar(pad): - raise ValueError(f'Parameter pad={pad!r} must be scalar.') + raise ValueError(f"Parameter pad={pad!r} must be scalar.") if space is not None and not np.isscalar(space): - raise ValueError(f'Parameter space={space!r} must be scalar.') + raise ValueError(f"Parameter space={space!r} must be scalar.") hpad = _not_none(hpad, pad) wpad = _not_none(wpad, pad) - hpad = units(hpad, 'em', 'in') - wpad = units(wpad, 'em', 'in') + hpad = units(hpad, "em", "in") + wpad = units(wpad, "em", "in") hspace = _not_none(hspace, space) wspace = _not_none(wspace, space) - hspace = units(hspace, 'em', 'in') - wspace = units(wspace, 'em', 'in') + hspace = units(hspace, "em", "in") + wspace = units(wspace, "em", "in") hratios = _not_none(hratios=hratios, height_ratios=height_ratios) wratios = _not_none(wratios=wratios, width_ratios=width_ratios) - _assign_vector('hpad', hpad, space=True) - _assign_vector('wpad', wpad, space=True) - _assign_vector('hspace', hspace, space=True) - _assign_vector('wspace', wspace, space=True) - _assign_vector('hratios', hratios, space=False) - _assign_vector('wratios', wratios, space=False) + _assign_vector("hpad", hpad, space=True) + _assign_vector("wpad", wpad, space=True) + _assign_vector("hspace", hspace, space=True) + _assign_vector("wspace", wspace, space=True) + _assign_vector("hratios", hratios, space=False) + _assign_vector("wratios", wratios, space=False) @docstring._snippet_manager def copy(self, **kwargs): @@ -1024,12 +1080,12 @@ def copy(self, **kwargs): # and hpanels on the copy also updates this object. No idea why. nrows, ncols = self.get_geometry() gs = GridSpec(nrows, ncols) - hidxs = self._get_indices('h') - widxs = self._get_indices('w') + hidxs = self._get_indices("h") + widxs = self._get_indices("w") gs._hratios_total = [self._hratios_total[i] for i in hidxs] gs._wratios_total = [self._wratios_total[i] for i in widxs] - hidxs = self._get_indices('h', space=True) - widxs = self._get_indices('w', space=True) + hidxs = self._get_indices("h", space=True) + widxs = self._get_indices("w", space=True) gs._hpad_total = [self._hpad_total[i] for i in hidxs] gs._wpad_total = [self._wpad_total[i] for i in widxs] gs._hspace_total = [self._hspace_total[i] for i in hidxs] @@ -1037,12 +1093,24 @@ def copy(self, **kwargs): gs._hspace_total_default = [self._hspace_total_default[i] for i in hidxs] gs._wspace_total_default = [self._wspace_total_default[i] for i in widxs] for key in ( - 'left', 'right', 'bottom', 'top', 'labelspace', 'titlespace', - 'xtickspace', 'ytickspace', 'xticklabelspace', 'yticklabelspace', - 'outerpad', 'innerpad', 'panelpad', 'hequal', 'wequal', + "left", + "right", + "bottom", + "top", + "labelspace", + "titlespace", + "xtickspace", + "ytickspace", + "xticklabelspace", + "yticklabelspace", + "outerpad", + "innerpad", + "panelpad", + "hequal", + "wequal", ): - value = getattr(self, '_' + key) - setattr(gs, '_' + key, value) + value = getattr(self, "_" + key) + setattr(gs, "_" + key, value) gs.update(**kwargs) return gs @@ -1108,15 +1176,17 @@ def get_grid_positions(self, figure=None): if not self.figure: self._figure = figure if not self.figure: - raise RuntimeError('Figure must be assigned to gridspec.') + raise RuntimeError("Figure must be assigned to gridspec.") if figure is not self.figure: - raise RuntimeError(f'Input figure {figure} does not match gridspec figure {self.figure}.') # noqa: E501 + raise RuntimeError( + f"Input figure {figure} does not match gridspec figure {self.figure}." + ) # noqa: E501 fig = _not_none(figure, self.figure) figwidth, figheight = fig.get_size_inches() spacewidth, spaceheight = self.spacewidth, self.spaceheight panelwidth, panelheight = self.panelwidth, self.panelheight hratios, wratios = self.hratios_total, self.wratios_total - hidxs, widxs = self._get_indices('h'), self._get_indices('w') + hidxs, widxs = self._get_indices("h"), self._get_indices("w") # Scale the subplot slot ratios and keep the panel slots fixed hsubplot = np.array([hratios[i] for i in hidxs]) @@ -1132,7 +1202,7 @@ def get_grid_positions(self, figure=None): norm = (figheight - spaceheight) / (figheight * sum(hratios)) if norm < 0: raise RuntimeError( - 'Not enough room for axes. Try increasing the figure height or ' + "Not enough room for axes. Try increasing the figure height or " "decreasing the 'top', 'bottom', or 'hspace' gridspec spaces." ) cell_heights = [r * norm for r in hratios] @@ -1143,7 +1213,7 @@ def get_grid_positions(self, figure=None): norm = (figwidth - spacewidth) / (figwidth * sum(wratios)) if norm < 0: raise RuntimeError( - 'Not enough room for axes. Try increasing the figure width or ' + "Not enough room for axes. Try increasing the figure width or " "decreasing the 'left', 'right', or 'wspace' gridspec spaces." ) cell_widths = [r * norm for r in wratios] @@ -1203,12 +1273,13 @@ def figure(self): @figure.setter def figure(self, fig): from .figure import Figure + if not isinstance(fig, Figure): - raise ValueError('Figure must be a proplot figure.') + raise ValueError("Figure must be a proplot figure.") if self._figure and self._figure is not fig: raise ValueError( - 'Cannot use the same gridspec for multiple figures. ' - 'Please use gridspec.copy() to make a copy.' + "Cannot use the same gridspec for multiple figures. " + "Please use gridspec.copy() to make a copy." ) self._figure = fig self._update_params(**fig._gridspec_params) @@ -1222,14 +1293,14 @@ def figure(self, fig): # Delete attributes. Don't like having special setters and getters for some # settings and not others. Width and height ratios can be updated with update(). # Also delete obsolete 'subplotpars' and built-in tight layout function. - tight_layout = _disable_method('tight_layout') # instead use custom tight layout - subgridspec = _disable_method('subgridspec') # instead use variable spaces - get_width_ratios = _disable_method('get_width_ratios') - get_height_ratios = _disable_method('get_height_ratios') - set_width_ratios = _disable_method('set_width_ratios') - set_height_ratios = _disable_method('set_height_ratios') - get_subplot_params = _disable_method('get_subplot_params') - locally_modified_subplot_params = _disable_method('locally_modified_subplot_params') + tight_layout = _disable_method("tight_layout") # instead use custom tight layout + subgridspec = _disable_method("subgridspec") # instead use variable spaces + get_width_ratios = _disable_method("get_width_ratios") + get_height_ratios = _disable_method("get_height_ratios") + set_width_ratios = _disable_method("set_width_ratios") + set_height_ratios = _disable_method("set_height_ratios") + get_subplot_params = _disable_method("get_subplot_params") + locally_modified_subplot_params = _disable_method("locally_modified_subplot_params") # Immutable helper properties used to calculate figure size and subplot positions # NOTE: The spaces are auto-filled with defaults wherever user left them unset @@ -1242,8 +1313,12 @@ def figure(self, fig): # Geometry properties. These are included for consistency with get_geometry # functions (would be really confusing if self.nrows, self.ncols disagree). - nrows = property(lambda self: self._nrows_total - sum(map(bool, self._hpanels)), doc='') # noqa: E501 - ncols = property(lambda self: self._ncols_total - sum(map(bool, self._wpanels)), doc='') # noqa: E501 + nrows = property( + lambda self: self._nrows_total - sum(map(bool, self._hpanels)), doc="" + ) # noqa: E501 + ncols = property( + lambda self: self._ncols_total - sum(map(bool, self._wpanels)), doc="" + ) # noqa: E501 nrows_panel = property(lambda self: sum(map(bool, self._hpanels))) ncols_panel = property(lambda self: sum(map(bool, self._wpanels))) nrows_total = property(lambda self: self._nrows_total) @@ -1253,26 +1328,26 @@ def figure(self, fig): # properties so they try to retrieve user settings then fallback to defaults. # NOTE: These are undocumented for the time being. Generally properties should # be changed with update() and introspection not really necessary. - left = property(lambda self: self._get_space('left')) - bottom = property(lambda self: self._get_space('bottom')) - right = property(lambda self: self._get_space('right')) - top = property(lambda self: self._get_space('top')) - hratios = property(lambda self: self._filter_indices('hratios', panel=False)) - wratios = property(lambda self: self._filter_indices('wratios', panel=False)) - hratios_panel = property(lambda self: self._filter_indices('hratios', panel=True)) - wratios_panel = property(lambda self: self._filter_indices('wratios', panel=True)) + left = property(lambda self: self._get_space("left")) + bottom = property(lambda self: self._get_space("bottom")) + right = property(lambda self: self._get_space("right")) + top = property(lambda self: self._get_space("top")) + hratios = property(lambda self: self._filter_indices("hratios", panel=False)) + wratios = property(lambda self: self._filter_indices("wratios", panel=False)) + hratios_panel = property(lambda self: self._filter_indices("hratios", panel=True)) + wratios_panel = property(lambda self: self._filter_indices("wratios", panel=True)) hratios_total = property(lambda self: list(self._hratios_total)) wratios_total = property(lambda self: list(self._wratios_total)) - hspace = property(lambda self: self._filter_indices('hspace', panel=False)) - wspace = property(lambda self: self._filter_indices('wspace', panel=False)) - hspace_panel = property(lambda self: self._filter_indices('hspace', panel=True)) - wspace_panel = property(lambda self: self._filter_indices('wspace', panel=True)) - hspace_total = property(lambda self: self._get_space('hspace_total')) - wspace_total = property(lambda self: self._get_space('wspace_total')) - hpad = property(lambda self: self._filter_indices('hpad', panel=False)) - wpad = property(lambda self: self._filter_indices('wpad', panel=False)) - hpad_panel = property(lambda self: self._filter_indices('hpad', panel=True)) - wpad_panel = property(lambda self: self._filter_indices('wpad', panel=True)) + hspace = property(lambda self: self._filter_indices("hspace", panel=False)) + wspace = property(lambda self: self._filter_indices("wspace", panel=False)) + hspace_panel = property(lambda self: self._filter_indices("hspace", panel=True)) + wspace_panel = property(lambda self: self._filter_indices("wspace", panel=True)) + hspace_total = property(lambda self: self._get_space("hspace_total")) + wspace_total = property(lambda self: self._get_space("wspace_total")) + hpad = property(lambda self: self._filter_indices("hpad", panel=False)) + wpad = property(lambda self: self._filter_indices("wpad", panel=False)) + hpad_panel = property(lambda self: self._filter_indices("hpad", panel=True)) + wpad_panel = property(lambda self: self._filter_indices("wpad", panel=True)) hpad_total = property(lambda self: list(self._hpad_total)) wpad_total = property(lambda self: list(self._wpad_total)) @@ -1284,12 +1359,13 @@ class SubplotGrid(MutableSequence, list): `~proplot.axes.Axes` while 2D indexing uses the `~SubplotGrid.gridspec`. See `~SubplotGrid.__getitem__` for details. """ + def __repr__(self): if not self: - return 'SubplotGrid(length=0)' + return "SubplotGrid(length=0)" length = len(self) nrows, ncols = self.gridspec.get_geometry() - return f'SubplotGrid(nrows={nrows}, ncols={ncols}, length={length})' + return f"SubplotGrid(nrows={nrows}, ncols={ncols}, length={length})" def __str__(self): return self.__repr__() @@ -1314,14 +1390,14 @@ def __init__(self, sequence=None, **kwargs): proplot.figure.Figure.subplots proplot.figure.Figure.add_subplots """ - n = kwargs.pop('n', None) - order = kwargs.pop('order', None) + n = kwargs.pop("n", None) + order = kwargs.pop("order", None) if n is not None or order is not None: warnings._warn_proplot( - f'Ignoring n={n!r} and order={order!r}. As of v0.8 SubplotGrid ' - 'handles 2D indexing by leveraging the subplotspec extents rather than ' - 'directly emulating 2D array indexing. These arguments are no longer ' - 'needed and will be removed in a future release.' + f"Ignoring n={n!r} and order={order!r}. As of v0.8 SubplotGrid " + "handles 2D indexing by leveraging the subplotspec extents rather than " + "directly emulating 2D array indexing. These arguments are no longer " + "needed and will be removed in a future release." ) sequence = _not_none(sequence, []) sequence = self._validate_item(sequence, scalar=False) @@ -1334,7 +1410,7 @@ def __getattr__(self, attr): single-axes figures generated with `~proplot.figure.Figure.subplots`. """ # Redirect to the axes - if not self or attr[:1] == '_': + if not self or attr[:1] == "_": return super().__getattribute__(attr) # trigger default error if len(self) == 1: return getattr(self[0], attr) @@ -1343,10 +1419,11 @@ def __getattr__(self, attr): # WARNING: This is now deprecated! Instead we dynamically define a few # dedicated relevant commands that can be called from the grid (see below). import functools + warnings._warn_proplot( - 'Calling arbitrary axes methods from SubplotGrid was deprecated in v0.8 ' - 'and will be removed in a future release. Please index the grid or loop ' - 'over the grid instead.' + "Calling arbitrary axes methods from SubplotGrid was deprecated in v0.8 " + "and will be removed in a future release. Please index the grid or loop " + "over the grid instead." ) if not self: return None @@ -1354,6 +1431,7 @@ def __getattr__(self, attr): if not any(map(callable, objs)): return objs[0] if len(self) == 1 else objs elif all(map(callable, objs)): + @functools.wraps(objs[0]) def _iterate_subplots(*args, **kwargs): result = [] @@ -1367,10 +1445,11 @@ def _iterate_subplots(*args, **kwargs): return SubplotGrid(result, n=self._n, order=self._order) else: return tuple(result) + _iterate_subplots.__doc__ = inspect.getdoc(objs[0]) return _iterate_subplots else: - raise AttributeError(f'Found mixed types for attribute {attr!r}.') + raise AttributeError(f"Found mixed types for attribute {attr!r}.") def __getitem__(self, key): """ @@ -1428,7 +1507,7 @@ def __getitem__(self, key): if not slices and len(objs) == 1: # accounts for overlapping subplots objs = objs[0] else: - raise IndexError(f'Invalid index {key!r}.') + raise IndexError(f"Invalid index {key!r}.") if isinstance(objs, list): return SubplotGrid(objs) else: @@ -1451,7 +1530,7 @@ def __setitem__(self, key, value): elif isinstance(key, slice): value = self._validate_item(value, scalar=False) else: - raise IndexError('Multi dimensional item assignment is not supported.') + raise IndexError("Multi dimensional item assignment is not supported.") return super().__setitem__(key, value) # could be list[:] = [1, 2, 3] @classmethod @@ -1459,6 +1538,7 @@ def _add_command(cls, src, name): """ Add a `SubplotGrid` method that iterates through axes methods. """ + # Create the method def _grid_command(self, *args, **kwargs): objs = [] @@ -1470,17 +1550,17 @@ def _grid_command(self, *args, **kwargs): # Clean the docstring cmd = getattr(src, name) doc = inspect.cleandoc(cmd.__doc__) # dedents - dot = doc.find('.') + dot = doc.find(".") if dot != -1: - doc = doc[:dot] + ' for every axes in the grid' + doc[dot:] + doc = doc[:dot] + " for every axes in the grid" + doc[dot:] doc = re.sub( - r'^(Returns\n-------\n)(.+)(\n\s+)(.+)', - r'\1SubplotGrid\2A grid of the resulting axes.', - doc + r"^(Returns\n-------\n)(.+)(\n\s+)(.+)", + r"\1SubplotGrid\2A grid of the resulting axes.", + doc, ) # Apply the method - _grid_command.__qualname__ = f'SubplotGrid.{name}' + _grid_command.__qualname__ = f"SubplotGrid.{name}" _grid_command.__name__ = name _grid_command.__doc__ = doc setattr(cls, name, _grid_command) @@ -1491,30 +1571,30 @@ def _validate_item(self, items, scalar=False): """ gridspec = None message = ( - 'SubplotGrid can only be filled with proplot subplots ' - 'belonging to the same GridSpec. Instead got {}.' + "SubplotGrid can only be filled with proplot subplots " + "belonging to the same GridSpec. Instead got {}." ) items = np.atleast_1d(items) if self: gridspec = self.gridspec # compare against existing gridspec for item in items.flat: if not isinstance(item, paxes.Axes): - raise ValueError(message.format(f'the object {item!r}')) + raise ValueError(message.format(f"the object {item!r}")) item = item._get_topmost_axes() if not isinstance(item, maxes.SubplotBase): - raise ValueError(message.format(f'the axes {item!r}')) + raise ValueError(message.format(f"the axes {item!r}")) gs = item.get_subplotspec().get_topmost_subplotspec().get_gridspec() if not isinstance(gs, GridSpec): - raise ValueError(message.format(f'the GridSpec {gs!r}')) + raise ValueError(message.format(f"the GridSpec {gs!r}")) if gridspec and gs is not gridspec: - raise ValueError(message.format('at least two different GridSpecs')) + raise ValueError(message.format("at least two different GridSpecs")) gridspec = gs if not scalar: items = tuple(items.flat) elif items.size == 1: items = items.flat[0] else: - raise ValueError('Input must be a single proplot axes.') + raise ValueError("Input must be a single proplot axes.") return items @docstring._snippet_manager @@ -1577,7 +1657,7 @@ def gridspec(self): """ # Return the gridspec associatd with the grid if not self: - raise ValueError('Unknown gridspec for empty SubplotGrid.') + raise ValueError("Unknown gridspec for empty SubplotGrid.") ax = self[0] ax = ax._get_topmost_axes() return ax.get_subplotspec().get_topmost_subplotspec().get_gridspec() @@ -1601,18 +1681,18 @@ def shape(self): # TODO: Add commands that plot the input data for every # axes in the grid along a third dimension. for _src, _name in ( - (paxes.Axes, 'panel'), - (paxes.Axes, 'panel_axes'), - (paxes.Axes, 'inset'), - (paxes.Axes, 'inset_axes'), - (paxes.CartesianAxes, 'altx'), - (paxes.CartesianAxes, 'alty'), - (paxes.CartesianAxes, 'dualx'), - (paxes.CartesianAxes, 'dualy'), - (paxes.CartesianAxes, 'twinx'), - (paxes.CartesianAxes, 'twiny'), + (paxes.Axes, "panel"), + (paxes.Axes, "panel_axes"), + (paxes.Axes, "inset"), + (paxes.Axes, "inset_axes"), + (paxes.CartesianAxes, "altx"), + (paxes.CartesianAxes, "alty"), + (paxes.CartesianAxes, "dualx"), + (paxes.CartesianAxes, "dualy"), + (paxes.CartesianAxes, "twinx"), + (paxes.CartesianAxes, "twiny"), ): SubplotGrid._add_command(_src, _name) # Deprecated -SubplotsContainer = warnings._rename_objs('0.8.0', SubplotsContainer=SubplotGrid) +SubplotsContainer = warnings._rename_objs("0.8.0", SubplotsContainer=SubplotGrid) diff --git a/proplot/internals/__init__.py b/proplot/internals/__init__.py index 9be36e0f2..3653679af 100644 --- a/proplot/internals/__init__.py +++ b/proplot/internals/__init__.py @@ -14,6 +14,8 @@ except ImportError: # graceful fallback if IceCream isn't installed ic = lambda *args: print(*args) # noqa: E731 +from . import warnings as warns + def _not_none(*args, default=None, **kwargs): """ @@ -22,7 +24,7 @@ def _not_none(*args, default=None, **kwargs): """ first = default if args and kwargs: - raise ValueError('_not_none can only be used with args or kwargs.') + raise ValueError("_not_none can only be used with args or kwargs.") elif args: for arg in args: if arg is not None: @@ -36,8 +38,8 @@ def _not_none(*args, default=None, **kwargs): kwargs = {name: arg for name, arg in kwargs.items() if arg is not None} if len(kwargs) > 1: warnings._warn_proplot( - f'Got conflicting or duplicate keyword arguments: {kwargs}. ' - 'Using the first keyword argument.' + f"Got conflicting or duplicate keyword arguments: {kwargs}. " + "Using the first keyword argument." ) return first @@ -54,7 +56,7 @@ def _not_none(*args, default=None, **kwargs): labels, rcsetup, versions, - warnings + warnings, ) from .versions import _version_mpl, _version_cartopy # noqa: F401 from .warnings import ProplotWarning # noqa: F401 @@ -64,70 +66,103 @@ def _not_none(*args, default=None, **kwargs): # NOTE: We add aliases 'edgewidth' and 'fillcolor' for patch edges and faces # NOTE: Alias cannot appear as key or else _translate_kwargs will overwrite with None! _alias_maps = { - 'rgba': { - 'red': ('r',), - 'green': ('g',), - 'blue': ('b',), - 'alpha': ('a',), + "rgba": { + "red": ("r",), + "green": ("g",), + "blue": ("b",), + "alpha": ("a",), }, - 'hsla': { - 'hue': ('h',), - 'saturation': ('s', 'c', 'chroma'), - 'luminance': ('l',), - 'alpha': ('a',), + "hsla": { + "hue": ("h",), + "saturation": ("s", "c", "chroma"), + "luminance": ("l",), + "alpha": ("a",), }, - 'patch': { - 'alpha': ('a', 'alphas', 'fa', 'facealpha', 'facealphas', 'fillalpha', 'fillalphas'), # noqa: E501 - 'color': ('c', 'colors'), - 'edgecolor': ('ec', 'edgecolors'), - 'facecolor': ('fc', 'facecolors', 'fillcolor', 'fillcolors'), - 'hatch': ('h', 'hatching'), - 'linestyle': ('ls', 'linestyles'), - 'linewidth': ('lw', 'linewidths', 'ew', 'edgewidth', 'edgewidths'), - 'zorder': ('z', 'zorders'), + "patch": { + "alpha": ( + "a", + "alphas", + "fa", + "facealpha", + "facealphas", + "fillalpha", + "fillalphas", + ), # noqa: E501 + "color": ("c", "colors"), + "edgecolor": ("ec", "edgecolors"), + "facecolor": ("fc", "facecolors", "fillcolor", "fillcolors"), + "hatch": ("h", "hatching"), + "linestyle": ("ls", "linestyles"), + "linewidth": ("lw", "linewidths", "ew", "edgewidth", "edgewidths"), + "zorder": ("z", "zorders"), }, - 'line': { # copied from lines.py but expanded to include plurals - 'alpha': ('a', 'alphas'), - 'color': ('c', 'colors'), - 'dashes': ('d', 'dash'), - 'drawstyle': ('ds', 'drawstyles'), - 'fillstyle': ('fs', 'fillstyles', 'mfs', 'markerfillstyle', 'markerfillstyles'), - 'linestyle': ('ls', 'linestyles'), - 'linewidth': ('lw', 'linewidths'), - 'marker': ('m', 'markers'), - 'markersize': ('s', 'ms', 'markersizes'), # WARNING: no 'sizes' here for barb - 'markeredgewidth': ('ew', 'edgewidth', 'edgewidths', 'mew', 'markeredgewidths'), - 'markeredgecolor': ('ec', 'edgecolor', 'edgecolors', 'mec', 'markeredgecolors'), - 'markerfacecolor': ( - 'fc', 'facecolor', 'facecolors', 'fillcolor', 'fillcolors', - 'mc', 'markercolor', 'markercolors', 'mfc', 'markerfacecolors' + "line": { # copied from lines.py but expanded to include plurals + "alpha": ("a", "alphas"), + "color": ("c", "colors"), + "dashes": ("d", "dash"), + "drawstyle": ("ds", "drawstyles"), + "fillstyle": ("fs", "fillstyles", "mfs", "markerfillstyle", "markerfillstyles"), + "linestyle": ("ls", "linestyles"), + "linewidth": ("lw", "linewidths"), + "marker": ("m", "markers"), + "markersize": ("s", "ms", "markersizes"), # WARNING: no 'sizes' here for barb + "markeredgewidth": ("ew", "edgewidth", "edgewidths", "mew", "markeredgewidths"), + "markeredgecolor": ("ec", "edgecolor", "edgecolors", "mec", "markeredgecolors"), + "markerfacecolor": ( + "fc", + "facecolor", + "facecolors", + "fillcolor", + "fillcolors", + "mc", + "markercolor", + "markercolors", + "mfc", + "markerfacecolors", ), - 'zorder': ('z', 'zorders'), + "zorder": ("z", "zorders"), }, - 'collection': { # WARNING: face color ignored for line collections - 'alpha': ('a', 'alphas'), # WARNING: collections and contours use singular! - 'colors': ('c', 'color'), - 'edgecolors': ('ec', 'edgecolor', 'mec', 'markeredgecolor', 'markeredgecolors'), - 'facecolors': ( - 'fc', 'facecolor', 'fillcolor', 'fillcolors', - 'mc', 'markercolor', 'markercolors', 'mfc', 'markerfacecolor', 'markerfacecolors' # noqa: E501 + "collection": { # WARNING: face color ignored for line collections + "alpha": ("a", "alphas"), # WARNING: collections and contours use singular! + "colors": ("c", "color"), + "edgecolors": ("ec", "edgecolor", "mec", "markeredgecolor", "markeredgecolors"), + "facecolors": ( + "fc", + "facecolor", + "fillcolor", + "fillcolors", + "mc", + "markercolor", + "markercolors", + "mfc", + "markerfacecolor", + "markerfacecolors", # noqa: E501 ), - 'linestyles': ('ls', 'linestyle'), - 'linewidths': ('lw', 'linewidth', 'ew', 'edgewidth', 'edgewidths', 'mew', 'markeredgewidth', 'markeredgewidths'), # noqa: E501 - 'marker': ('m', 'markers'), - 'sizes': ('s', 'ms', 'markersize', 'markersizes'), - 'zorder': ('z', 'zorders'), + "linestyles": ("ls", "linestyle"), + "linewidths": ( + "lw", + "linewidth", + "ew", + "edgewidth", + "edgewidths", + "mew", + "markeredgewidth", + "markeredgewidths", + ), # noqa: E501 + "marker": ("m", "markers"), + "sizes": ("s", "ms", "markersize", "markersizes"), + "zorder": ("z", "zorders"), }, - 'text': { - 'color': ('c', 'fontcolor'), # NOTE: see text.py source code - 'fontfamily': ('family', 'name', 'fontname'), - 'fontsize': ('size',), - 'fontstretch': ('stretch',), - 'fontstyle': ('style',), - 'fontvariant': ('variant',), - 'fontweight': ('weight',), - 'fontproperties': ('fp', 'font', 'font_properties'), - 'zorder': ('z', 'zorders'), + "text": { + "color": ("c", "fontcolor"), # NOTE: see text.py source code + "fontfamily": ("family", "name", "fontname"), + "fontsize": ("size",), + "fontstretch": ("stretch",), + "fontstyle": ("style",), + "fontvariant": ("variant",), + "fontweight": ("weight",), + "fontproperties": ("fp", "font", "font_properties"), + "zorder": ("z", "zorders"), }, } @@ -135,10 +170,10 @@ def _not_none(*args, default=None, **kwargs): # Unit docstrings # NOTE: Try to fit this into a single line. Cannot break up with newline as that will # mess up docstring indentation since this is placed in indented param lines. -_units_docstring = 'If float, units are {units}. If string, interpreted by `~proplot.utils.units`.' # noqa: E501 -docstring._snippet_manager['units.pt'] = _units_docstring.format(units='points') -docstring._snippet_manager['units.in'] = _units_docstring.format(units='inches') -docstring._snippet_manager['units.em'] = _units_docstring.format(units='em-widths') +_units_docstring = "If float, units are {units}. If string, interpreted by `~proplot.utils.units`." # noqa: E501 +docstring._snippet_manager["units.pt"] = _units_docstring.format(units="points") +docstring._snippet_manager["units.in"] = _units_docstring.format(units="inches") +docstring._snippet_manager["units.em"] = _units_docstring.format(units="em-widths") # Style docstrings @@ -220,12 +255,14 @@ def _not_none(*args, default=None, **kwargs): ========================== ===== """ -docstring._snippet_manager['artist.line'] = _line_docstring -docstring._snippet_manager['artist.text'] = _text_docstring -docstring._snippet_manager['artist.patch'] = _patch_docstring.format(edgecolor='none') -docstring._snippet_manager['artist.patch_black'] = _patch_docstring.format(edgecolor='black') # noqa: E501 -docstring._snippet_manager['artist.collection_pcolor'] = _pcolor_collection_docstring -docstring._snippet_manager['artist.collection_contour'] = _contour_collection_docstring +docstring._snippet_manager["artist.line"] = _line_docstring +docstring._snippet_manager["artist.text"] = _text_docstring +docstring._snippet_manager["artist.patch"] = _patch_docstring.format(edgecolor="none") +docstring._snippet_manager["artist.patch_black"] = _patch_docstring.format( + edgecolor="black" +) # noqa: E501 +docstring._snippet_manager["artist.collection_pcolor"] = _pcolor_collection_docstring +docstring._snippet_manager["artist.collection_contour"] = _contour_collection_docstring def _get_aliases(category, *keys): @@ -246,7 +283,7 @@ def _kwargs_to_args(options, *args, allow_extra=False, **kwargs): """ nargs, nopts = len(args), len(options) if nargs > nopts and not allow_extra: - raise ValueError(f'Expected up to {nopts} positional arguments. Got {nargs}.') + raise ValueError(f"Expected up to {nopts} positional arguments. Got {nargs}.") args = list(args) # WARNING: Axes.text() expects return type of list args.extend(None for _ in range(nopts - nargs)) # fill missing args for idx, keys in enumerate(options): @@ -254,7 +291,7 @@ def _kwargs_to_args(options, *args, allow_extra=False, **kwargs): keys = (keys,) opts = {} if args[idx] is not None: # positional args have first priority - opts[keys[0] + '_positional'] = args[idx] + opts[keys[0] + "_positional"] = args[idx] for key in keys: # keyword args opts[key] = kwargs.pop(key, None) args[idx] = _not_none(**opts) # may reassign None @@ -281,13 +318,13 @@ def _pop_params(kwargs, *funcs, ignore_internal=False): Pop parameters of the input functions or methods. """ internal_params = { - 'default_cmap', - 'default_discrete', - 'inbounds', - 'plot_contours', - 'plot_lines', - 'skip_autolev', - 'to_centers', + "default_cmap", + "default_discrete", + "inbounds", + "plot_contours", + "plot_lines", + "skip_autolev", + "to_centers", } output = {} for func in funcs: @@ -298,7 +335,7 @@ def _pop_params(kwargs, *funcs, ignore_internal=False): elif func is None: continue else: - raise RuntimeError(f'Internal error. Invalid function {func!r}.') + raise RuntimeError(f"Internal error. Invalid function {func!r}.") for key in sig.parameters: value = kwargs.pop(key, None) if ignore_internal and key in internal_params: @@ -319,7 +356,7 @@ def _pop_props(input, *categories, prefix=None, ignore=None, skip=None): skip = (skip,) if isinstance(ignore, str): # e.g. 'marker' to ignore marker properties ignore = (ignore,) - prefix = prefix or '' # e.g. 'box' for boxlw, boxlinewidth, etc. + prefix = prefix or "" # e.g. 'box' for boxlw, boxlinewidth, etc. for category in categories: for key, aliases in _alias_maps[category].items(): if isinstance(aliases, str): @@ -333,15 +370,17 @@ def _pop_props(input, *categories, prefix=None, ignore=None, skip=None): if prop is None: continue if any(string in key for string in ignore): - warnings._warn_proplot(f'Ignoring property {key}={prop!r}.') + warnings._warn_proplot(f"Ignoring property {key}={prop!r}.") continue if isinstance(prop, str): # ad-hoc unit conversion - if key in ('fontsize',): + if key in ("fontsize",): from ..utils import _fontsize_to_pt + prop = _fontsize_to_pt(prop) - if key in ('linewidth', 'linewidths', 'markersize'): + if key in ("linewidth", "linewidths", "markersize"): from ..utils import units - prop = units(prop, 'pt') + + prop = units(prop, "pt") output[key] = prop return output @@ -354,25 +393,25 @@ def _pop_rc(src, *, ignore_conflicts=True): # NOTE: rc_mode == 2 applies only the updated params. A power user # could use ax.format(rc_mode=0) to re-apply all the current settings conflict_params = ( - 'alpha', - 'color', - 'facecolor', - 'edgecolor', - 'linewidth', - 'basemap', - 'backend', - 'share', - 'span', - 'tight', - 'span', + "alpha", + "color", + "facecolor", + "edgecolor", + "linewidth", + "basemap", + "backend", + "share", + "span", + "tight", + "span", ) - kw = src.pop('rc_kw', None) or {} - if 'mode' in src: - src['rc_mode'] = src.pop('mode') + kw = src.pop("rc_kw", None) or {} + if "mode" in src: + src["rc_mode"] = src.pop("mode") warnings._warn_proplot( "Keyword 'mode' was deprecated in v0.6. Please use 'rc_mode' instead." ) - mode = src.pop('rc_mode', None) + mode = src.pop("rc_mode", None) mode = _not_none(mode, 2) # only apply updated params by default for key, value in tuple(src.items()): name = rcsetup._rc_nodots.get(key, None) @@ -392,18 +431,18 @@ def _translate_loc(loc, mode, *, default=None, **kwargs): # Create specific options dictionary # NOTE: This is not inside validators.py because it is also used to # validate various user-input locations. - if mode == 'align': + if mode == "align": loc_dict = rcsetup.ALIGN_LOCS - elif mode == 'panel': + elif mode == "panel": loc_dict = rcsetup.PANEL_LOCS - elif mode == 'legend': + elif mode == "legend": loc_dict = rcsetup.LEGEND_LOCS - elif mode == 'colorbar': + elif mode == "colorbar": loc_dict = rcsetup.COLORBAR_LOCS - elif mode == 'text': + elif mode == "text": loc_dict = rcsetup.TEXT_LOCS else: - raise ValueError(f'Invalid mode {mode!r}.') + raise ValueError(f"Invalid mode {mode!r}.") loc_dict = loc_dict.copy() loc_dict.update(kwargs) @@ -415,24 +454,24 @@ def _translate_loc(loc, mode, *, default=None, **kwargs): loc = loc_dict[loc] except KeyError: raise KeyError( - f'Invalid {mode} location {loc!r}. Options are: ' - + ', '.join(map(repr, loc_dict)) - + '.' + f"Invalid {mode} location {loc!r}. Options are: " + + ", ".join(map(repr, loc_dict)) + + "." ) elif ( - mode == 'legend' + mode == "legend" and np.iterable(loc) and len(loc) == 2 and all(isinstance(l, Real) for l in loc) ): loc = tuple(loc) else: - raise KeyError(f'Invalid {mode} location {loc!r}.') + raise KeyError(f"Invalid {mode} location {loc!r}.") # Kludge / white lie # TODO: Implement 'best' colorbar location - if mode == 'colorbar' and loc == 'best': - loc = 'lower right' + if mode == "colorbar" and loc == "best": + loc = "lower right" return loc @@ -442,8 +481,8 @@ def _translate_grid(b, key): Translate an instruction to turn either major or minor gridlines on or off into a boolean and string applied to :rcraw:`axes.grid` and :rcraw:`axes.grid.which`. """ - ob = rc_matplotlib['axes.grid'] - owhich = rc_matplotlib['axes.grid.which'] + ob = rc_matplotlib["axes.grid"] + owhich = rc_matplotlib["axes.grid.which"] # Instruction is to turn off gridlines if not b: @@ -451,16 +490,18 @@ def _translate_grid(b, key): # ones that we want to turn off. Instruct to turn both off. if ( not ob - or key == 'grid' and owhich == 'major' - or key == 'gridminor' and owhich == 'minor' + or key == "grid" + and owhich == "major" + or key == "gridminor" + and owhich == "minor" ): - which = 'both' # disable both sides + which = "both" # disable both sides # Gridlines are currently on for major and minor ticks, so we # instruct to turn on gridlines for the one we *don't* want off - elif owhich == 'both': # and ob is True, as already tested + elif owhich == "both": # and ob is True, as already tested # if gridminor=False, enable major, and vice versa b = True - which = 'major' if key == 'gridminor' else 'minor' + which = "major" if key == "gridminor" else "minor" # Gridlines are on for the ones that we *didn't* instruct to # turn off, and off for the ones we do want to turn off. This # just re-asserts the ones that are already on. @@ -473,11 +514,13 @@ def _translate_grid(b, key): # Gridlines are already both on, or they are off only for the # ones that we want to turn on. Turn on gridlines for both. if ( - owhich == 'both' - or key == 'grid' and owhich == 'minor' - or key == 'gridminor' and owhich == 'major' + owhich == "both" + or key == "grid" + and owhich == "minor" + or key == "gridminor" + and owhich == "major" ): - which = 'both' + which = "both" # Gridlines are off for both, or off for the ones that we # don't want to turn on. We can just turn on these ones. else: diff --git a/proplot/internals/benchmarks.py b/proplot/internals/benchmarks.py index 086b8313c..8836436cf 100644 --- a/proplot/internals/benchmarks.py +++ b/proplot/internals/benchmarks.py @@ -13,6 +13,7 @@ class _benchmark(object): """ Context object for timing arbitrary blocks of code. """ + def __init__(self, message): self.message = message @@ -22,4 +23,4 @@ def __enter__(self): def __exit__(self, *args): # noqa: U100 if BENCHMARK: - print(f'{self.message}: {time.perf_counter() - self.time}s') + print(f"{self.message}: {time.perf_counter() - self.time}s") diff --git a/proplot/internals/context.py b/proplot/internals/context.py index 494254d4b..f429e6898 100644 --- a/proplot/internals/context.py +++ b/proplot/internals/context.py @@ -9,6 +9,7 @@ class _empty_context(object): """ A dummy context manager. """ + def __init__(self): pass @@ -23,6 +24,7 @@ class _state_context(object): """ Temporarily modify attribute(s) for an arbitrary object. """ + def __init__(self, obj, **kwargs): self._obj = obj self._attrs_new = kwargs diff --git a/proplot/internals/docstring.py b/proplot/internals/docstring.py index d1589d42e..6a8645696 100644 --- a/proplot/internals/docstring.py +++ b/proplot/internals/docstring.py @@ -68,13 +68,13 @@ def _concatenate_inherited(func, prepend_summary=False): # NOTE: Do not bother inheriting from cartopy GeoAxes. Cartopy completely # truncates the matplotlib docstrings (which is kind of not great). qual = func.__qualname__ - if 'Axes' in qual: + if "Axes" in qual: cls = maxes.Axes - elif 'Figure' in qual: + elif "Figure" in qual: cls = mfigure.Figure else: - raise ValueError(f'Unexpected method {qual!r}. Must be Axes or Figure method.') - doc = inspect.getdoc(func) or '' # also dedents + raise ValueError(f"Unexpected method {qual!r}. Must be Axes or Figure method.") + doc = inspect.getdoc(func) or "" # also dedents func_orig = getattr(cls, func.__name__, None) doc_orig = inspect.getdoc(func_orig) if not doc_orig: # should never happen @@ -82,10 +82,10 @@ def _concatenate_inherited(func, prepend_summary=False): # Optionally prepend the function summary # Concatenate docstrings only if this is not generated for website - regex = re.search(r'\.( | *\n|\Z)', doc_orig) + regex = re.search(r"\.( | *\n|\Z)", doc_orig) if regex and prepend_summary: - doc = doc_orig[:regex.start() + 1] + '\n\n' + doc - if not rc_matplotlib['docstring.hardcopy']: + doc = doc_orig[: regex.start() + 1] + "\n\n" + doc + if not rc_matplotlib["docstring.hardcopy"]: doc = f""" ===================== Proplot documentation @@ -111,6 +111,7 @@ class _SnippetManager(dict): """ A simple database for handling documentation snippets. """ + def __call__(self, obj): """ Add snippets to the string or object using ``%(name)s`` substitution. Here @@ -130,7 +131,7 @@ def __setitem__(self, key, value): should take care to import modules in the correct order. """ value = self(value) - value = value.strip('\n') + value = value.strip("\n") super().__setitem__(key, value) diff --git a/proplot/internals/fonts.py b/proplot/internals/fonts.py index f9659a0b9..c0beb4202 100644 --- a/proplot/internals/fonts.py +++ b/proplot/internals/fonts.py @@ -31,6 +31,7 @@ class _UnicodeFonts(UnicodeFonts): `~matplotlib._mathtext.TrueTypeFont` are taken by replacing ``'regular'`` in the "math" fontset with the active font name. """ + def __init__(self, *args, **kwargs): # Initialize font # NOTE: Could also capture the 'default_font_prop' passed as positional @@ -38,37 +39,37 @@ def __init__(self, *args, **kwargs): # private and it is easier to do graceful fallback with _fonts dictionary. ctx = {} # rc context regular = {} # styles - for texfont in ('cal', 'rm', 'tt', 'it', 'bf', 'sf'): - key = 'mathtext.' + texfont + for texfont in ("cal", "rm", "tt", "it", "bf", "sf"): + key = "mathtext." + texfont prop = mpl.rcParams[key] - if prop.startswith('regular'): - ctx[key] = prop.replace('regular', 'sans', 1) + if prop.startswith("regular"): + ctx[key] = prop.replace("regular", "sans", 1) regular[texfont] = prop with mpl.rc_context(ctx): super().__init__(*args, **kwargs) # Apply current font replacements global WARN_MATHPARSER if ( - hasattr(self, 'fontmap') - and hasattr(self, '_fonts') - and 'regular' in self._fonts + hasattr(self, "fontmap") + and hasattr(self, "_fonts") + and "regular" in self._fonts ): - font = self._fonts['regular'] # an ft2font.FT2Font instance + font = self._fonts["regular"] # an ft2font.FT2Font instance font = ttfFontProperty(font) for texfont, prop in regular.items(): - prop = prop.replace('regular', font.name) + prop = prop.replace("regular", font.name) self.fontmap[texfont] = findfont(prop, fallback_to_default=False) elif WARN_MATHPARSER: # Suppress duplicate warnings in case API changes - warnings._warn_proplot('Failed to update the math text parser.') + warnings._warn_proplot("Failed to update the math text parser.") WARN_MATHPARSER = False # Replace the parser try: mapping = MathTextParser._font_type_mapping - if mapping['custom'] is UnicodeFonts: - mapping['custom'] = _UnicodeFonts + if mapping["custom"] is UnicodeFonts: + mapping["custom"] = _UnicodeFonts except (KeyError, AttributeError): - warnings._warn_proplot('Failed to update math text parser.') + warnings._warn_proplot("Failed to update math text parser.") WARN_MATHPARSER = False diff --git a/proplot/internals/guides.py b/proplot/internals/guides.py index e2b620853..8d62c4748 100644 --- a/proplot/internals/guides.py +++ b/proplot/internals/guides.py @@ -13,12 +13,18 @@ # Global constants REMOVE_AFTER_FLUSH = ( - 'pad', 'space', 'width', 'length', 'shrink', 'align', 'queue', + "pad", + "space", + "width", + "length", + "shrink", + "align", + "queue", ) GUIDE_ALIASES = ( - ('title', 'label'), - ('locator', 'ticks'), - ('format', 'formatter', 'ticklabels') + ("title", "label"), + ("locator", "ticks"), + ("format", "formatter", "ticklabels"), ) @@ -28,7 +34,7 @@ def _add_guide_kw(name, kwargs, **opts): """ # NOTE: Here we *do not* want to overwrite properties in dictionary. Indicates # e.g. default locator inferred from levels or default title inferred from metadata. - attr = f'{name}_kw' + attr = f"{name}_kw" if not opts: return if not kwargs.get(attr, None): @@ -44,7 +50,7 @@ def _cache_guide_kw(obj, name, kwargs): # NOTE: Here we overwrite the hidden dictionary if it already exists. # This is only called once in _update_guide() so its fine. try: - setattr(obj, f'_{name}_kw', kwargs) + setattr(obj, f"_{name}_kw", kwargs) except AttributeError: pass if isinstance(obj, (tuple, list, np.ndarray)): @@ -62,7 +68,7 @@ def _flush_guide_kw(obj, name, kwargs): # colorbar() because locator or formatter axis would get reset. Old solution was # to delete the _guide_kw but that destroyed default behavior. New solution is # to keep _guide_kw but have constructor functions return shallow copies. - guide_kw = getattr(obj, f'_{name}_kw', None) + guide_kw = getattr(obj, f"_{name}_kw", None) if guide_kw: _update_kw(kwargs, overwrite=False, **guide_kw) for key in REMOVE_AFTER_FLUSH: @@ -95,7 +101,7 @@ def _iter_children(*args): This is used to update legend handle properties. """ for arg in args: - if hasattr(arg, '_children'): + if hasattr(arg, "_children"): yield from _iter_children(*arg._children) elif arg is not None: yield arg @@ -121,7 +127,7 @@ def _update_ticks(self, manual_only=False): # NOTE: Matplotlib 3.5+ does not define _use_auto_colorbar_locator since # ticks are always automatically adjusted by its colorbar subclass. This # override is thus backwards and forwards compatible. - attr = '_use_auto_colorbar_locator' + attr = "_use_auto_colorbar_locator" if not hasattr(self, attr) or getattr(self, attr)(): if manual_only: pass @@ -129,16 +135,18 @@ def _update_ticks(self, manual_only=False): mcolorbar.Colorbar.update_ticks(self) # AutoMinorLocator auto updates else: mcolorbar.Colorbar.update_ticks(self) # update necessary - minorlocator = getattr(self, 'minorlocator', None) + minorlocator = getattr(self, "minorlocator", None) if minorlocator is None: pass - elif hasattr(self, '_ticker'): + elif hasattr(self, "_ticker"): ticks, *_ = self._ticker(self.minorlocator, mticker.NullFormatter()) - axis = self.ax.yaxis if self.orientation == 'vertical' else self.ax.xaxis + axis = self.ax.yaxis if self.orientation == "vertical" else self.ax.xaxis axis.set_ticks(ticks, minor=True) axis.set_ticklabels([], minor=True) else: - warnings._warn_proplot(f'Cannot use user-input colorbar minor locator {minorlocator!r} (older matplotlib version). Turning on minor ticks instead.') # noqa: E501 + warnings._warn_proplot( + f"Cannot use user-input colorbar minor locator {minorlocator!r} (older matplotlib version). Turning on minor ticks instead." + ) # noqa: E501 self.minorlocator = None self.minorticks_on() # at least turn them on @@ -147,6 +155,7 @@ class _InsetColorbar(martist.Artist): """ Legend-like class for managing inset colorbars. """ + # TODO: Write this! @@ -154,4 +163,5 @@ class _CenteredLegend(martist.Artist): """ Legend-like class for managing centered-row legends. """ + # TODO: Write this! diff --git a/proplot/internals/inputs.py b/proplot/internals/inputs.py index 2a7a2ec05..1e7dbfe43 100644 --- a/proplot/internals/inputs.py +++ b/proplot/internals/inputs.py @@ -19,16 +19,37 @@ # Constants BASEMAP_FUNCS = ( # default latlon=True - 'barbs', 'contour', 'contourf', 'hexbin', - 'imshow', 'pcolor', 'pcolormesh', 'plot', - 'quiver', 'scatter', 'streamplot', 'step', + "barbs", + "contour", + "contourf", + "hexbin", + "imshow", + "pcolor", + "pcolormesh", + "plot", + "quiver", + "scatter", + "streamplot", + "step", ) CARTOPY_FUNCS = ( # default transform=PlateCarree() - 'barbs', 'contour', 'contourf', - 'fill', 'fill_between', 'fill_betweenx', # NOTE: not sure if these work - 'imshow', 'pcolor', 'pcolormesh', 'plot', - 'quiver', 'scatter', 'streamplot', 'step', - 'tricontour', 'tricontourf', 'tripcolor', # NOTE: not sure why these work + "barbs", + "contour", + "contourf", + "fill", + "fill_between", + "fill_betweenx", # NOTE: not sure if these work + "imshow", + "pcolor", + "pcolormesh", + "plot", + "quiver", + "scatter", + "streamplot", + "step", + "tricontour", + "tricontourf", + "tripcolor", # NOTE: not sure why these work ) @@ -43,11 +64,11 @@ def _load_objects(): # are careful to check membership to np.ndarray before anything else. global ndarray, DataArray, DataFrame, Series, Index, Quantity ndarray = np.ndarray - DataArray = getattr(sys.modules.get('xarray', None), 'DataArray', ndarray) - DataFrame = getattr(sys.modules.get('pandas', None), 'DataFrame', ndarray) - Series = getattr(sys.modules.get('pandas', None), 'Series', ndarray) - Index = getattr(sys.modules.get('pandas', None), 'Index', ndarray) - Quantity = getattr(sys.modules.get('pint', None), 'Quantity', ndarray) + DataArray = getattr(sys.modules.get("xarray", None), "DataArray", ndarray) + DataFrame = getattr(sys.modules.get("pandas", None), "DataFrame", ndarray) + Series = getattr(sys.modules.get("pandas", None), "Series", ndarray) + Index = getattr(sys.modules.get("pandas", None), "Index", ndarray) + Quantity = getattr(sys.modules.get("pint", None), "Quantity", ndarray) _load_objects() @@ -99,11 +120,10 @@ def _to_duck_array(data, strip_units=False): """ _load_objects() if data is None: - raise ValueError('Invalid data None.') - if ( - not isinstance(data, (ndarray, DataArray, DataFrame, Series, Index, Quantity)) - or not np.iterable(data) - ): + raise ValueError("Invalid data None.") + if not isinstance( + data, (ndarray, DataArray, DataFrame, Series, Index, Quantity) + ) or not np.iterable(data): # WARNING: this strips e.g. scalar DataArray metadata data = _to_numpy_array(data) if strip_units: # used for z coordinates that cannot have units @@ -123,7 +143,7 @@ def _to_numpy_array(data, strip_units=False): """ _load_objects() if data is None: - raise ValueError('Invalid data None.') + raise ValueError("Invalid data None.") if isinstance(data, ndarray): pass elif isinstance(data, DataArray): @@ -148,7 +168,7 @@ def _to_masked_array(data, *, copy=False): data, units = data.magnitude, data.units else: data = _to_numpy_array(data) - if data.dtype == 'O': + if data.dtype == "O": data = ma.array(data, mask=False) else: data = ma.masked_invalid(data, copy=copy) @@ -167,6 +187,7 @@ def _to_edges(x, y, z): Enforce that coordinates are edges. Convert from centers if possible. """ from ..utils import edges, edges2d + xlen, ylen = x.shape[-1], y.shape[0] if z.ndim == 2 and z.shape[1] == xlen and z.shape[0] == ylen: # Get edges given centers @@ -181,9 +202,9 @@ def _to_edges(x, y, z): elif z.shape[-1] != xlen - 1 or z.shape[0] != ylen - 1: # Helpful error message raise ValueError( - f'Input shapes x {x.shape} and y {y.shape} must match ' - f'array centers {z.shape} or ' - f'array borders {tuple(i + 1 for i in z.shape)}.' + f"Input shapes x {x.shape} and y {y.shape} must match " + f"array centers {z.shape} or " + f"array borders {tuple(i + 1 for i in z.shape)}." ) return x, y @@ -206,9 +227,9 @@ def _to_centers(x, y, z): elif z.shape[-1] != xlen or z.shape[0] != ylen: # Helpful error message raise ValueError( - f'Input shapes x {x.shape} and y {y.shape} ' - f'must match z centers {z.shape} ' - f'or z borders {tuple(i+1 for i in z.shape)}.' + f"Input shapes x {x.shape} and y {y.shape} " + f"must match z centers {z.shape} " + f"or z borders {tuple(i+1 for i in z.shape)}." ) return x, y @@ -250,29 +271,31 @@ def _decorator(func): @functools.wraps(func) def _preprocess_or_redirect(self, *args, **kwargs): - if getattr(self, '_internal_call', None): + if getattr(self, "_internal_call", None): # Redirect internal matplotlib call to native function from ..axes import PlotAxes + func_native = getattr(super(PlotAxes, self), name) return func_native(*args, **kwargs) else: # Impose default coordinate system from ..constructor import Proj - if self._name == 'basemap' and name in BASEMAP_FUNCS: - if kwargs.get('latlon', None) is None: - kwargs['latlon'] = True - if self._name == 'cartopy' and name in CARTOPY_FUNCS: - if kwargs.get('transform', None) is None: - kwargs['transform'] = PlateCarree() + + if self._name == "basemap" and name in BASEMAP_FUNCS: + if kwargs.get("latlon", None) is None: + kwargs["latlon"] = True + if self._name == "cartopy" and name in CARTOPY_FUNCS: + if kwargs.get("transform", None) is None: + kwargs["transform"] = PlateCarree() else: - kwargs['transform'] = Proj(kwargs['transform']) + kwargs["transform"] = Proj(kwargs["transform"]) # Process data args # NOTE: Raises error if there are more args than keys args, kwargs = _kwargs_to_args( keys, *args, allow_extra=allow_extra, **kwargs ) - data = kwargs.pop('data', None) + data = kwargs.pop("data", None) if data is not None: args = _from_data(data, *args) for key in set(keywords) & set(kwargs): @@ -284,10 +307,22 @@ def _preprocess_or_redirect(self, *args, **kwargs): if ndarray is not DataArray and isinstance(arg, DataArray): arg = arg.data if ndarray is not Quantity and isinstance(arg, Quantity): - ureg = getattr(arg, '_REGISTRY', None) - if hasattr(ureg, 'setup_matplotlib'): + ureg = getattr(arg, "_REGISTRY", None) + if hasattr(ureg, "setup_matplotlib"): ureg.setup_matplotlib(True) + # Sanitize colors + # Lookup can be a 2-tuple with colormap, and integer denoting the color from that map + # it also sanitizes if the second color is a float. Note that for float, the color + # will be picked proportional to idx/255 + from .. import colors as pcolor + + color = kwargs.pop("color", None) + if isinstance(color, tuple) and len(color) == 2: + cmap, color = color + color = pcolor._cmap_database.get_cmap(cmap)(color) + if color is not None: + kwargs["color"] = color # Call main function return func(self, *args, **kwargs) # call unbound method @@ -299,18 +334,37 @@ def _preprocess_or_redirect(self, *args, **kwargs): # Stats utiltiies def _dist_clean(distribution): """ - Clean the distrubtion data for processing by `boxplot` or `violinplot`. - Without this invalid values break the algorithm. + Clean the distribution data for processing by `boxplot` or `violinplot`. + Handles np.ndarrays where the ndarray is a list of lists of variable sizes. """ - if distribution.ndim == 1: - distribution = distribution[:, None] - distribution, units = _to_masked_array(distribution) # no copy needed - distribution = tuple( - distribution[..., i].compressed() for i in range(distribution.shape[-1]) - ) - if units is not None: - distribution = tuple(dist * units for dist in distribution) - return distribution + if isinstance(distribution, np.ndarray): + if distribution.dtype == object: + # Handle list of lists with variable sizes + return tuple( + np.array(sublist, dtype=float) + for sublist in distribution + if len(sublist) > 0 + ) + else: + # Handle regular numpy arrays + if distribution.ndim == 1: + distribution = distribution[:, None] + distribution, units = _to_masked_array(distribution) # no copy needed + distribution = tuple( + distribution[..., i].compressed() for i in range(distribution.shape[-1]) + ) + if units is not None: + distribution = tuple(dist * units for dist in distribution) + return distribution + elif isinstance(distribution, list): + # Handle list of lists directly + return tuple( + np.array(sublist, dtype=float) + for sublist in distribution + if len(sublist) > 0 + ) + else: + raise ValueError("Input must be a numpy array or a list of lists") def _dist_reduce(data, *, mean=None, means=None, median=None, medians=None, **kwargs): @@ -323,7 +377,7 @@ def _dist_reduce(data, *, mean=None, means=None, median=None, medians=None, **kw medians = _not_none(median=median, medians=medians) if means and medians: warnings._warn_proplot( - 'Cannot have both means=True and medians=True. Using former.' + "Cannot have both means=True and medians=True. Using former." ) medians = None if means or medians: @@ -331,7 +385,7 @@ def _dist_reduce(data, *, mean=None, means=None, median=None, medians=None, **kw distribution = distribution.filled() if distribution.ndim != 2: raise ValueError( - f'Expected 2D array for means=True. Got {distribution.ndim}D.' + f"Expected 2D array for means=True. Got {distribution.ndim}D." ) if units is not None: distribution = distribution * units @@ -339,15 +393,23 @@ def _dist_reduce(data, *, mean=None, means=None, median=None, medians=None, **kw data = np.nanmean(distribution, axis=0) else: data = np.nanmedian(distribution, axis=0) - kwargs['distribution'] = distribution + kwargs["distribution"] = distribution # Save argument passed to _error_bars return (data, kwargs) def _dist_range( - data, distribution, *, errdata=None, absolute=False, label=False, - stds=None, pctiles=None, stds_default=None, pctiles_default=None, + data, + distribution, + *, + errdata=None, + absolute=False, + label=False, + stds=None, + pctiles=None, + stds_default=None, + pctiles_default=None, ): """ Return a plottable characteristic range for the statistical distribution @@ -364,7 +426,7 @@ def _dist_range( if stds.size == 1: stds = sorted((-stds.item(), stds.item())) elif stds.size != 2: - raise ValueError('Expected scalar or length-2 stdev specification.') + raise ValueError("Expected scalar or length-2 stdev specification.") # Parse pctiles arguments if pctiles is True: @@ -377,27 +439,27 @@ def _dist_range( delta = (100 - pctiles.item()) / 2.0 pctiles = sorted((delta, 100 - delta)) elif pctiles.size != 2: - raise ValueError('Expected scalar or length-2 pctiles specification.') + raise ValueError("Expected scalar or length-2 pctiles specification.") # Incompatible settings if distribution is None and any(_ is not None for _ in (stds, pctiles)): raise ValueError( - 'To automatically compute standard deviations or percentiles on ' - 'columns of data you must pass means=True or medians=True.' + "To automatically compute standard deviations or percentiles on " + "columns of data you must pass means=True or medians=True." ) if stds is not None and pctiles is not None: warnings._warn_proplot( - 'Got both a standard deviation range and a percentile range for ' - 'auto error indicators. Using the standard deviation range.' + "Got both a standard deviation range and a percentile range for " + "auto error indicators. Using the standard deviation range." ) pctiles = None if distribution is not None and errdata is not None: stds = pctiles = None warnings._warn_proplot( - 'You explicitly provided the error bounds but also requested ' - 'automatically calculating means or medians on data columns. ' + "You explicitly provided the error bounds but also requested " + "automatically calculating means or medians on data columns. " 'It may make more sense to use the "stds" or "pctiles" keyword args ' - 'and have *proplot* calculate the error bounds.' + "and have *proplot* calculate the error bounds." ) # Compute error data in format that can be passed to maxes.Axes.errorbar() @@ -408,12 +470,13 @@ def _dist_range( raise ValueError( "Passing both 2D data coordinates and 'errdata' is not yet supported." ) - label_default = 'uncertainty' + label_default = "uncertainty" err = _to_numpy_array(errdata) if ( err.ndim not in (1, 2) or err.shape[-1] != data.size - or err.ndim == 2 and err.shape[0] != 2 + or err.ndim == 2 + and err.shape[0] != 2 ): raise ValueError( f"Input 'errdata' has shape {err.shape}. Expected (2, {data.size})." @@ -426,18 +489,18 @@ def _dist_range( elif stds is not None: # Standard deviations # NOTE: Invalid values were handled by _dist_reduce - label_default = fr'{abs(stds[1])}$\sigma$ range' + label_default = rf"{abs(stds[1])}$\sigma$ range" stds = _to_numpy_array(stds)[:, None] err = data + stds * np.nanstd(distribution, axis=0) elif pctiles is not None: # Percentiles # NOTE: Invalid values were handled by _dist_reduce - label_default = f'{pctiles[1] - pctiles[0]}% range' + label_default = f"{pctiles[1] - pctiles[0]}% range" err = np.nanpercentile(distribution, pctiles, axis=0) else: warnings._warn_proplot( - 'Error indications are missing from the dataset reduced by a ' - 'mean or median operation. Consider passing e.g. bars=True.' + "Error indications are missing from the dataset reduced by a " + "mean or median operation. Consider passing e.g. bars=True." ) err = None @@ -473,7 +536,7 @@ def _safe_mask(mask, *args): data = data.filled() if data.size > 1 and data.shape != invalid.shape: raise ValueError( - f'Mask shape {mask.shape} incompatible with array shape {data.shape}.' + f"Mask shape {mask.shape} incompatible with array shape {data.shape}." ) if data.size == 1 or invalid.size == 1: # NOTE: happens with _restrict_inbounds pass @@ -499,7 +562,7 @@ def _safe_range(data, lo=0, hi=100): min_ = max_ = None if data.size: min_ = np.min(data) if lo <= 0 else np.percentile(data, lo) - if hasattr(min_, 'dtype') and np.issubdtype(min_.dtype, np.integer): + if hasattr(min_, "dtype") and np.issubdtype(min_.dtype, np.integer): min_ = np.float64(min_) try: is_finite = np.isfinite(min_) @@ -511,7 +574,7 @@ def _safe_range(data, lo=0, hi=100): min_ *= units if data.size: max_ = np.max(data) if hi >= 100 else np.percentile(data, hi) - if hasattr(max_, 'dtype') and np.issubdtype(max_.dtype, np.integer): + if hasattr(max_, "dtype") and np.issubdtype(max_.dtype, np.integer): max_ = np.float64(max_) try: is_finite = np.isfinite(min_) @@ -525,7 +588,7 @@ def _safe_range(data, lo=0, hi=100): # Metadata utilities -def _meta_coords(*args, which='x', **kwargs): +def _meta_coords(*args, which="x", **kwargs): """ Return the index arrays associated with string coordinates and keyword arguments updated with index locators and formatters. @@ -535,6 +598,7 @@ def _meta_coords(*args, which='x', **kwargs): # NOTE: Why IndexFormatter and not FixedFormatter? The former ensures labels # correspond to indices while the latter can mysteriously truncate labels. from ..constructor import Formatter, Locator + res = [] for data in args: data = _to_duck_array(data) @@ -542,12 +606,12 @@ def _meta_coords(*args, which='x', **kwargs): res.append(data) continue if data.ndim > 1: - raise ValueError('Non-1D string coordinate input is unsupported.') + raise ValueError("Non-1D string coordinate input is unsupported.") ticks = np.arange(len(data)) labels = list(map(str, data)) - kwargs.setdefault(which + 'locator', Locator(ticks)) - kwargs.setdefault(which + 'formatter', Formatter(labels, index=True)) - kwargs.setdefault(which + 'minorlocator', Locator('null')) + kwargs.setdefault(which + "locator", Locator(ticks)) + kwargs.setdefault(which + "formatter", Formatter(labels, index=True)) + kwargs.setdefault(which + "minorlocator", Locator("null")) res.append(ticks) # use these as data coordinates return (*res, kwargs) @@ -564,7 +628,7 @@ def _meta_labels(data, axis=0, always=True): _load_objects() labels = None if axis not in (0, 1, 2): - raise ValueError(f'Invalid axis {axis}.') + raise ValueError(f"Invalid axis {axis}.") if isinstance(data, (ndarray, Quantity)): if not always: pass @@ -594,7 +658,7 @@ def _meta_labels(data, axis=0, always=True): # Everything else # NOTE: Ensure data is at least 1D in _to_duck_array so this covers everything else: - raise ValueError(f'Unrecognized array type {type(data)}.') + raise ValueError(f"Unrecognized array type {type(data)}.") return labels @@ -610,22 +674,22 @@ def _meta_title(data, include_units=True): # Xarray object with possible long_name, standard_name, and units attributes. # Output depends on if units is True. Prefer long_name (come last in loop). elif isinstance(data, DataArray): - title = getattr(data, 'name', None) - for key in ('standard_name', 'long_name'): + title = getattr(data, "name", None) + for key in ("standard_name", "long_name"): title = data.attrs.get(key, title) if include_units: units = _meta_units(data) # Pandas object. DataFrame has no native name attribute but user can add one # See: https://github.com/pandas-dev/pandas/issues/447 elif isinstance(data, (DataFrame, Series, Index)): - title = getattr(data, 'name', None) or None + title = getattr(data, "name", None) or None # Pint Quantity elif isinstance(data, Quantity): if include_units: units = _meta_units(data) # Add units or return units alone if title and units: - title = f'{title} ({units})' + title = f"{title} ({units})" else: title = title or units if title is not None: @@ -641,23 +705,24 @@ def _meta_units(data): _load_objects() # Get units from the attributes if ndarray is not DataArray and isinstance(data, DataArray): - units = data.attrs.get('units', None) + units = data.attrs.get("units", None) data = data.data if units is not None: return units # Get units from the quantity if ndarray is not Quantity and isinstance(data, Quantity): from ..config import rc + fmt = rc.unitformat try: units = format(data.units, fmt) except (TypeError, ValueError): warnings._warn_proplot( - f'Failed to format pint quantity with format string {fmt!r}.' + f"Failed to format pint quantity with format string {fmt!r}." ) else: - if 'L' in fmt: # auto-apply LaTeX math indicator - units = '$' + units + '$' + if "L" in fmt: # auto-apply LaTeX math indicator + units = "$" + units + "$" return units @@ -747,7 +812,7 @@ def _geo_inbounds(x, y, xmin=-180, xmax=180): mask = (x[1:] < xmin) | (x[:-1] > xmax) y[..., mask] = nan elif x.size == y.shape[-1]: # test the centers and pad by one for safety - where, = np.where((x < xmin) | (x > xmax)) + (where,) = np.where((x < xmin) | (x > xmax)) y[..., where[1:-1]] = nan return x, y @@ -758,7 +823,7 @@ def _geo_globe(x, y, z, xmin=-180, modulo=False): longitude seams. Increases the size of the arrays. """ # Cover gaps over poles by appending polar data - with np.errstate(all='ignore'): + with np.errstate(all="ignore"): p1 = np.mean(z[0, :]) # do not ignore NaN if present p2 = np.mean(z[-1, :]) ps = (-90, 90) if (y[0] < y[-1]) else (90, -90) @@ -781,7 +846,9 @@ def _geo_globe(x, y, z, xmin=-180, modulo=False): xi = np.array([x[-1], x[0] + 360]) # input coordinates xq = xmin + 360 # query coordinate zq = ma.concatenate((z[:, -1:], z[:, :1]), axis=1) - zq = (zq[:, :1] * (xi[1] - xq) + zq[:, 1:] * (xq - xi[0])) / (xi[1] - xi[0]) # noqa: E501 + zq = (zq[:, :1] * (xi[1] - xq) + zq[:, 1:] * (xq - xi[0])) / ( + xi[1] - xi[0] + ) # noqa: E501 x = ma.concatenate(((xmin,), x, (xmin + 360,))) z = ma.concatenate((zq, z, zq), axis=1) # Extend coordinate edges to seam. Size possibly augmented by 1. @@ -791,5 +858,5 @@ def _geo_globe(x, y, z, xmin=-180, modulo=False): x[-1] = xmin + 360 z = ma.concatenate((z[:, -1:], z), axis=1) else: - raise ValueError('Unexpected shapes of coordinates or data arrays.') + raise ValueError("Unexpected shapes of coordinates or data arrays.") return x, y, z diff --git a/proplot/internals/labels.py b/proplot/internals/labels.py index cbe4dbe66..1fb6255c0 100644 --- a/proplot/internals/labels.py +++ b/proplot/internals/labels.py @@ -19,7 +19,7 @@ def _transfer_label(src, dest): if not text.strip(): # WARNING: must test strip() (see _align_axis_labels()) return dest.set_text(text) - src.set_text('') + src.set_text("") def _update_label(text, props=None, **kwargs): @@ -32,19 +32,19 @@ def _update_label(text, props=None, **kwargs): props.update(kwargs) # Update border - border = props.pop('border', None) - bordercolor = props.pop('bordercolor', 'w') - borderinvert = props.pop('borderinvert', False) - borderwidth = props.pop('borderwidth', 2) - borderstyle = props.pop('borderstyle', 'miter') + border = props.pop("border", None) + bordercolor = props.pop("bordercolor", "w") + borderinvert = props.pop("borderinvert", False) + borderwidth = props.pop("borderwidth", 2) + borderstyle = props.pop("borderstyle", "miter") if border: facecolor, bgcolor = text.get_color(), bordercolor if borderinvert: facecolor, bgcolor = bgcolor, facecolor kw = { - 'linewidth': borderwidth, - 'foreground': bgcolor, - 'joinstyle': borderstyle, + "linewidth": borderwidth, + "foreground": bgcolor, + "joinstyle": borderstyle, } text.set_color(facecolor) text.set_path_effects( @@ -59,24 +59,24 @@ def _update_label(text, props=None, **kwargs): # NOTE: For some reason using pad / 10 results in perfect alignment for # med-large labels. Tried scaling to be font size relative but never works. pad = text.axes._title_pad / 10 # default pad - bbox = props.pop('bbox', None) - bboxcolor = props.pop('bboxcolor', 'w') - bboxstyle = props.pop('bboxstyle', 'round') - bboxalpha = props.pop('bboxalpha', 0.5) - bboxpad = props.pop('bboxpad', None) + bbox = props.pop("bbox", None) + bboxcolor = props.pop("bboxcolor", "w") + bboxstyle = props.pop("bboxstyle", "round") + bboxalpha = props.pop("bboxalpha", 0.5) + bboxpad = props.pop("bboxpad", None) bboxpad = pad if bboxpad is None else bboxpad if bbox is None: pass elif isinstance(bbox, dict): # *native* matplotlib usage - props['bbox'] = bbox + props["bbox"] = bbox elif not bbox: - props['bbox'] = None # disable the bbox + props["bbox"] = None # disable the bbox else: - props['bbox'] = { - 'edgecolor': 'black', - 'facecolor': bboxcolor, - 'boxstyle': bboxstyle, - 'alpha': bboxalpha, - 'pad': bboxpad, + props["bbox"] = { + "edgecolor": "black", + "facecolor": bboxcolor, + "boxstyle": bboxstyle, + "alpha": bboxalpha, + "pad": bboxpad, } return mtext.Text.update(text, props) diff --git a/proplot/internals/rcsetup.py b/proplot/internals/rcsetup.py index 105f219f3..51e2a469a 100644 --- a/proplot/internals/rcsetup.py +++ b/proplot/internals/rcsetup.py @@ -3,7 +3,7 @@ Utilities for global configuration. """ import functools -import re +import re, matplotlib as mpl from collections.abc import MutableMapping from numbers import Integral, Real @@ -14,7 +14,11 @@ from matplotlib import rcParamsDefault as _rc_matplotlib_native from matplotlib.colors import Colormap from matplotlib.font_manager import font_scalings -from matplotlib.fontconfig_pattern import parse_fontconfig_pattern + +if hasattr(mpl, "_fontconfig_pattern"): + from matplotlib._fontconfig_pattern import parse_fontconfig_pattern +else: + from matplotlib.fontconfig_pattern import parse_fontconfig_pattern from . import ic # noqa: F401 from . import warnings @@ -22,7 +26,7 @@ # Regex for "probable" unregistered named colors. Try to retain warning message for # colors that were most likely a failed literal string evaluation during startup. -REGEX_NAMED_COLOR = re.compile(r'\A[a-zA-Z0-9:_ -]*\Z') +REGEX_NAMED_COLOR = re.compile(r"\A[a-zA-Z0-9:_ -]*\Z") # Configurable validation settings # NOTE: These are set to True inside __init__.py @@ -41,127 +45,154 @@ # to sync them when proplot is imported... but also sync them here so that we can # simply compare any Configurator state to these dictionaries and use save() to # save only the settings changed by the user. -BLACK = 'black' -CYCLE = 'colorblind' -CMAPCYC = 'twilight' -CMAPDIV = 'BuRd' -CMAPSEQ = 'Fire' -CMAPCAT = 'colorblind10' -DIVERGING = 'div' +BLACK = "black" +CYCLE = "colorblind" +CMAPCYC = "twilight" +CMAPDIV = "BuRd" +CMAPSEQ = "Fire" +CMAPCAT = "colorblind10" +DIVERGING = "div" FRAMEALPHA = 0.8 # legend and colorbar -FONTNAME = 'sans-serif' +FONTNAME = "sans-serif" FONTSIZE = 9.0 GRIDALPHA = 0.1 -GRIDBELOW = 'line' +GRIDBELOW = "line" GRIDPAD = 3.0 GRIDRATIO = 0.5 # differentiated from major by half size reduction -GRIDSTYLE = '-' +GRIDSTYLE = "-" LABELPAD = 4.0 # default is 4.0, previously was 3.0 -LARGESIZE = 'med-large' +LARGESIZE = "med-large" LINEWIDTH = 0.6 MARGIN = 0.05 MATHTEXT = False -SMALLSIZE = 'medium' -TICKDIR = 'out' +SMALLSIZE = "medium" +TICKDIR = "out" TICKLEN = 4.0 TICKLENRATIO = 0.5 # very noticeable length reduction TICKMINOR = True TICKPAD = 2.0 TICKWIDTHRATIO = 0.8 # very slight width reduction TITLEPAD = 5.0 # default is 6.0, previously was 3.0 -WHITE = 'white' +WHITE = "white" ZLINES = 2 # default zorder for lines ZPATCHES = 1 # Preset legend locations and aliases LEGEND_LOCS = { - 'fill': 'fill', - 'inset': 'best', - 'i': 'best', - 0: 'best', - 1: 'upper right', - 2: 'upper left', - 3: 'lower left', - 4: 'lower right', - 5: 'center left', - 6: 'center right', - 7: 'lower center', - 8: 'upper center', - 9: 'center', - 'l': 'left', - 'r': 'right', - 'b': 'bottom', - 't': 'top', - 'c': 'center', - 'ur': 'upper right', - 'ul': 'upper left', - 'll': 'lower left', - 'lr': 'lower right', - 'cr': 'center right', - 'cl': 'center left', - 'uc': 'upper center', - 'lc': 'lower center', + "fill": "fill", + "inset": "best", + "i": "best", + 0: "best", + 1: "upper right", + 2: "upper left", + 3: "lower left", + 4: "lower right", + 5: "center left", + 6: "center right", + 7: "lower center", + 8: "upper center", + 9: "center", + "l": "left", + "r": "right", + "b": "bottom", + "t": "top", + "c": "center", + "ur": "upper right", + "ul": "upper left", + "ll": "lower left", + "lr": "lower right", + "cr": "center right", + "cl": "center left", + "uc": "upper center", + "lc": "lower center", } for _loc in tuple(LEGEND_LOCS.values()): if _loc not in LEGEND_LOCS: LEGEND_LOCS[_loc] = _loc # identity assignments TEXT_LOCS = { - key: val for key, val in LEGEND_LOCS.items() if val in ( - 'left', 'center', 'right', - 'upper left', 'upper center', 'upper right', - 'lower left', 'lower center', 'lower right', + key: val + for key, val in LEGEND_LOCS.items() + if val + in ( + "left", + "center", + "right", + "upper left", + "upper center", + "upper right", + "lower left", + "lower center", + "lower right", ) } COLORBAR_LOCS = { - key: val for key, val in LEGEND_LOCS.items() if val in ( - 'fill', 'best', - 'left', 'right', 'top', 'bottom', - 'upper left', 'upper right', 'lower left', 'lower right', + key: val + for key, val in LEGEND_LOCS.items() + if val + in ( + "fill", + "best", + "left", + "right", + "top", + "bottom", + "upper left", + "upper right", + "lower left", + "lower right", ) } PANEL_LOCS = { - key: val for key, val in LEGEND_LOCS.items() if val in ( - 'left', 'right', 'top', 'bottom' - ) + key: val + for key, val in LEGEND_LOCS.items() + if val in ("left", "right", "top", "bottom") } ALIGN_LOCS = { - key: val for key, val in LEGEND_LOCS.items() if isinstance(key, str) and val in ( - 'left', 'right', 'top', 'bottom', 'center', + key: val + for key, val in LEGEND_LOCS.items() + if isinstance(key, str) + and val + in ( + "left", + "right", + "top", + "bottom", + "center", ) } # Matplotlib setting categories EM_KEYS = ( # em-width units - 'legend.borderpad', - 'legend.labelspacing', - 'legend.handlelength', - 'legend.handleheight', - 'legend.handletextpad', - 'legend.borderaxespad', - 'legend.columnspacing', + "legend.borderpad", + "legend.labelspacing", + "legend.handlelength", + "legend.handleheight", + "legend.handletextpad", + "legend.borderaxespad", + "legend.columnspacing", ) PT_KEYS = ( - 'font.size', # special case - 'xtick.major.size', - 'xtick.minor.size', - 'ytick.major.size', - 'ytick.minor.size', - 'xtick.major.pad', - 'xtick.minor.pad', - 'ytick.major.pad', - 'ytick.minor.pad', - 'xtick.major.width', - 'xtick.minor.width', - 'ytick.major.width', - 'ytick.minor.width', - 'axes.labelpad', - 'axes.titlepad', - 'axes.linewidth', - 'grid.linewidth', - 'patch.linewidth', - 'hatch.linewidth', - 'lines.linewidth', - 'contour.linewidth', + "font.size", # special case + "xtick.major.size", + "xtick.minor.size", + "ytick.major.size", + "ytick.minor.size", + "xtick.major.pad", + "xtick.minor.pad", + "ytick.major.pad", + "ytick.minor.pad", + "xtick.major.width", + "xtick.minor.width", + "ytick.major.width", + "ytick.minor.width", + "axes.labelpad", + "axes.titlepad", + "axes.linewidth", + "grid.linewidth", + "patch.linewidth", + "hatch.linewidth", + "lines.linewidth", + "contour.linewidth", ) FONT_KEYS = set() # dynamically add to this below @@ -180,7 +211,7 @@ def _get_default_param(key): value = dict_.get(key, sentinel) if value is not sentinel: return value - raise KeyError(f'Invalid key {key!r}.') + raise KeyError(f"Invalid key {key!r}.") def _validate_abc(value): @@ -195,7 +226,7 @@ def _validate_abc(value): except ValueError: pass if isinstance(value, str): - if 'a' in value.lower(): + if "a" in value.lower(): return value else: if all(isinstance(_, str) for _ in value): @@ -209,6 +240,7 @@ def _validate_belongs(*options): """ Return a validator ensuring the item belongs in the list. """ + def _validate_belongs(value): # noqa: E306 for opt in options: if isinstance(value, str) and isinstance(opt, str): @@ -220,10 +252,11 @@ def _validate_belongs(value): # noqa: E306 elif value == opt: return opt raise ValueError( - f'Invalid value {value!r}. Options are: ' - + ', '.join(map(repr, options)) - + '.' + f"Invalid value {value!r}. Options are: " + + ", ".join(map(repr, options)) + + "." ) + return _validate_belongs @@ -232,20 +265,24 @@ def _validate_cmap(subtype): Validate the colormap or cycle. Possibly skip name registration check and assign the colormap name rather than a colormap instance. """ + def _validate_cmap(value): name = value if isinstance(value, str): if VALIDATE_REGISTERED_CMAPS: from ..colors import _get_cmap_subtype + _get_cmap_subtype(name, subtype) # may trigger useful error message return name elif isinstance(value, Colormap): - name = getattr(value, 'name', None) + name = getattr(value, "name", None) if isinstance(name, str): from ..colors import _cmap_database # avoid circular imports + _cmap_database[name] = value return name - raise ValueError(f'Invalid colormap or color cycle name {name!r}.') + raise ValueError(f"Invalid colormap or color cycle name {name!r}.") + return _validate_cmap @@ -263,7 +300,7 @@ def _validate_color(value, alternative=None): or not isinstance(value, str) or not REGEX_NAMED_COLOR.match(value) ): - raise ValueError(f'{value!r} is not a valid color arg.') from None + raise ValueError(f"{value!r} is not a valid color arg.") from None return value except Exception as error: raise error @@ -273,12 +310,12 @@ def _validate_fontprops(s): """ Parse font property with support for ``'regular'`` placeholder. """ - b = s.startswith('regular') + b = s.startswith("regular") if b: - s = s.replace('regular', 'sans', 1) + s = s.replace("regular", "sans", 1) parse_fontconfig_pattern(s) if b: - s = s.replace('sans', 'regular', 1) + s = s.replace("sans", "regular", 1) return s @@ -297,8 +334,8 @@ def _validate_fontsize(value): except ValueError: pass raise ValueError( - f'Invalid font size {value!r}. Can be points or one of the ' - 'preset scalings: ' + ', '.join(map(repr, font_scalings)) + '.' + f"Invalid font size {value!r}. Can be points or one of the " + "preset scalings: " + ", ".join(map(repr, font_scalings)) + "." ) @@ -308,24 +345,24 @@ def _validate_labels(labels, lon=True): """ if labels is None: return [None] * 4 - which = 'lon' if lon else 'lat' + which = "lon" if lon else "lat" if isinstance(labels, str): labels = (labels,) array = np.atleast_1d(labels).tolist() if all(isinstance(_, str) for _ in array): bool_ = [False] * 4 - opts = ('left', 'right', 'bottom', 'top') + opts = ("left", "right", "bottom", "top") for string in array: if string in opts: string = string[0] - elif set(string) - set('lrbt'): + elif set(string) - set("lrbt"): raise ValueError( - f'Invalid {which}label string {string!r}. Must be one of ' - + ', '.join(map(repr, opts)) + f"Invalid {which}label string {string!r}. Must be one of " + + ", ".join(map(repr, opts)) + " or a string of single-letter characters like 'lr'." ) for char in string: - bool_['lrbt'.index(char)] = True + bool_["lrbt".index(char)] = True array = bool_ if len(array) == 1: array.append(False) # default is to label bottom or left @@ -335,7 +372,7 @@ def _validate_labels(labels, lon=True): else: array = [*array, False, False] if len(array) != 4 or any(isinstance(_, str) for _ in array): - raise ValueError(f'Invalid {which}label spec: {labels}.') + raise ValueError(f"Invalid {which}label spec: {labels}.") return array @@ -343,14 +380,16 @@ def _validate_or_none(validator): """ Allow none otherwise pass to the input validator. """ + @functools.wraps(validator) def _validate_or_none(value): if value is None: return - if isinstance(value, str) and value.lower() == 'none': + if isinstance(value, str) and value.lower() == "none": return return validator(value) - _validate_or_none.__name__ = validator.__name__ + '_or_none' + + _validate_or_none.__name__ = validator.__name__ + "_or_none" return _validate_or_none @@ -358,7 +397,7 @@ def _validate_rotation(value): """ Valid rotation arguments. """ - if isinstance(value, str) and value.lower() in ('horizontal', 'vertical'): + if isinstance(value, str) and value.lower() in ("horizontal", "vertical"): return value return _validate_float(value) @@ -367,11 +406,14 @@ def _validate_units(dest): """ Validate the input using the units function. """ + def _validate_units(value): if isinstance(value, str): from ..utils import units # avoid circular imports + value = units(value, dest) # validation happens here return _validate_float(value) + return _validate_units @@ -382,19 +424,19 @@ def _rst_table(): # Initial stuff colspace = 2 # spaces between each column descrips = tuple(descrip for (_, _, descrip) in _rc_proplot_table.values()) - keylen = len(max((*_rc_proplot_table, 'Key'), key=len)) + 4 # literal backticks - vallen = len(max((*descrips, 'Description'), key=len)) - divider = '=' * keylen + ' ' * colspace + '=' * vallen + '\n' - header = 'Key' + ' ' * (keylen - 3 + colspace) + 'Description\n' + keylen = len(max((*_rc_proplot_table, "Key"), key=len)) + 4 # literal backticks + vallen = len(max((*descrips, "Description"), key=len)) + divider = "=" * keylen + " " * colspace + "=" * vallen + "\n" + header = "Key" + " " * (keylen - 3 + colspace) + "Description\n" # Build table string = divider + header + divider for key, (_, _, descrip) in _rc_proplot_table.items(): - spaces = ' ' * (keylen - (len(key) + 4) + colspace) - string += f'``{key}``{spaces}{descrip}\n' + spaces = " " * (keylen - (len(key) + 4) + colspace) + string += f"``{key}``{spaces}{descrip}\n" string = string + divider - return '.. rst-class:: proplot-rctable\n\n' + string.strip() + return ".. rst-class:: proplot-rctable\n\n" + string.strip() def _to_string(value): @@ -405,14 +447,14 @@ def _to_string(value): # and proplotrc this will be read as comment character. if value is None or isinstance(value, (str, bool, Integral)): value = str(value) - if value[:1] == '#': # i.e. a HEX string + if value[:1] == "#": # i.e. a HEX string value = value[1:] elif isinstance(value, Real): value = str(round(value, 6)) # truncate decimals elif isinstance(value, Cycler): value = repr(value) # special case! elif isinstance(value, (list, tuple, np.ndarray)): - value = ', '.join(map(_to_string, value)) # sexy recursion + value = ", ".join(map(_to_string, value)) # sexy recursion else: value = None return value @@ -422,19 +464,19 @@ def _yaml_table(rcdict, comment=True, description=False): """ Return the settings as a nicely tabulated YAML-style table. """ - prefix = '# ' if comment else '' + prefix = "# " if comment else "" data = [] for key, args in rcdict.items(): # Optionally append description includes_descrip = isinstance(args, tuple) and len(args) == 3 if not description: - descrip = '' + descrip = "" value = args[0] if includes_descrip else args elif includes_descrip: value, validator, descrip = args - descrip = '# ' + descrip # skip the validator + descrip = "# " + descrip # skip the validator else: - raise ValueError(f'Unexpected input {key}={args!r}.') + raise ValueError(f"Unexpected input {key}={args!r}.") # Translate object to string value = _to_string(value) @@ -442,18 +484,18 @@ def _yaml_table(rcdict, comment=True, description=False): data.append((key, value, descrip)) else: warnings._warn_proplot( - f'Failed to write rc setting {key} = {value!r}. Must be None, bool, ' - 'string, int, float, a list or tuple thereof, or a property cycler.' + f"Failed to write rc setting {key} = {value!r}. Must be None, bool, " + "string, int, float, a list or tuple thereof, or a property cycler." ) # Generate string - string = '' + string = "" keylen = len(max(rcdict, key=len)) vallen = len(max((tup[1] for tup in data), key=len)) for key, value, descrip in data: - space1 = ' ' * (keylen - len(key) + 1) - space2 = ' ' * (vallen - len(value) + 2) if descrip else '' - string += f'{prefix}{key}:{space1}{value}{space2}{descrip}\n' + space1 = " " * (keylen - len(key) + 1) + space2 = " " * (vallen - len(value) + 2) if descrip else "" + string += f"{prefix}{key}:{space1}{value}{space2}{descrip}\n" return string.strip() @@ -462,6 +504,7 @@ class _RcParams(MutableMapping, dict): """ A simple dictionary with locked inputs and validated assignments. """ + # NOTE: By omitting __delitem__ in MutableMapping we effectively # disable mutability. Also disables deleting items with pop(). def __init__(self, source, validate): @@ -490,11 +533,11 @@ def __getitem__(self, key): def __setitem__(self, key, value): key, value = self._check_key(key, value) if key not in self._validate: - raise KeyError(f'Invalid rc key {key!r}.') + raise KeyError(f"Invalid rc key {key!r}.") try: value = self._validate[key](value) except (ValueError, TypeError) as error: - raise ValueError(f'Key {key}: {error}') from None + raise ValueError(f"Key {key}: {error}") from None if key is not None: dict.__setitem__(self, key, value) @@ -507,19 +550,19 @@ def _check_key(key, value=None): if key in _rc_renamed: key_new, version = _rc_renamed[key] warnings._warn_proplot( - f'The rc setting {key!r} was deprecated in version {version} and may be ' # noqa: E501 - f'removed in {warnings._next_release()}. Please use {key_new!r} instead.' # noqa: E501 + f"The rc setting {key!r} was deprecated in version {version} and may be " # noqa: E501 + f"removed in {warnings.next_release()}. Please use {key_new!r} instead." # noqa: E501 ) - if key == 'basemap': # special case - value = ('cartopy', 'basemap')[int(bool(value))] - if key == 'cartopy.autoextent': - value = ('globe', 'auto')[int(bool(value))] + if key == "basemap": # special case + value = ("cartopy", "basemap")[int(bool(value))] + if key == "cartopy.autoextent": + value = ("globe", "auto")[int(bool(value))] key = key_new if key in _rc_removed: info, version = _rc_removed[key] raise KeyError( - f'The rc setting {key!r} was removed in version {version}.' - + (info and ' ' + info) + f"The rc setting {key!r} was removed in version {version}." + + (info and " " + info) ) return key, value @@ -533,55 +576,70 @@ def copy(self): # until version 3.1.2. So use that as backup here. # WARNING: We create custom 'or none' validators since their # availability seems less consistent across matplotlib versions. -_validate_pt = _validate_units('pt') -_validate_em = _validate_units('em') -_validate_in = _validate_units('in') +_validate_pt = _validate_units("pt") +_validate_em = _validate_units("em") +_validate_in = _validate_units("in") _validate_bool = msetup.validate_bool _validate_int = msetup.validate_int _validate_float = msetup.validate_float _validate_string = msetup.validate_string _validate_fontname = msetup.validate_stringlist # same as 'font.family' -_validate_fontweight = getattr(msetup, 'validate_fontweight', _validate_string) +_validate_fontweight = getattr(msetup, "validate_fontweight", _validate_string) # Special style validators # See: https://matplotlib.org/stable/api/_as_gen/matplotlib.patches.FancyBboxPatch.html _validate_boxstyle = _validate_belongs( - 'square', 'circle', 'round', 'round4', 'sawtooth', 'roundtooth', + "square", + "circle", + "round", + "round4", + "sawtooth", + "roundtooth", ) -if hasattr(msetup, '_validate_linestyle'): # fancy validation including dashes +if hasattr(msetup, "_validate_linestyle"): # fancy validation including dashes _validate_linestyle = msetup._validate_linestyle else: # no dashes allowed then but no big deal _validate_linestyle = _validate_belongs( - '-', ':', '--', '-.', 'solid', 'dashed', 'dashdot', 'dotted', 'none', ' ', '', + "-", + ":", + "--", + "-.", + "solid", + "dashed", + "dashdot", + "dotted", + "none", + " ", + "", ) # Patch existing matplotlib validators. # NOTE: validate_fontsizelist is unused in recent matplotlib versions and # validate_colorlist is only used with prop cycle eval (which we don't care about) -font_scalings['med'] = 1.0 # consistent shorthand -font_scalings['med-small'] = 0.9 # add scaling -font_scalings['med-large'] = 1.1 # add scaling -if not hasattr(RcParams, 'validate'): # not mission critical so skip - warnings._warn_proplot('Failed to update matplotlib rcParams validators.') +font_scalings["med"] = 1.0 # consistent shorthand +font_scalings["med-small"] = 0.9 # add scaling +font_scalings["med-large"] = 1.1 # add scaling +if not hasattr(RcParams, "validate"): # not mission critical so skip + warnings._warn_proplot("Failed to update matplotlib rcParams validators.") else: _validate = RcParams.validate - _validate['image.cmap'] = _validate_cmap('continuous') - _validate['legend.loc'] = _validate_belongs(*LEGEND_LOCS) + _validate["image.cmap"] = _validate_cmap("continuous") + _validate["legend.loc"] = _validate_belongs(*LEGEND_LOCS) for _key, _validator in _validate.items(): - if _validator is getattr(msetup, 'validate_fontsize', None): # should exist + if _validator is getattr(msetup, "validate_fontsize", None): # should exist FONT_KEYS.add(_key) _validate[_key] = _validate_fontsize - if _validator is getattr(msetup, 'validate_fontsize_None', None): + if _validator is getattr(msetup, "validate_fontsize_None", None): FONT_KEYS.add(_key) _validate[_key] = _validate_or_none(_validate_fontsize) - if _validator is getattr(msetup, 'validate_font_properties', None): + if _validator is getattr(msetup, "validate_font_properties", None): _validate[_key] = _validate_fontprops - if _validator is getattr(msetup, 'validate_color', None): # should exist + if _validator is getattr(msetup, "validate_color", None): # should exist _validate[_key] = _validate_color - if _validator is getattr(msetup, 'validate_color_or_auto', None): - _validate[_key] = functools.partial(_validate_color, alternative='auto') - if _validator is getattr(msetup, 'validate_color_or_inherit', None): - _validate[_key] = functools.partial(_validate_color, alternative='inherit') + if _validator is getattr(msetup, "validate_color_or_auto", None): + _validate[_key] = functools.partial(_validate_color, alternative="auto") + if _validator is getattr(msetup, "validate_color_or_inherit", None): + _validate[_key] = functools.partial(_validate_color, alternative="inherit") for _keys, _validator_replace in ((EM_KEYS, _validate_em), (PT_KEYS, _validate_pt)): for _key in _keys: _validator = _validate.get(_key, None) @@ -589,7 +647,7 @@ def copy(self): continue if _validator is msetup.validate_float: _validate[_key] = _validator_replace - if _validator is getattr(msetup, 'validate_float_or_None'): + if _validator is getattr(msetup, "validate_float_or_None"): _validate[_key] = _validate_or_none(_validator_replace) @@ -598,1386 +656,1183 @@ def copy(self): # "meta" setting so that _get_default_param returns the value imposed by *proplot* # and so that "changed" settings detected by Configurator.save are correct. _rc_matplotlib_default = { - 'axes.axisbelow': GRIDBELOW, - 'axes.formatter.use_mathtext': MATHTEXT, - 'axes.grid': True, # enable lightweight transparent grid by default - 'axes.grid.which': 'major', - 'axes.edgecolor': BLACK, - 'axes.labelcolor': BLACK, - 'axes.labelpad': LABELPAD, # more compact - 'axes.labelsize': SMALLSIZE, - 'axes.labelweight': 'normal', - 'axes.linewidth': LINEWIDTH, - 'axes.titlepad': TITLEPAD, # more compact - 'axes.titlesize': LARGESIZE, - 'axes.titleweight': 'normal', - 'axes.xmargin': MARGIN, - 'axes.ymargin': MARGIN, - 'errorbar.capsize': 3.0, - 'figure.autolayout': False, - 'figure.figsize': (4.0, 4.0), # for interactife backends - 'figure.dpi': 100, - 'figure.facecolor': '#f4f4f4', # similar to MATLAB interface - 'figure.titlesize': LARGESIZE, - 'figure.titleweight': 'bold', # differentiate from axes titles - 'font.serif': [ - 'TeX Gyre Schola', # Century lookalike - 'TeX Gyre Bonum', # Bookman lookalike - 'TeX Gyre Termes', # Times New Roman lookalike - 'TeX Gyre Pagella', # Palatino lookalike - 'DejaVu Serif', - 'Bitstream Vera Serif', - 'Computer Modern Roman', - 'Bookman', - 'Century Schoolbook L', - 'Charter', - 'ITC Bookman', - 'New Century Schoolbook', - 'Nimbus Roman No9 L', - 'Noto Serif', - 'Palatino', - 'Source Serif Pro', - 'Times New Roman', - 'Times', - 'Utopia', - 'serif', + "axes.axisbelow": GRIDBELOW, + "axes.formatter.use_mathtext": MATHTEXT, + "axes.grid": True, # enable lightweight transparent grid by default + "axes.grid.which": "major", + "axes.edgecolor": BLACK, + "axes.labelcolor": BLACK, + "axes.labelpad": LABELPAD, # more compact + "axes.labelsize": SMALLSIZE, + "axes.labelweight": "normal", + "axes.linewidth": LINEWIDTH, + "axes.titlepad": TITLEPAD, # more compact + "axes.titlesize": LARGESIZE, + "axes.titleweight": "normal", + "axes.xmargin": MARGIN, + "axes.ymargin": MARGIN, + "errorbar.capsize": 3.0, + "figure.autolayout": False, + "figure.figsize": (4.0, 4.0), # for interactife backends + "figure.dpi": 100, + "figure.facecolor": "#f4f4f4", # similar to MATLAB interface + "figure.titlesize": LARGESIZE, + "figure.titleweight": "bold", # differentiate from axes titles + "font.serif": [ + "TeX Gyre Schola", # Century lookalike + "TeX Gyre Bonum", # Bookman lookalike + "TeX Gyre Termes", # Times New Roman lookalike + "TeX Gyre Pagella", # Palatino lookalike + "DejaVu Serif", + "Bitstream Vera Serif", + "Computer Modern Roman", + "Bookman", + "Century Schoolbook L", + "Charter", + "ITC Bookman", + "New Century Schoolbook", + "Nimbus Roman No9 L", + "Noto Serif", + "Palatino", + "Source Serif Pro", + "Times New Roman", + "Times", + "Utopia", + "serif", ], - 'font.sans-serif': [ - 'TeX Gyre Heros', # Helvetica lookalike - 'DejaVu Sans', - 'Bitstream Vera Sans', - 'Computer Modern Sans Serif', - 'Arial', - 'Avenir', - 'Fira Math', - 'Fira Sans', - 'Frutiger', - 'Geneva', - 'Gill Sans', - 'Helvetica', - 'Lucid', - 'Lucida Grande', - 'Myriad Pro', - 'Noto Sans', - 'Roboto', - 'Source Sans Pro', - 'Tahoma', - 'Trebuchet MS', - 'Ubuntu', - 'Univers', - 'Verdana', - 'sans-serif', + "font.sans-serif": [ + "TeX Gyre Heros", # Helvetica lookalike + "DejaVu Sans", + "Bitstream Vera Sans", + "Computer Modern Sans Serif", + "Arial", + "Avenir", + "Fira Math", + "Fira Sans", + "Frutiger", + "Geneva", + "Gill Sans", + "Helvetica", + "Lucid", + "Lucida Grande", + "Myriad Pro", + "Noto Sans", + "Roboto", + "Source Sans Pro", + "Tahoma", + "Trebuchet MS", + "Ubuntu", + "Univers", + "Verdana", + "sans-serif", ], - 'font.cursive': [ - 'TeX Gyre Chorus', # Chancery lookalike - 'Apple Chancery', - 'Felipa', - 'Sand', - 'Script MT', - 'Textile', - 'Zapf Chancery', - 'cursive', + "font.cursive": [ + "TeX Gyre Chorus", # Chancery lookalike + "Apple Chancery", + "Felipa", + "Sand", + "Script MT", + "Textile", + "Zapf Chancery", + "cursive", ], - 'font.fantasy': [ - 'TeX Gyre Adventor', # Avant Garde lookalike - 'Avant Garde', - 'Charcoal', - 'Chicago', - 'Comic Sans MS', - 'Futura', - 'Humor Sans', - 'Impact', - 'Optima', - 'Western', - 'xkcd', - 'fantasy', + "font.fantasy": [ + "TeX Gyre Adventor", # Avant Garde lookalike + "Avant Garde", + "Charcoal", + "Chicago", + "Comic Sans MS", + "Futura", + "Humor Sans", + "Impact", + "Optima", + "Western", + "xkcd", + "fantasy", ], - 'font.monospace': [ - 'TeX Gyre Cursor', # Courier lookalike - 'DejaVu Sans Mono', - 'Bitstream Vera Sans Mono', - 'Computer Modern Typewriter', - 'Andale Mono', - 'Courier New', - 'Courier', - 'Fixed', - 'Nimbus Mono L', - 'Terminal', - 'monospace', + "font.monospace": [ + "TeX Gyre Cursor", # Courier lookalike + "DejaVu Sans Mono", + "Bitstream Vera Sans Mono", + "Computer Modern Typewriter", + "Andale Mono", + "Courier New", + "Courier", + "Fixed", + "Nimbus Mono L", + "Terminal", + "monospace", ], - 'font.family': FONTNAME, - 'font.size': FONTSIZE, - 'grid.alpha': GRIDALPHA, # lightweight unobtrusive gridlines - 'grid.color': BLACK, # lightweight unobtrusive gridlines - 'grid.linestyle': GRIDSTYLE, - 'grid.linewidth': LINEWIDTH, - 'hatch.color': BLACK, - 'hatch.linewidth': LINEWIDTH, - 'image.cmap': CMAPSEQ, - 'lines.linestyle': '-', - 'lines.linewidth': 1.5, - 'lines.markersize': 6.0, - 'legend.borderaxespad': 0, # i.e. flush against edge - 'legend.borderpad': 0.5, # a bit more roomy - 'legend.columnspacing': 1.5, # a bit more compact (see handletextpad) - 'legend.edgecolor': BLACK, - 'legend.facecolor': WHITE, - 'legend.fancybox': False, # i.e. BboxStyle 'square' not 'round' - 'legend.fontsize': SMALLSIZE, - 'legend.framealpha': FRAMEALPHA, - 'legend.handleheight': 1.0, # default is 0.7 - 'legend.handlelength': 2.0, # default is 2.0 - 'legend.handletextpad': 0.5, # a bit more compact (see columnspacing) - 'mathtext.default': 'it', - 'mathtext.fontset': 'custom', - 'mathtext.bf': 'regular:bold', # custom settings implemented above - 'mathtext.cal': 'cursive', - 'mathtext.it': 'regular:italic', - 'mathtext.rm': 'regular', - 'mathtext.sf': 'regular', - 'mathtext.tt': 'monospace', - 'patch.linewidth': LINEWIDTH, - 'savefig.bbox': None, # do not use 'tight' - 'savefig.directory': '', # use the working directory - 'savefig.dpi': 1000, # use academic journal recommendation - 'savefig.facecolor': WHITE, # use white instead of 'auto' - 'savefig.format': 'pdf', # use vector graphics - 'savefig.transparent': False, - 'xtick.color': BLACK, - 'xtick.direction': TICKDIR, - 'xtick.labelsize': SMALLSIZE, - 'xtick.major.pad': TICKPAD, - 'xtick.major.size': TICKLEN, - 'xtick.major.width': LINEWIDTH, - 'xtick.minor.pad': TICKPAD, - 'xtick.minor.size': TICKLEN * TICKLENRATIO, - 'xtick.minor.width': LINEWIDTH * TICKWIDTHRATIO, - 'xtick.minor.visible': TICKMINOR, - 'ytick.color': BLACK, - 'ytick.direction': TICKDIR, - 'ytick.labelsize': SMALLSIZE, - 'ytick.major.pad': TICKPAD, - 'ytick.major.size': TICKLEN, - 'ytick.major.width': LINEWIDTH, - 'ytick.minor.pad': TICKPAD, - 'ytick.minor.size': TICKLEN * TICKLENRATIO, - 'ytick.minor.width': LINEWIDTH * TICKWIDTHRATIO, - 'ytick.minor.visible': TICKMINOR, + "font.family": FONTNAME, + "font.size": FONTSIZE, + "grid.alpha": GRIDALPHA, # lightweight unobtrusive gridlines + "grid.color": BLACK, # lightweight unobtrusive gridlines + "grid.linestyle": GRIDSTYLE, + "grid.linewidth": LINEWIDTH, + "hatch.color": BLACK, + "hatch.linewidth": LINEWIDTH, + "image.cmap": CMAPSEQ, + "image.interpolation": "none", + "lines.linestyle": "-", + "lines.linewidth": 1.5, + "lines.markersize": 6.0, + "legend.borderaxespad": 0, # i.e. flush against edge + "legend.borderpad": 0.5, # a bit more roomy + "legend.columnspacing": 1.5, # a bit more compact (see handletextpad) + "legend.edgecolor": BLACK, + "legend.facecolor": WHITE, + "legend.fancybox": False, # i.e. BboxStyle 'square' not 'round' + "legend.fontsize": SMALLSIZE, + "legend.framealpha": FRAMEALPHA, + "legend.handleheight": 1.0, # default is 0.7 + "legend.handlelength": 2.0, # default is 2.0 + "legend.handletextpad": 0.5, # a bit more compact (see columnspacing) + "mathtext.default": "it", + "mathtext.fontset": "custom", + "mathtext.bf": "regular:bold", # custom settings implemented above + "mathtext.cal": "cursive", + "mathtext.it": "regular:italic", + "mathtext.rm": "regular", + "mathtext.sf": "regular", + "mathtext.tt": "monospace", + "patch.linewidth": LINEWIDTH, + "savefig.bbox": None, # do not use 'tight' + "savefig.directory": "", # use the working directory + "savefig.dpi": 1000, # use academic journal recommendation + "savefig.facecolor": WHITE, # use white instead of 'auto' + "savefig.format": "pdf", # use vector graphics + "savefig.transparent": False, + "xtick.color": BLACK, + "xtick.direction": TICKDIR, + "xtick.labelsize": SMALLSIZE, + "xtick.major.pad": TICKPAD, + "xtick.major.size": TICKLEN, + "xtick.major.width": LINEWIDTH, + "xtick.minor.pad": TICKPAD, + "xtick.minor.size": TICKLEN * TICKLENRATIO, + "xtick.minor.width": LINEWIDTH * TICKWIDTHRATIO, + "xtick.minor.visible": TICKMINOR, + "ytick.color": BLACK, + "ytick.direction": TICKDIR, + "ytick.labelsize": SMALLSIZE, + "ytick.major.pad": TICKPAD, + "ytick.major.size": TICKLEN, + "ytick.major.width": LINEWIDTH, + "ytick.minor.pad": TICKPAD, + "ytick.minor.size": TICKLEN * TICKLENRATIO, + "ytick.minor.width": LINEWIDTH * TICKWIDTHRATIO, + "ytick.minor.visible": TICKMINOR, } -if 'mathtext.fallback' in _rc_matplotlib_native: - _rc_matplotlib_default['mathtext.fallback'] = 'stixsans' +if "mathtext.fallback" in _rc_matplotlib_native: + _rc_matplotlib_default["mathtext.fallback"] = "stixsans" # Proplot pseudo-setting defaults, validators, and descriptions # NOTE: Cannot have different a-b-c and title paddings because they are both controlled # by matplotlib's _title_offset_trans transform and want to keep them aligned anyway. _addendum_rotation = " Must be 'vertical', 'horizontal', or a float indicating degrees." -_addendum_em = ' Interpreted by `~proplot.utils.units`. Numeric units are em-widths.' -_addendum_in = ' Interpreted by `~proplot.utils.units`. Numeric units are inches.' -_addendum_pt = ' Interpreted by `~proplot.utils.units`. Numeric units are points.' +_addendum_em = " Interpreted by `~proplot.utils.units`. Numeric units are em-widths." +_addendum_in = " Interpreted by `~proplot.utils.units`. Numeric units are inches." +_addendum_pt = " Interpreted by `~proplot.utils.units`. Numeric units are points." _addendum_font = ( - ' Must be a :ref:`relative font size ` or unit string ' - 'interpreted by `~proplot.utils.units`. Numeric units are points.' + " Must be a :ref:`relative font size ` or unit string " + "interpreted by `~proplot.utils.units`. Numeric units are points." ) _rc_proplot_table = { # Stylesheet - 'style': ( + "style": ( None, _validate_or_none(_validate_string), - 'The default matplotlib `stylesheet ' - '`__ ' # noqa: E501 - 'name. If ``None``, a custom proplot style is used. ' - "If ``'default'``, the default matplotlib style is used." + "The default matplotlib `stylesheet " + "`__ " # noqa: E501 + "name. If ``None``, a custom proplot style is used. " + "If ``'default'``, the default matplotlib style is used.", ), - # A-b-c labels - 'abc': ( + "abc": ( False, _validate_abc, - 'If ``False`` then a-b-c labels are disabled. If ``True`` the default label ' - 'style ``a`` is used. If string this indicates the style and must contain the ' - "character ``a`` or ``A``, for example ``'a.'`` or ``'(A)'``." + "If ``False`` then a-b-c labels are disabled. If ``True`` the default label " + "style ``a`` is used. If string this indicates the style and must contain the " + "character ``a`` or ``A``, for example ``'a.'`` or ``'(A)'``.", ), - 'abc.border': ( + "abc.border": ( True, _validate_bool, - 'Whether to draw a white border around a-b-c labels ' - 'when :rcraw:`abc.loc` is inside the axes.' + "Whether to draw a white border around a-b-c labels " + "when :rcraw:`abc.loc` is inside the axes.", ), - 'abc.borderwidth': ( + "abc.borderwidth": ( 1.5, _validate_pt, - 'Width of the white border around a-b-c labels.' + "Width of the white border around a-b-c labels.", ), - 'abc.bbox': ( + "abc.bbox": ( False, _validate_bool, - 'Whether to draw semi-transparent bounding boxes around a-b-c labels ' - 'when :rcraw:`abc.loc` is inside the axes.' - ), - 'abc.bboxcolor': ( - WHITE, - _validate_color, - 'a-b-c label bounding box color.' - ), - 'abc.bboxstyle': ( - 'square', - _validate_boxstyle, - 'a-b-c label bounding box style.' - ), - 'abc.bboxalpha': ( - 0.5, - _validate_float, - 'a-b-c label bounding box opacity.' + "Whether to draw semi-transparent bounding boxes around a-b-c labels " + "when :rcraw:`abc.loc` is inside the axes.", ), - 'abc.bboxpad': ( + "abc.bboxcolor": (WHITE, _validate_color, "a-b-c label bounding box color."), + "abc.bboxstyle": ("square", _validate_boxstyle, "a-b-c label bounding box style."), + "abc.bboxalpha": (0.5, _validate_float, "a-b-c label bounding box opacity."), + "abc.bboxpad": ( None, _validate_or_none(_validate_pt), - 'Padding for the a-b-c label bounding box. By default this is scaled ' - 'to make the box flush against the subplot edge.' + _addendum_pt + "Padding for the a-b-c label bounding box. By default this is scaled " + "to make the box flush against the subplot edge." + _addendum_pt, ), - 'abc.color': ( - BLACK, - _validate_color, - 'a-b-c label color.' - ), - 'abc.loc': ( - 'left', # left side above the axes + "abc.color": (BLACK, _validate_color, "a-b-c label color."), + "abc.loc": ( + "left", # left side above the axes _validate_belongs(*TEXT_LOCS), - 'a-b-c label position. ' - 'For options see the :ref:`location table `.' + "a-b-c label position. " + "For options see the :ref:`location table `.", ), - 'abc.size': ( + "abc.size": ( LARGESIZE, _validate_fontsize, - 'a-b-c label font size.' + _addendum_font + "a-b-c label font size." + _addendum_font, ), - 'abc.titlepad': ( + "abc.titlepad": ( LABELPAD, _validate_pt, - 'Padding separating the title and a-b-c label when in the same location.' - + _addendum_pt + "Padding separating the title and a-b-c label when in the same location." + + _addendum_pt, ), - 'abc.weight': ( - 'bold', - _validate_fontweight, - 'a-b-c label font weight.' - ), - + "abc.weight": ("bold", _validate_fontweight, "a-b-c label font weight."), # Autoformatting - 'autoformat': ( + "autoformat": ( True, _validate_bool, - 'Whether to automatically apply labels from `pandas.Series`, ' - '`pandas.DataFrame`, and `xarray.DataArray` objects passed to ' - 'plotting functions. See also :rcraw:`unitformat`.' + "Whether to automatically apply labels from `pandas.Series`, " + "`pandas.DataFrame`, and `xarray.DataArray` objects passed to " + "plotting functions. See also :rcraw:`unitformat`.", ), - # Axes additions - 'axes.alpha': ( + "axes.alpha": ( None, _validate_or_none(_validate_float), - 'Opacity of the background axes patch.' + "Opacity of the background axes patch.", ), - 'axes.inbounds': ( + "axes.inbounds": ( True, _validate_bool, - 'Whether to exclude out-of-bounds data when determining the default *y* (*x*) ' - 'axis limits and the *x* (*y*) axis limits have been locked.' + "Whether to exclude out-of-bounds data when determining the default *y* (*x*) " + "axis limits and the *x* (*y*) axis limits have been locked.", ), - 'axes.margin': ( + "axes.margin": ( MARGIN, _validate_float, - 'The fractional *x* and *y* axis margins when limits are unset.' + "The fractional *x* and *y* axis margins when limits are unset.", ), - # Country borders - 'borders': ( - False, - _validate_bool, - 'Toggles country border lines on and off.' - ), - 'borders.alpha': ( + "borders": (False, _validate_bool, "Toggles country border lines on and off."), + "borders.alpha": ( None, _validate_or_none(_validate_float), - 'Opacity for country border lines.', + "Opacity for country border lines.", ), - 'borders.color': ( - BLACK, - _validate_color, - 'Line color for country border lines.' - ), - 'borders.linewidth': ( + "borders.color": (BLACK, _validate_color, "Line color for country border lines."), + "borders.linewidth": ( LINEWIDTH, _validate_pt, - 'Line width for country border lines.' - ), - 'borders.zorder': ( - ZLINES, - _validate_float, - 'Z-order for country border lines.' + "Line width for country border lines.", ), - + "borders.zorder": (ZLINES, _validate_float, "Z-order for country border lines."), # Bottom subplot labels - 'bottomlabel.color': ( + "bottomlabel.color": ( BLACK, _validate_color, - 'Font color for column labels on the bottom of the figure.' + "Font color for column labels on the bottom of the figure.", ), - 'bottomlabel.pad': ( + "bottomlabel.pad": ( TITLEPAD, _validate_pt, - 'Padding between axes content and column labels on the bottom of the figure.' - + _addendum_pt + "Padding between axes content and column labels on the bottom of the figure." + + _addendum_pt, ), - 'bottomlabel.rotation': ( - 'horizontal', + "bottomlabel.rotation": ( + "horizontal", _validate_rotation, - 'Rotation for column labels at the bottom of the figure.' + _addendum_rotation + "Rotation for column labels at the bottom of the figure." + _addendum_rotation, ), - 'bottomlabel.size': ( + "bottomlabel.size": ( LARGESIZE, _validate_fontsize, - 'Font size for column labels on the bottom of the figure.' + _addendum_font + "Font size for column labels on the bottom of the figure." + _addendum_font, ), - 'bottomlabel.weight': ( - 'bold', + "bottomlabel.weight": ( + "bold", _validate_fontweight, - 'Font weight for column labels on the bottom of the figure.' + "Font weight for column labels on the bottom of the figure.", ), - # Coastlines - 'coast': ( - False, - _validate_bool, - 'Toggles coastline lines on and off.' - ), - 'coast.alpha': ( + "coast": (False, _validate_bool, "Toggles coastline lines on and off."), + "coast.alpha": ( None, _validate_or_none(_validate_float), - 'Opacity for coast lines', - ), - 'coast.color': ( - BLACK, - _validate_color, - 'Line color for coast lines.' - ), - 'coast.linewidth': ( - LINEWIDTH, - _validate_pt, - 'Line width for coast lines.' - ), - 'coast.zorder': ( - ZLINES, - _validate_float, - 'Z-order for coast lines.' + "Opacity for coast lines", ), - + "coast.color": (BLACK, _validate_color, "Line color for coast lines."), + "coast.linewidth": (LINEWIDTH, _validate_pt, "Line width for coast lines."), + "coast.zorder": (ZLINES, _validate_float, "Z-order for coast lines."), # Colorbars - 'colorbar.edgecolor': ( + "colorbar.edgecolor": ( BLACK, _validate_color, - 'Color for the inset colorbar frame edge.' + "Color for the inset colorbar frame edge.", ), - 'colorbar.extend': ( + "colorbar.extend": ( 1.3, _validate_em, 'Length of rectangular or triangular "extensions" for panel colorbars.' - + _addendum_em + + _addendum_em, ), - 'colorbar.fancybox': ( + "colorbar.fancybox": ( False, _validate_bool, - 'Whether to use a "fancy" round bounding box for inset colorbar frames.' + 'Whether to use a "fancy" round bounding box for inset colorbar frames.', ), - 'colorbar.framealpha': ( + "colorbar.framealpha": ( FRAMEALPHA, _validate_float, - 'Opacity for inset colorbar frames.' + "Opacity for inset colorbar frames.", ), - 'colorbar.facecolor': ( + "colorbar.facecolor": ( WHITE, _validate_color, - 'Color for the inset colorbar frame.' + "Color for the inset colorbar frame.", ), - 'colorbar.frameon': ( + "colorbar.frameon": ( True, _validate_bool, - 'Whether to draw a frame behind inset colorbars.' + "Whether to draw a frame behind inset colorbars.", ), - 'colorbar.grid': ( + "colorbar.grid": ( False, _validate_bool, - 'Whether to draw borders between each level of the colorbar.' + "Whether to draw borders between each level of the colorbar.", ), - 'colorbar.insetextend': ( + "colorbar.insetextend": ( 0.9, _validate_em, 'Length of rectangular or triangular "extensions" for inset colorbars.' - + _addendum_em + + _addendum_em, ), - 'colorbar.insetlength': ( + "colorbar.insetlength": ( 8, _validate_em, - 'Length of inset colorbars.' + _addendum_em + "Length of inset colorbars." + _addendum_em, ), - 'colorbar.insetpad': ( + "colorbar.insetpad": ( 0.7, _validate_em, - 'Padding between axes edge and inset colorbars.' + _addendum_em + "Padding between axes edge and inset colorbars." + _addendum_em, ), - 'colorbar.insetwidth': ( + "colorbar.insetwidth": ( 1.2, _validate_em, - 'Width of inset colorbars.' + _addendum_em - ), - 'colorbar.length': ( - 1, - _validate_em, - 'Length of outer colorbars.' + "Width of inset colorbars." + _addendum_em, ), - 'colorbar.loc': ( - 'right', + "colorbar.length": (1, _validate_em, "Length of outer colorbars."), + "colorbar.loc": ( + "right", _validate_belongs(*COLORBAR_LOCS), - 'Inset colorbar location. ' - 'For options see the :ref:`location table `.' + "Inset colorbar location. " + "For options see the :ref:`location table `.", ), - 'colorbar.width': ( - 0.2, - _validate_in, - 'Width of outer colorbars.' + _addendum_in - ), - 'colorbar.rasterized': ( + "colorbar.width": (0.2, _validate_in, "Width of outer colorbars." + _addendum_in), + "colorbar.rasterized": ( False, _validate_bool, - 'Whether to use rasterization for colorbar solids.' + "Whether to use rasterization for colorbar solids.", ), - 'colorbar.shadow': ( + "colorbar.shadow": ( False, _validate_bool, - 'Whether to add a shadow underneath inset colorbar frames.' + "Whether to add a shadow underneath inset colorbar frames.", ), - # Color cycle additions - 'cycle': ( + "cycle": ( CYCLE, - _validate_cmap('discrete'), - 'Name of the color cycle assigned to :rcraw:`axes.prop_cycle`.' + _validate_cmap("discrete"), + "Name of the color cycle assigned to :rcraw:`axes.prop_cycle`.", ), - # Colormap additions - 'cmap': ( + "cmap": ( CMAPSEQ, - _validate_cmap('continuous'), - 'Alias for :rcraw:`cmap.sequential` and :rcraw:`image.cmap`.' + _validate_cmap("continuous"), + "Alias for :rcraw:`cmap.sequential` and :rcraw:`image.cmap`.", ), - 'cmap.autodiverging': ( + "cmap.autodiverging": ( True, _validate_bool, - 'Whether to automatically apply a diverging colormap and ' - 'normalizer based on the data.' + "Whether to automatically apply a diverging colormap and " + "normalizer based on the data.", ), - 'cmap.qualitative': ( + "cmap.qualitative": ( CMAPCAT, - _validate_cmap('discrete'), - 'Default colormap for qualitative datasets.' + _validate_cmap("discrete"), + "Default colormap for qualitative datasets.", ), - 'cmap.cyclic': ( + "cmap.cyclic": ( CMAPCYC, - _validate_cmap('continuous'), - 'Default colormap for cyclic datasets.' + _validate_cmap("continuous"), + "Default colormap for cyclic datasets.", ), - 'cmap.discrete': ( + "cmap.discrete": ( None, _validate_or_none(_validate_bool), - 'If ``True``, `~proplot.colors.DiscreteNorm` is used for every colormap plot. ' - 'If ``False``, it is never used. If ``None``, it is used for all plot types ' - 'except `imshow`, `matshow`, `spy`, `hexbin`, and `hist2d`.' + "If ``True``, `~proplot.colors.DiscreteNorm` is used for every colormap plot. " + "If ``False``, it is never used. If ``None``, it is used for all plot types " + "except `imshow`, `matshow`, `spy`, `hexbin`, and `hist2d`.", ), - 'cmap.diverging': ( + "cmap.diverging": ( CMAPDIV, - _validate_cmap('continuous'), - 'Default colormap for diverging datasets.' + _validate_cmap("continuous"), + "Default colormap for diverging datasets.", ), - 'cmap.inbounds': ( + "cmap.inbounds": ( True, _validate_bool, - 'If ``True`` and the *x* and *y* axis limits are fixed, only in-bounds data ' - 'is considered when determining the default colormap `vmin` and `vmax`.' + "If ``True`` and the *x* and *y* axis limits are fixed, only in-bounds data " + "is considered when determining the default colormap `vmin` and `vmax`.", ), - 'cmap.levels': ( + "cmap.levels": ( 11, _validate_int, - 'Default number of `~proplot.colors.DiscreteNorm` levels for plotting ' - 'commands that use colormaps.' + "Default number of `~proplot.colors.DiscreteNorm` levels for plotting " + "commands that use colormaps.", ), - 'cmap.listedthresh': ( + "cmap.listedthresh": ( 64, _validate_int, - 'Native `~matplotlib.colors.ListedColormap`\\ s with more colors than ' - 'this are converted to `~proplot.colors.ContinuousColormap` rather than ' - '`~proplot.colors.DiscreteColormap`. This helps translate continuous ' - 'colormaps from external projects.' + "Native `~matplotlib.colors.ListedColormap`\\ s with more colors than " + "this are converted to `~proplot.colors.ContinuousColormap` rather than " + "`~proplot.colors.DiscreteColormap`. This helps translate continuous " + "colormaps from external projects.", ), - 'cmap.lut': ( + "cmap.lut": ( 256, _validate_int, - 'Number of colors in the colormap lookup table. ' - 'Alias for :rcraw:`image.lut`.' + "Number of colors in the colormap lookup table. " + "Alias for :rcraw:`image.lut`.", ), - 'cmap.robust': ( + "cmap.robust": ( False, _validate_bool, - 'If ``True``, the default colormap `vmin` and `vmax` are chosen using the ' - '2nd to 98th percentiles rather than the minimum and maximum.' + "If ``True``, the default colormap `vmin` and `vmax` are chosen using the " + "2nd to 98th percentiles rather than the minimum and maximum.", ), - 'cmap.sequential': ( + "cmap.sequential": ( CMAPSEQ, - _validate_cmap('continuous'), - 'Default colormap for sequential datasets. Alias for :rcraw:`image.cmap`.' + _validate_cmap("continuous"), + "Default colormap for sequential datasets. Alias for :rcraw:`image.cmap`.", ), - # Special setting - 'edgefix': ( + "edgefix": ( True, _validate_bool, 'Whether to fix issues with "white lines" appearing between patches ' - 'in saved vector graphics and with vector graphic backends. Applies ' - 'to colorbar levels and bar, area, pcolor, and contour plots.' + "in saved vector graphics and with vector graphic backends. Applies " + "to colorbar levels and bar, area, pcolor, and contour plots.", ), - # Font settings - 'font.name': ( - FONTNAME, - _validate_fontname, - 'Alias for :rcraw:`font.family`.' - ), - 'font.small': ( + "font.name": (FONTNAME, _validate_fontname, "Alias for :rcraw:`font.family`."), + "font.small": (SMALLSIZE, _validate_fontsize, "Alias for :rcraw:`font.smallsize`."), + "font.smallsize": ( SMALLSIZE, _validate_fontsize, - 'Alias for :rcraw:`font.smallsize`.' - ), - 'font.smallsize': ( - SMALLSIZE, - _validate_fontsize, - 'Meta setting that changes the label-like sizes ``axes.labelsize``, ' - '``legend.fontsize``, ``tick.labelsize``, and ``grid.labelsize``. Default is ' - "``'medium'`` (equivalent to :rcraw:`font.size`)." + _addendum_font - ), - 'font.large': ( - LARGESIZE, - _validate_fontsize, - 'Alias for :rcraw:`font.largesize`.' + "Meta setting that changes the label-like sizes ``axes.labelsize``, " + "``legend.fontsize``, ``tick.labelsize``, and ``grid.labelsize``. Default is " + "``'medium'`` (equivalent to :rcraw:`font.size`)." + _addendum_font, ), - 'font.largesize': ( + "font.large": (LARGESIZE, _validate_fontsize, "Alias for :rcraw:`font.largesize`."), + "font.largesize": ( LARGESIZE, _validate_fontsize, - 'Meta setting that changes the title-like sizes ``abc.size``, ``title.size``, ' - '``suptitle.size``, ``leftlabel.size``, ``rightlabel.size``, etc. Default is ' - "``'med-large'`` (i.e. 1.1 times :rcraw:`font.size`)." + _addendum_font + "Meta setting that changes the title-like sizes ``abc.size``, ``title.size``, " + "``suptitle.size``, ``leftlabel.size``, ``rightlabel.size``, etc. Default is " + "``'med-large'`` (i.e. 1.1 times :rcraw:`font.size`)." + _addendum_font, ), - # Formatter settings - 'formatter.timerotation': ( - 'vertical', + "formatter.timerotation": ( + "vertical", _validate_rotation, - 'Rotation for *x* axis datetime tick labels.' + _addendum_rotation + "Rotation for *x* axis datetime tick labels." + _addendum_rotation, ), - 'formatter.zerotrim': ( + "formatter.zerotrim": ( True, _validate_bool, - 'Whether to trim trailing decimal zeros on tick labels.' + "Whether to trim trailing decimal zeros on tick labels.", ), - 'formatter.limits': ( + "formatter.limits": ( [-5, 6], # must be list or else validated - _validate['axes.formatter.limits'], - 'Alias for :rcraw:`axes.formatter.limits`.' + _validate["axes.formatter.limits"], + "Alias for :rcraw:`axes.formatter.limits`.", ), - 'formatter.min_exponent': ( + "formatter.min_exponent": ( 0, - _validate['axes.formatter.min_exponent'], - 'Alias for :rcraw:`axes.formatter.min_exponent`.' + _validate["axes.formatter.min_exponent"], + "Alias for :rcraw:`axes.formatter.min_exponent`.", ), - 'formatter.offset_threshold': ( + "formatter.offset_threshold": ( 4, - _validate['axes.formatter.offset_threshold'], - 'Alias for :rcraw:`axes.formatter.offset_threshold`.' + _validate["axes.formatter.offset_threshold"], + "Alias for :rcraw:`axes.formatter.offset_threshold`.", ), - 'formatter.use_locale': ( + "formatter.use_locale": ( False, _validate_bool, - 'Alias for :rcraw:`axes.formatter.use_locale`.' + "Alias for :rcraw:`axes.formatter.use_locale`.", ), - 'formatter.use_mathtext': ( + "formatter.use_mathtext": ( MATHTEXT, _validate_bool, - 'Alias for :rcraw:`axes.formatter.use_mathtext`.' + "Alias for :rcraw:`axes.formatter.use_mathtext`.", ), - 'formatter.use_offset': ( + "formatter.use_offset": ( True, _validate_bool, - 'Alias for :rcraw:`axes.formatter.useOffset`.' + "Alias for :rcraw:`axes.formatter.useOffset`.", ), - # Geographic axes settings - 'geo.backend': ( - 'cartopy', - _validate_belongs('cartopy', 'basemap'), - 'The backend used for `~proplot.axes.GeoAxes`. Must be ' - "either 'cartopy' or 'basemap'." - ), - 'geo.extent': ( - 'globe', - _validate_belongs('globe', 'auto'), + "geo.backend": ( + "cartopy", + _validate_belongs("cartopy", "basemap"), + "The backend used for `~proplot.axes.GeoAxes`. Must be " + "either 'cartopy' or 'basemap'.", + ), + "geo.extent": ( + "globe", + _validate_belongs("globe", "auto"), "If ``'globe'``, the extent of cartopy `~proplot.axes.GeoAxes` is always " "global. If ``'auto'``, the extent is automatically adjusted based on " - "plotted content. Default is ``'globe'``." + "plotted content. Default is ``'globe'``.", ), - 'geo.round': ( + "geo.round": ( True, _validate_bool, "If ``True`` (the default), polar `~proplot.axes.GeoAxes` like ``'npstere'`` " - "and ``'spstere'`` are bounded with circles rather than squares." + "and ``'spstere'`` are bounded with circles rather than squares.", ), - - # Gridlines # NOTE: Here 'grid' and 'gridminor' or *not* aliases for native 'axes.grid' and # invented 'axes.gridminor' because native 'axes.grid' controls both major *and* # minor gridlines. Must handle it independently from these settings. - 'grid': ( - True, - _validate_bool, - 'Toggle major gridlines on and off.' - ), - 'grid.below': ( + "grid": (True, _validate_bool, "Toggle major gridlines on and off."), + "grid.below": ( GRIDBELOW, # like axes.axisbelow - _validate_belongs(False, 'line', True), - 'Alias for :rcraw:`axes.axisbelow`. If ``True``, draw gridlines below ' + _validate_belongs(False, "line", True), + "Alias for :rcraw:`axes.axisbelow`. If ``True``, draw gridlines below " "everything. If ``True``, draw them above everything. If ``'line'``, " - 'draw them above patches but below lines and markers.' + "draw them above patches but below lines and markers.", ), - 'grid.checkoverlap': ( + "grid.checkoverlap": ( True, _validate_bool, - 'Whether to have cartopy automatically check for and remove overlapping ' - '`~proplot.axes.GeoAxes` gridline labels.' + "Whether to have cartopy automatically check for and remove overlapping " + "`~proplot.axes.GeoAxes` gridline labels.", ), - 'grid.dmslabels': ( + "grid.dmslabels": ( True, _validate_bool, - 'Whether to use degrees-minutes-seconds rather than decimals for ' - 'cartopy `~proplot.axes.GeoAxes` gridlines.' + "Whether to use degrees-minutes-seconds rather than decimals for " + "cartopy `~proplot.axes.GeoAxes` gridlines.", ), - 'grid.geolabels': ( + "grid.geolabels": ( True, _validate_bool, "Whether to include the ``'geo'`` spine in cartopy >= 0.20 when otherwise " - 'toggling left, right, bottom, or top `~proplot.axes.GeoAxes` gridline labels.' + "toggling left, right, bottom, or top `~proplot.axes.GeoAxes` gridline labels.", ), - 'grid.inlinelabels': ( + "grid.inlinelabels": ( False, _validate_bool, - 'Whether to add inline labels for cartopy `~proplot.axes.GeoAxes` gridlines.' + "Whether to add inline labels for cartopy `~proplot.axes.GeoAxes` gridlines.", ), - 'grid.labels': ( + "grid.labels": ( False, _validate_bool, - 'Whether to add outer labels for `~proplot.axes.GeoAxes` gridlines.' + "Whether to add outer labels for `~proplot.axes.GeoAxes` gridlines.", ), - 'grid.labelcolor': ( + "grid.labelcolor": ( BLACK, _validate_color, - 'Font color for `~proplot.axes.GeoAxes` gridline labels.' + "Font color for `~proplot.axes.GeoAxes` gridline labels.", ), - 'grid.labelpad': ( + "grid.labelpad": ( GRIDPAD, _validate_pt, - 'Padding between the map boundary and cartopy `~proplot.axes.GeoAxes` ' - 'gridline labels.' + _addendum_pt + "Padding between the map boundary and cartopy `~proplot.axes.GeoAxes` " + "gridline labels." + _addendum_pt, ), - 'grid.labelsize': ( + "grid.labelsize": ( SMALLSIZE, _validate_fontsize, - 'Font size for `~proplot.axes.GeoAxes` gridline labels.' + _addendum_font + "Font size for `~proplot.axes.GeoAxes` gridline labels." + _addendum_font, ), - 'grid.labelweight': ( - 'normal', + "grid.labelweight": ( + "normal", _validate_fontweight, - 'Font weight for `~proplot.axes.GeoAxes` gridline labels.' + "Font weight for `~proplot.axes.GeoAxes` gridline labels.", ), - 'grid.nsteps': ( + "grid.nsteps": ( 250, _validate_int, - 'Number of points used to draw cartopy `~proplot.axes.GeoAxes` gridlines.' - ), - 'grid.pad': ( - GRIDPAD, - _validate_pt, - 'Alias for :rcraw:`grid.labelpad`.' + "Number of points used to draw cartopy `~proplot.axes.GeoAxes` gridlines.", ), - 'grid.rotatelabels': ( + "grid.pad": (GRIDPAD, _validate_pt, "Alias for :rcraw:`grid.labelpad`."), + "grid.rotatelabels": ( False, # False limits projections where labels are available _validate_bool, - 'Whether to rotate cartopy `~proplot.axes.GeoAxes` gridline labels.' + "Whether to rotate cartopy `~proplot.axes.GeoAxes` gridline labels.", ), - 'grid.style': ( - '-', + "grid.style": ( + "-", _validate_linestyle, - 'Major gridline style. Alias for :rcraw:`grid.linestyle`.' + "Major gridline style. Alias for :rcraw:`grid.linestyle`.", ), - 'grid.width': ( + "grid.width": ( LINEWIDTH, _validate_pt, - 'Major gridline width. Alias for :rcraw:`grid.linewidth`.' + "Major gridline width. Alias for :rcraw:`grid.linewidth`.", ), - 'grid.widthratio': ( + "grid.widthratio": ( GRIDRATIO, _validate_float, - 'Ratio of minor gridline width to major gridline width.' + "Ratio of minor gridline width to major gridline width.", ), - # Minor gridlines - 'gridminor': ( - False, - _validate_bool, - 'Toggle minor gridlines on and off.' - ), - 'gridminor.alpha': ( - GRIDALPHA, - _validate_float, - 'Minor gridline opacity.' - ), - 'gridminor.color': ( - BLACK, - _validate_color, - 'Minor gridline color.' - ), - 'gridminor.linestyle': ( - GRIDSTYLE, - _validate_linestyle, - 'Minor gridline style.' - ), - 'gridminor.linewidth': ( + "gridminor": (False, _validate_bool, "Toggle minor gridlines on and off."), + "gridminor.alpha": (GRIDALPHA, _validate_float, "Minor gridline opacity."), + "gridminor.color": (BLACK, _validate_color, "Minor gridline color."), + "gridminor.linestyle": (GRIDSTYLE, _validate_linestyle, "Minor gridline style."), + "gridminor.linewidth": ( GRIDRATIO * LINEWIDTH, _validate_pt, - 'Minor gridline width.' + "Minor gridline width.", ), - 'gridminor.style': ( + "gridminor.style": ( GRIDSTYLE, _validate_linestyle, - 'Minor gridline style. Alias for :rcraw:`gridminor.linestyle`.' + "Minor gridline style. Alias for :rcraw:`gridminor.linestyle`.", ), - 'gridminor.width': ( + "gridminor.width": ( GRIDRATIO * LINEWIDTH, _validate_pt, - 'Minor gridline width. Alias for :rcraw:`gridminor.linewidth`.' + "Minor gridline width. Alias for :rcraw:`gridminor.linewidth`.", ), - # Backend stuff - 'inlineformat': ( - 'retina', - _validate_belongs('svg', 'pdf', 'retina', 'png', 'jpeg'), - 'The inline backend figure format. Valid formats include ' - "``'svg'``, ``'pdf'``, ``'retina'``, ``'png'``, and ``jpeg``." + "inlineformat": ( + "retina", + _validate_belongs("svg", "pdf", "retina", "png", "jpeg"), + "The inline backend figure format. Valid formats include " + "``'svg'``, ``'pdf'``, ``'retina'``, ``'png'``, and ``jpeg``.", ), - # Inner borders - 'innerborders': ( + "innerborders": ( False, _validate_bool, - 'Toggles internal political border lines (e.g. states and provinces) ' - 'on and off.' + "Toggles internal political border lines (e.g. states and provinces) " + "on and off.", ), - 'innerborders.alpha': ( + "innerborders.alpha": ( None, _validate_or_none(_validate_float), - 'Opacity for internal political border lines', + "Opacity for internal political border lines", ), - 'innerborders.color': ( + "innerborders.color": ( BLACK, _validate_color, - 'Line color for internal political border lines.' + "Line color for internal political border lines.", ), - 'innerborders.linewidth': ( + "innerborders.linewidth": ( LINEWIDTH, _validate_pt, - 'Line width for internal political border lines.' + "Line width for internal political border lines.", ), - 'innerborders.zorder': ( + "innerborders.zorder": ( ZLINES, _validate_float, - 'Z-order for internal political border lines.' + "Z-order for internal political border lines.", ), - # Axis label settings - 'label.color': ( - BLACK, - _validate_color, - 'Alias for :rcraw:`axes.labelcolor`.' - ), - 'label.pad': ( + "label.color": (BLACK, _validate_color, "Alias for :rcraw:`axes.labelcolor`."), + "label.pad": ( LABELPAD, _validate_pt, - 'Alias for :rcraw:`axes.labelpad`.' - + _addendum_pt + "Alias for :rcraw:`axes.labelpad`." + _addendum_pt, ), - 'label.size': ( + "label.size": ( SMALLSIZE, _validate_fontsize, - 'Alias for :rcraw:`axes.labelsize`.' + _addendum_font + "Alias for :rcraw:`axes.labelsize`." + _addendum_font, ), - 'label.weight': ( - 'normal', + "label.weight": ( + "normal", _validate_fontweight, - 'Alias for :rcraw:`axes.labelweight`.' + "Alias for :rcraw:`axes.labelweight`.", ), - # Lake patches - 'lakes': ( - False, - _validate_bool, - 'Toggles lake patches on and off.' - ), - 'lakes.alpha': ( + "lakes": (False, _validate_bool, "Toggles lake patches on and off."), + "lakes.alpha": ( None, _validate_or_none(_validate_float), - 'Opacity for lake patches', + "Opacity for lake patches", ), - 'lakes.color': ( - WHITE, - _validate_color, - 'Face color for lake patches.' - ), - 'lakes.zorder': ( - ZPATCHES, - _validate_float, - 'Z-order for lake patches.' - ), - + "lakes.color": (WHITE, _validate_color, "Face color for lake patches."), + "lakes.zorder": (ZPATCHES, _validate_float, "Z-order for lake patches."), # Land patches - 'land': ( - False, - _validate_bool, - 'Toggles land patches on and off.' - ), - 'land.alpha': ( + "land": (False, _validate_bool, "Toggles land patches on and off."), + "land.alpha": ( None, _validate_or_none(_validate_float), - 'Opacity for land patches', - ), - 'land.color': ( - BLACK, - _validate_color, - 'Face color for land patches.' + "Opacity for land patches", ), - 'land.zorder': ( - ZPATCHES, - _validate_float, - 'Z-order for land patches.' - ), - + "land.color": (BLACK, _validate_color, "Face color for land patches."), + "land.zorder": (ZPATCHES, _validate_float, "Z-order for land patches."), # Left subplot labels - 'leftlabel.color': ( + "leftlabel.color": ( BLACK, _validate_color, - 'Font color for row labels on the left-hand side.' + "Font color for row labels on the left-hand side.", ), - 'leftlabel.pad': ( + "leftlabel.pad": ( TITLEPAD, _validate_pt, - 'Padding between axes content and row labels on the left-hand side.' - + _addendum_pt + "Padding between axes content and row labels on the left-hand side." + + _addendum_pt, ), - 'leftlabel.rotation': ( - 'vertical', + "leftlabel.rotation": ( + "vertical", _validate_rotation, - 'Rotation for row labels on the left-hand side.' + _addendum_rotation + "Rotation for row labels on the left-hand side." + _addendum_rotation, ), - 'leftlabel.size': ( + "leftlabel.size": ( LARGESIZE, _validate_fontsize, - 'Font size for row labels on the left-hand side.' + _addendum_font + "Font size for row labels on the left-hand side." + _addendum_font, ), - 'leftlabel.weight': ( - 'bold', + "leftlabel.weight": ( + "bold", _validate_fontweight, - 'Font weight for row labels on the left-hand side.' + "Font weight for row labels on the left-hand side.", ), - # Meta settings - 'margin': ( + "margin": ( MARGIN, _validate_float, - 'The fractional *x* and *y* axis data margins when limits are unset. ' - 'Alias for :rcraw:`axes.margin`.' + "The fractional *x* and *y* axis data margins when limits are unset. " + "Alias for :rcraw:`axes.margin`.", ), - 'meta.edgecolor': ( + "meta.edgecolor": ( BLACK, _validate_color, - 'Color of axis spines, tick marks, tick labels, and labels.' + "Color of axis spines, tick marks, tick labels, and labels.", ), - 'meta.color': ( + "meta.color": ( BLACK, _validate_color, - 'Color of axis spines, tick marks, tick labels, and labels. ' - 'Alias for :rcraw:`meta.edgecolor`.' + "Color of axis spines, tick marks, tick labels, and labels. " + "Alias for :rcraw:`meta.edgecolor`.", ), - 'meta.linewidth': ( + "meta.linewidth": ( LINEWIDTH, _validate_pt, - 'Thickness of axis spines and major tick lines.' + "Thickness of axis spines and major tick lines.", ), - 'meta.width': ( + "meta.width": ( LINEWIDTH, _validate_pt, - 'Thickness of axis spines and major tick lines. ' - 'Alias for :rcraw:`meta.linewidth`.' + "Thickness of axis spines and major tick lines. " + "Alias for :rcraw:`meta.linewidth`.", ), - # For negative positive patches - 'negcolor': ( - 'blue7', + "negcolor": ( + "blue7", _validate_color, - 'Color for negative bars and shaded areas when using ``negpos=True``. ' - 'See also :rcraw:`poscolor`.' + "Color for negative bars and shaded areas when using ``negpos=True``. " + "See also :rcraw:`poscolor`.", ), - 'poscolor': ( - 'red7', + "poscolor": ( + "red7", _validate_color, - 'Color for positive bars and shaded areas when using ``negpos=True``. ' - 'See also :rcraw:`negcolor`.' + "Color for positive bars and shaded areas when using ``negpos=True``. " + "See also :rcraw:`negcolor`.", ), - # Ocean patches - 'ocean': ( - False, - _validate_bool, - 'Toggles ocean patches on and off.' - ), - 'ocean.alpha': ( + "ocean": (False, _validate_bool, "Toggles ocean patches on and off."), + "ocean.alpha": ( None, _validate_or_none(_validate_float), - 'Opacity for ocean patches', - ), - 'ocean.color': ( - WHITE, - _validate_color, - 'Face color for ocean patches.' + "Opacity for ocean patches", ), - 'ocean.zorder': ( - ZPATCHES, - _validate_float, - 'Z-order for ocean patches.' - ), - + "ocean.color": (WHITE, _validate_color, "Face color for ocean patches."), + "ocean.zorder": (ZPATCHES, _validate_float, "Z-order for ocean patches."), # Geographic resolution - 'reso': ( - 'lo', - _validate_belongs('lo', 'med', 'hi', 'x-hi', 'xx-hi'), - 'Resolution for `~proplot.axes.GeoAxes` geographic features. ' - "Must be one of ``'lo'``, ``'med'``, ``'hi'``, ``'x-hi'``, or ``'xx-hi'``." + "reso": ( + "lo", + _validate_belongs("lo", "med", "hi", "x-hi", "xx-hi"), + "Resolution for `~proplot.axes.GeoAxes` geographic features. " + "Must be one of ``'lo'``, ``'med'``, ``'hi'``, ``'x-hi'``, or ``'xx-hi'``.", ), - # Right subplot labels - 'rightlabel.color': ( + "rightlabel.color": ( BLACK, _validate_color, - 'Font color for row labels on the right-hand side.' + "Font color for row labels on the right-hand side.", ), - 'rightlabel.pad': ( + "rightlabel.pad": ( TITLEPAD, _validate_pt, - 'Padding between axes content and row labels on the right-hand side.' - + _addendum_pt + "Padding between axes content and row labels on the right-hand side." + + _addendum_pt, ), - 'rightlabel.rotation': ( - 'vertical', + "rightlabel.rotation": ( + "vertical", _validate_rotation, - 'Rotation for row labels on the right-hand side.' + _addendum_rotation + "Rotation for row labels on the right-hand side." + _addendum_rotation, ), - 'rightlabel.size': ( + "rightlabel.size": ( LARGESIZE, _validate_fontsize, - 'Font size for row labels on the right-hand side.' + _addendum_font + "Font size for row labels on the right-hand side." + _addendum_font, ), - 'rightlabel.weight': ( - 'bold', + "rightlabel.weight": ( + "bold", _validate_fontweight, - 'Font weight for row labels on the right-hand side.' + "Font weight for row labels on the right-hand side.", ), - # River lines - 'rivers': ( - False, - _validate_bool, - 'Toggles river lines on and off.' - ), - 'rivers.alpha': ( + "rivers": (False, _validate_bool, "Toggles river lines on and off."), + "rivers.alpha": ( None, _validate_or_none(_validate_float), - 'Opacity for river lines.', - ), - 'rivers.color': ( - BLACK, - _validate_color, - 'Line color for river lines.' + "Opacity for river lines.", ), - 'rivers.linewidth': ( - LINEWIDTH, - _validate_pt, - 'Line width for river lines.' - ), - 'rivers.zorder': ( - ZLINES, - _validate_float, - 'Z-order for river lines.' - ), - + "rivers.color": (BLACK, _validate_color, "Line color for river lines."), + "rivers.linewidth": (LINEWIDTH, _validate_pt, "Line width for river lines."), + "rivers.zorder": (ZLINES, _validate_float, "Z-order for river lines."), # Subplots settings - 'subplots.align': ( + "subplots.align": ( False, _validate_bool, - 'Whether to align axis labels during draw. See `aligning labels ' - '`__.' # noqa: E501 + "Whether to align axis labels during draw. See `aligning labels " + "`__.", # noqa: E501 ), - 'subplots.equalspace': ( + "subplots.equalspace": ( False, _validate_bool, - 'Whether to make the tight layout algorithm assign the same space for ' - 'every row and the same space for every column.' + "Whether to make the tight layout algorithm assign the same space for " + "every row and the same space for every column.", ), - 'subplots.groupspace': ( + "subplots.groupspace": ( True, _validate_bool, - 'Whether to make the tight layout algorithm consider space between only ' - 'adjacent subplot "groups" rather than every subplot in the row or column.' + "Whether to make the tight layout algorithm consider space between only " + 'adjacent subplot "groups" rather than every subplot in the row or column.', ), - 'subplots.innerpad': ( + "subplots.innerpad": ( 1, _validate_em, - 'Padding between adjacent subplots.' + _addendum_em + "Padding between adjacent subplots." + _addendum_em, ), - 'subplots.outerpad': ( + "subplots.outerpad": ( 0.5, _validate_em, - 'Padding around figure edge.' + _addendum_em + "Padding around figure edge." + _addendum_em, ), - 'subplots.panelpad': ( + "subplots.panelpad": ( 0.5, _validate_em, - 'Padding between subplots and panels, and between stacked panels.' - + _addendum_em - ), - 'subplots.panelwidth': ( - 0.5, - _validate_in, - 'Width of side panels.' + _addendum_in + "Padding between subplots and panels, and between stacked panels." + + _addendum_em, ), - 'subplots.refwidth': ( + "subplots.panelwidth": (0.5, _validate_in, "Width of side panels." + _addendum_in), + "subplots.refwidth": ( 2.5, _validate_in, - 'Default width of the reference subplot.' + _addendum_in + "Default width of the reference subplot." + _addendum_in, ), - 'subplots.share': ( + "subplots.share": ( True, - _validate_belongs(0, 1, 2, 3, 4, False, 'labels', 'limits', True, 'all'), - 'The axis sharing level, one of ``0``, ``1``, ``2``, or ``3``, or the ' + _validate_belongs(0, 1, 2, 3, 4, False, "labels", "limits", True, "all"), + "The axis sharing level, one of ``0``, ``1``, ``2``, or ``3``, or the " "more intuitive aliases ``False``, ``'labels'``, ``'limits'``, or ``True``. " - 'See `~proplot.figure.Figure` for details.' + "See `~proplot.figure.Figure` for details.", ), - 'subplots.span': ( + "subplots.span": ( True, _validate_bool, - 'Toggles spanning axis labels. See `~proplot.ui.subplots` for details.' + "Toggles spanning axis labels. See `~proplot.ui.subplots` for details.", ), - 'subplots.tight': ( + "subplots.tight": ( True, _validate_bool, - 'Whether to auto-adjust the subplot spaces and figure margins.' + "Whether to auto-adjust the subplot spaces and figure margins.", ), - # Super title settings - 'suptitle.color': ( - BLACK, - _validate_color, - 'Figure title color.' - ), - 'suptitle.pad': ( + "suptitle.color": (BLACK, _validate_color, "Figure title color."), + "suptitle.pad": ( TITLEPAD, _validate_pt, - 'Padding between axes content and the figure super title.' + _addendum_pt + "Padding between axes content and the figure super title." + _addendum_pt, ), - 'suptitle.size': ( + "suptitle.size": ( LARGESIZE, _validate_fontsize, - 'Figure title font size.' + _addendum_font - ), - 'suptitle.weight': ( - 'bold', - _validate_fontweight, - 'Figure title font weight.' + "Figure title font size." + _addendum_font, ), - + "suptitle.weight": ("bold", _validate_fontweight, "Figure title font weight."), # Tick settings - 'tick.color': ( - BLACK, - _validate_color, - 'Major and minor tick color.' - ), - 'tick.dir': ( + "tick.color": (BLACK, _validate_color, "Major and minor tick color."), + "tick.dir": ( TICKDIR, - _validate_belongs('in', 'out', 'inout'), - 'Major and minor tick direction. Must be one of ' - "``'out'``, ``'in'``, or ``'inout'``." - ), - 'tick.labelcolor': ( - BLACK, - _validate_color, - 'Axis tick label color.' + _validate_belongs("in", "out", "inout"), + "Major and minor tick direction. Must be one of " + "``'out'``, ``'in'``, or ``'inout'``.", ), - 'tick.labelpad': ( + "tick.labelcolor": (BLACK, _validate_color, "Axis tick label color."), + "tick.labelpad": ( TICKPAD, _validate_pt, - 'Padding between ticks and tick labels.' + _addendum_pt + "Padding between ticks and tick labels." + _addendum_pt, ), - 'tick.labelsize': ( + "tick.labelsize": ( SMALLSIZE, _validate_fontsize, - 'Axis tick label font size.' + _addendum_font + "Axis tick label font size." + _addendum_font, ), - 'tick.labelweight': ( - 'normal', + "tick.labelweight": ( + "normal", _validate_fontweight, - 'Axis tick label font weight.' + "Axis tick label font weight.", ), - 'tick.len': ( - TICKLEN, - _validate_pt, - 'Length of major ticks in points.' - ), - 'tick.lenratio': ( + "tick.len": (TICKLEN, _validate_pt, "Length of major ticks in points."), + "tick.lenratio": ( TICKLENRATIO, _validate_float, - 'Ratio of minor tickline length to major tickline length.' - ), - 'tick.linewidth': ( - LINEWIDTH, - _validate_pt, - 'Major tickline width.' + "Ratio of minor tickline length to major tickline length.", ), - 'tick.minor': ( + "tick.linewidth": (LINEWIDTH, _validate_pt, "Major tickline width."), + "tick.minor": ( TICKMINOR, _validate_bool, - 'Toggles minor ticks on and off.', + "Toggles minor ticks on and off.", ), - 'tick.pad': ( - TICKPAD, - _validate_pt, - 'Alias for :rcraw:`tick.labelpad`.' - ), - 'tick.width': ( + "tick.pad": (TICKPAD, _validate_pt, "Alias for :rcraw:`tick.labelpad`."), + "tick.width": ( LINEWIDTH, _validate_pt, - 'Major tickline width. Alias for :rcraw:`tick.linewidth`.' + "Major tickline width. Alias for :rcraw:`tick.linewidth`.", ), - 'tick.widthratio': ( + "tick.widthratio": ( TICKWIDTHRATIO, _validate_float, - 'Ratio of minor tickline width to major tickline width.' + "Ratio of minor tickline width to major tickline width.", ), - # Title settings - 'title.above': ( + "title.above": ( True, - _validate_belongs(False, True, 'panels'), - 'Whether to move outer titles and a-b-c labels above panels, colorbars, or ' + _validate_belongs(False, True, "panels"), + "Whether to move outer titles and a-b-c labels above panels, colorbars, or " "legends that are above the axes. If the string 'panels' then text is only " - 'redirected above axes panels. Otherwise should be boolean.' + "redirected above axes panels. Otherwise should be boolean.", ), - 'title.border': ( + "title.border": ( True, _validate_bool, - 'Whether to draw a white border around titles ' - 'when :rcraw:`title.loc` is inside the axes.' - ), - 'title.borderwidth': ( - 1.5, - _validate_pt, - 'Width of the border around titles.' + "Whether to draw a white border around titles " + "when :rcraw:`title.loc` is inside the axes.", ), - 'title.bbox': ( + "title.borderwidth": (1.5, _validate_pt, "Width of the border around titles."), + "title.bbox": ( False, _validate_bool, - 'Whether to draw semi-transparent bounding boxes around titles ' - 'when :rcraw:`title.loc` is inside the axes.' - ), - 'title.bboxcolor': ( - WHITE, - _validate_color, - 'Axes title bounding box color.' - ), - 'title.bboxstyle': ( - 'square', - _validate_boxstyle, - 'Axes title bounding box style.' + "Whether to draw semi-transparent bounding boxes around titles " + "when :rcraw:`title.loc` is inside the axes.", ), - 'title.bboxalpha': ( - 0.5, - _validate_float, - 'Axes title bounding box opacity.' - ), - 'title.bboxpad': ( + "title.bboxcolor": (WHITE, _validate_color, "Axes title bounding box color."), + "title.bboxstyle": ("square", _validate_boxstyle, "Axes title bounding box style."), + "title.bboxalpha": (0.5, _validate_float, "Axes title bounding box opacity."), + "title.bboxpad": ( None, _validate_or_none(_validate_pt), - 'Padding for the title bounding box. By default this is scaled ' - 'to make the box flush against the axes edge.' + _addendum_pt + "Padding for the title bounding box. By default this is scaled " + "to make the box flush against the axes edge." + _addendum_pt, ), - 'title.color': ( + "title.color": ( BLACK, _validate_color, - 'Axes title color. Alias for :rcraw:`axes.titlecolor`.' + "Axes title color. Alias for :rcraw:`axes.titlecolor`.", ), - 'title.loc': ( - 'center', + "title.loc": ( + "center", _validate_belongs(*TEXT_LOCS), - 'Title position. For options see the :ref:`location table `.' + "Title position. For options see the :ref:`location table `.", ), - 'title.pad': ( + "title.pad": ( TITLEPAD, _validate_pt, - 'Padding between the axes edge and the inner and outer titles and ' - 'a-b-c labels. Alias for :rcraw:`axes.titlepad`.' + _addendum_pt + "Padding between the axes edge and the inner and outer titles and " + "a-b-c labels. Alias for :rcraw:`axes.titlepad`." + _addendum_pt, ), - 'title.size': ( + "title.size": ( LARGESIZE, _validate_fontsize, - 'Axes title font size. Alias for :rcraw:`axes.titlesize`.' + _addendum_font + "Axes title font size. Alias for :rcraw:`axes.titlesize`." + _addendum_font, ), - 'title.weight': ( - 'normal', + "title.weight": ( + "normal", _validate_fontweight, - 'Axes title font weight. Alias for :rcraw:`axes.titleweight`.' + "Axes title font weight. Alias for :rcraw:`axes.titleweight`.", ), - # Top subplot label settings - 'toplabel.color': ( + "toplabel.color": ( BLACK, _validate_color, - 'Font color for column labels on the top of the figure.' + "Font color for column labels on the top of the figure.", ), - 'toplabel.pad': ( + "toplabel.pad": ( TITLEPAD, _validate_pt, - 'Padding between axes content and column labels on the top of the figure.' - + _addendum_pt + "Padding between axes content and column labels on the top of the figure." + + _addendum_pt, ), - 'toplabel.rotation': ( - 'horizontal', + "toplabel.rotation": ( + "horizontal", _validate_rotation, - 'Rotation for column labels at the top of the figure.' + _addendum_rotation + "Rotation for column labels at the top of the figure." + _addendum_rotation, ), - 'toplabel.size': ( + "toplabel.size": ( LARGESIZE, _validate_fontsize, - 'Font size for column labels on the top of the figure.' + _addendum_font + "Font size for column labels on the top of the figure." + _addendum_font, ), - 'toplabel.weight': ( - 'bold', + "toplabel.weight": ( + "bold", _validate_fontweight, - 'Font weight for column labels on the top of the figure.' + "Font weight for column labels on the top of the figure.", ), - # Unit formatting - 'unitformat': ( - 'L', + "unitformat": ( + "L", _validate_string, - 'The format string used to format `pint.Quantity` default unit labels ' - 'using ``format(units, unitformat)``. See also :rcraw:`autoformat`.' + "The format string used to format `pint.Quantity` default unit labels " + "using ``format(units, unitformat)``. See also :rcraw:`autoformat`.", ), } # Child settings. Changing the parent changes all the children, but # changing any of the children does not change the parent. _rc_children = { - 'font.smallsize': ( # the 'small' fonts - 'tick.labelsize', 'xtick.labelsize', 'ytick.labelsize', - 'axes.labelsize', 'legend.fontsize', 'grid.labelsize' - ), - 'font.largesize': ( # the 'large' fonts - 'abc.size', 'figure.titlesize', 'suptitle.size', 'axes.titlesize', 'title.size', - 'leftlabel.size', 'toplabel.size', 'rightlabel.size', 'bottomlabel.size' - ), - 'meta.color': ( # change the 'color' of an axes - 'axes.edgecolor', 'axes.labelcolor', 'legend.edgecolor', 'colorbar.edgecolor', - 'tick.labelcolor', 'hatch.color', 'xtick.color', 'ytick.color' - ), - 'meta.width': ( # change the tick and frame line width - 'axes.linewidth', 'tick.width', 'tick.linewidth', 'xtick.major.width', - 'ytick.major.width', 'grid.width', 'grid.linewidth', - ), - 'axes.margin': ('axes.xmargin', 'axes.ymargin'), - 'grid.color': ('gridminor.color', 'grid.labelcolor'), - 'grid.alpha': ('gridminor.alpha',), - 'grid.linewidth': ('gridminor.linewidth',), - 'grid.linestyle': ('gridminor.linestyle',), - 'tick.color': ('xtick.color', 'ytick.color'), - 'tick.dir': ('xtick.direction', 'ytick.direction'), - 'tick.len': ('xtick.major.size', 'ytick.major.size'), - 'tick.minor': ('xtick.minor.visible', 'ytick.minor.visible'), - 'tick.pad': ('xtick.major.pad', 'xtick.minor.pad', 'ytick.major.pad', 'ytick.minor.pad'), # noqa: E501 - 'tick.width': ('xtick.major.width', 'ytick.major.width'), - 'tick.labelsize': ('xtick.labelsize', 'ytick.labelsize'), + "font.smallsize": ( # the 'small' fonts + "tick.labelsize", + "xtick.labelsize", + "ytick.labelsize", + "axes.labelsize", + "legend.fontsize", + "grid.labelsize", + ), + "font.largesize": ( # the 'large' fonts + "abc.size", + "figure.titlesize", + "suptitle.size", + "axes.titlesize", + "title.size", + "leftlabel.size", + "toplabel.size", + "rightlabel.size", + "bottomlabel.size", + ), + "meta.color": ( # change the 'color' of an axes + "axes.edgecolor", + "axes.labelcolor", + "legend.edgecolor", + "colorbar.edgecolor", + "tick.labelcolor", + "hatch.color", + "xtick.color", + "ytick.color", + ), + "meta.width": ( # change the tick and frame line width + "axes.linewidth", + "tick.width", + "tick.linewidth", + "xtick.major.width", + "ytick.major.width", + "grid.width", + "grid.linewidth", + ), + "axes.margin": ("axes.xmargin", "axes.ymargin"), + "grid.color": ("gridminor.color", "grid.labelcolor"), + "grid.alpha": ("gridminor.alpha",), + "grid.linewidth": ("gridminor.linewidth",), + "grid.linestyle": ("gridminor.linestyle",), + "tick.color": ("xtick.color", "ytick.color"), + "tick.dir": ("xtick.direction", "ytick.direction"), + "tick.len": ("xtick.major.size", "ytick.major.size"), + "tick.minor": ("xtick.minor.visible", "ytick.minor.visible"), + "tick.pad": ( + "xtick.major.pad", + "xtick.minor.pad", + "ytick.major.pad", + "ytick.minor.pad", + ), # noqa: E501 + "tick.width": ("xtick.major.width", "ytick.major.width"), + "tick.labelsize": ("xtick.labelsize", "ytick.labelsize"), } # Recently added settings. Update these only if the version is recent enough # NOTE: We don't make 'title.color' a child of 'axes.titlecolor' because # the latter can take on the value 'auto' and can't handle that right now. -if _version_mpl >= '3.2': - _rc_matplotlib_default['axes.titlecolor'] = BLACK - _rc_children['title.color'] = ('axes.titlecolor',) -if _version_mpl >= '3.4': - _rc_matplotlib_default['xtick.labelcolor'] = BLACK - _rc_matplotlib_default['ytick.labelcolor'] = BLACK - _rc_children['tick.labelcolor'] = ('xtick.labelcolor', 'ytick.labelcolor') - _rc_children['grid.labelcolor'] = ('xtick.labelcolor', 'ytick.labelcolor') - _rc_children['meta.color'] += ('xtick.labelcolor', 'ytick.labelcolor') +if _version_mpl >= "3.2": + _rc_matplotlib_default["axes.titlecolor"] = BLACK + _rc_children["title.color"] = ("axes.titlecolor",) +if _version_mpl >= "3.4": + _rc_matplotlib_default["xtick.labelcolor"] = BLACK + _rc_matplotlib_default["ytick.labelcolor"] = BLACK + _rc_children["tick.labelcolor"] = ("xtick.labelcolor", "ytick.labelcolor") + _rc_children["grid.labelcolor"] = ("xtick.labelcolor", "ytick.labelcolor") + _rc_children["meta.color"] += ("xtick.labelcolor", "ytick.labelcolor") # Setting synonyms. Changing one setting changes the other. Also account for existing # children. Most of these are aliased due to proplot settings overlapping with # existing matplotlib settings. _rc_synonyms = ( - ('cmap', 'image.cmap', 'cmap.sequential'), - ('cmap.lut', 'image.lut'), - ('font.name', 'font.family'), - ('font.small', 'font.smallsize'), - ('font.large', 'font.largesize'), - ('formatter.limits', 'axes.formatter.limits'), - ('formatter.use_locale', 'axes.formatter.use_locale'), - ('formatter.use_mathtext', 'axes.formatter.use_mathtext'), - ('formatter.min_exponent', 'axes.formatter.min_exponent'), - ('formatter.use_offset', 'axes.formatter.useoffset'), - ('formatter.offset_threshold', 'axes.formatter.offset_threshold'), - ('grid.below', 'axes.axisbelow'), - ('grid.labelpad', 'grid.pad'), - ('grid.linewidth', 'grid.width'), - ('grid.linestyle', 'grid.style'), - ('gridminor.linewidth', 'gridminor.width'), - ('gridminor.linestyle', 'gridminor.style'), - ('label.color', 'axes.labelcolor'), - ('label.pad', 'axes.labelpad'), - ('label.size', 'axes.labelsize'), - ('label.weight', 'axes.labelweight'), - ('margin', 'axes.margin'), - ('meta.width', 'meta.linewidth'), - ('meta.color', 'meta.edgecolor'), - ('tick.labelpad', 'tick.pad'), - ('tick.labelsize', 'grid.labelsize'), - ('tick.labelcolor', 'grid.labelcolor'), - ('tick.labelweight', 'grid.labelweight'), - ('tick.linewidth', 'tick.width'), - ('title.pad', 'axes.titlepad'), - ('title.size', 'axes.titlesize'), - ('title.weight', 'axes.titleweight'), + ("cmap", "image.cmap", "cmap.sequential"), + ("cmap.lut", "image.lut"), + ("font.name", "font.family"), + ("font.small", "font.smallsize"), + ("font.large", "font.largesize"), + ("formatter.limits", "axes.formatter.limits"), + ("formatter.use_locale", "axes.formatter.use_locale"), + ("formatter.use_mathtext", "axes.formatter.use_mathtext"), + ("formatter.min_exponent", "axes.formatter.min_exponent"), + ("formatter.use_offset", "axes.formatter.useoffset"), + ("formatter.offset_threshold", "axes.formatter.offset_threshold"), + ("grid.below", "axes.axisbelow"), + ("grid.labelpad", "grid.pad"), + ("grid.linewidth", "grid.width"), + ("grid.linestyle", "grid.style"), + ("gridminor.linewidth", "gridminor.width"), + ("gridminor.linestyle", "gridminor.style"), + ("label.color", "axes.labelcolor"), + ("label.pad", "axes.labelpad"), + ("label.size", "axes.labelsize"), + ("label.weight", "axes.labelweight"), + ("margin", "axes.margin"), + ("meta.width", "meta.linewidth"), + ("meta.color", "meta.edgecolor"), + ("tick.labelpad", "tick.pad"), + ("tick.labelsize", "grid.labelsize"), + ("tick.labelcolor", "grid.labelcolor"), + ("tick.labelweight", "grid.labelweight"), + ("tick.linewidth", "tick.width"), + ("title.pad", "axes.titlepad"), + ("title.size", "axes.titlesize"), + ("title.weight", "axes.titleweight"), ) for _keys in _rc_synonyms: for _key in _keys: @@ -1992,73 +1847,72 @@ def copy(self): # do not have to be added as _rc_children, since Configurator translates before # retrieving the list of children in _get_item_dicts. _rc_removed = { # {key: (alternative, version)} dictionary - 'rgbcycle': ('', '0.6.0'), # no alternative, we no longer offer this feature - 'geogrid.latmax': ('Please use ax.format(latmax=N) instead.', '0.6.0'), - 'geogrid.latstep': ('Please use ax.format(latlines=N) instead.', '0.6.0'), - 'geogrid.lonstep': ('Please use ax.format(lonlines=N) instead.', '0.6.0'), - 'gridminor.latstep': ('Please use ax.format(latminorlines=N) instead.', '0.6.0'), - 'gridminor.lonstep': ('Please use ax.format(lonminorlines=N) instead.', '0.6.0'), + "rgbcycle": ("", "0.6.0"), # no alternative, we no longer offer this feature + "geogrid.latmax": ("Please use ax.format(latmax=N) instead.", "0.6.0"), + "geogrid.latstep": ("Please use ax.format(latlines=N) instead.", "0.6.0"), + "geogrid.lonstep": ("Please use ax.format(lonlines=N) instead.", "0.6.0"), + "gridminor.latstep": ("Please use ax.format(latminorlines=N) instead.", "0.6.0"), + "gridminor.lonstep": ("Please use ax.format(lonminorlines=N) instead.", "0.6.0"), } _rc_renamed = { # {old_key: (new_key, version)} dictionary - 'abc.format': ('abc', '0.5.0'), - 'align': ('subplots.align', '0.6.0'), - 'axes.facealpha': ('axes.alpha', '0.6.0'), - 'geoaxes.edgecolor': ('axes.edgecolor', '0.6.0'), - 'geoaxes.facealpha': ('axes.alpha', '0.6.0'), - 'geoaxes.facecolor': ('axes.facecolor', '0.6.0'), - 'geoaxes.linewidth': ('axes.linewidth', '0.6.0'), - 'geogrid.alpha': ('grid.alpha', '0.6.0'), - 'geogrid.color': ('grid.color', '0.6.0'), - 'geogrid.labels': ('grid.labels', '0.6.0'), - 'geogrid.labelpad': ('grid.pad', '0.6.0'), - 'geogrid.labelsize': ('grid.labelsize', '0.6.0'), - 'geogrid.linestyle': ('grid.linestyle', '0.6.0'), - 'geogrid.linewidth': ('grid.linewidth', '0.6.0'), - 'share': ('subplots.share', '0.6.0'), - 'small': ('font.smallsize', '0.6.0'), - 'large': ('font.largesize', '0.6.0'), - 'span': ('subplots.span', '0.6.0'), - 'tight': ('subplots.tight', '0.6.0'), - 'axes.formatter.timerotation': ('formatter.timerotation', '0.6.0'), - 'axes.formatter.zerotrim': ('formatter.zerotrim', '0.6.0'), - 'abovetop': ('title.above', '0.7.0'), - 'subplots.pad': ('subplots.outerpad', '0.7.0'), - 'subplots.axpad': ('subplots.innerpad', '0.7.0'), - 'subplots.axwidth': ('subplots.refwidth', '0.7.0'), - 'text.labelsize': ('font.smallsize', '0.8.0'), - 'text.titlesize': ('font.largesize', '0.8.0'), - 'alpha': ('axes.alpha', '0.8.0'), - 'facecolor': ('axes.facecolor', '0.8.0'), - 'edgecolor': ('meta.color', '0.8.0'), - 'color': ('meta.color', '0.8.0'), - 'linewidth': ('meta.width', '0.8.0'), - 'lut': ('cmap.lut', '0.8.0'), - 'image.levels': ('cmap.levels', '0.8.0'), - 'image.inbounds': ('cmap.inbounds', '0.8.0'), - 'image.discrete': ('cmap.discrete', '0.8.0'), - 'image.edgefix': ('edgefix', '0.8.0'), - 'tick.ratio': ('tick.widthratio', '0.8.0'), - 'grid.ratio': ('grid.widthratio', '0.8.0'), - 'abc.style': ('abc', '0.8.0'), - 'grid.loninline': ('grid.inlinelabels', '0.8.0'), - 'grid.latinline': ('grid.inlinelabels', '0.8.0'), - 'cmap.edgefix': ('edgefix', '0.9.0'), - 'basemap': ('geo.backend', '0.10.0'), - 'inlinefmt': ('inlineformat', '0.10.0'), - 'cartopy.circular': ('geo.round', '0.10.0'), - 'cartopy.autoextent': ('geo.extent', '0.10.0'), - 'colorbar.rasterize': ('colorbar.rasterized', '0.10.0'), + "abc.format": ("abc", "0.5.0"), + "align": ("subplots.align", "0.6.0"), + "axes.facealpha": ("axes.alpha", "0.6.0"), + "geoaxes.edgecolor": ("axes.edgecolor", "0.6.0"), + "geoaxes.facealpha": ("axes.alpha", "0.6.0"), + "geoaxes.facecolor": ("axes.facecolor", "0.6.0"), + "geoaxes.linewidth": ("axes.linewidth", "0.6.0"), + "geogrid.alpha": ("grid.alpha", "0.6.0"), + "geogrid.color": ("grid.color", "0.6.0"), + "geogrid.labels": ("grid.labels", "0.6.0"), + "geogrid.labelpad": ("grid.pad", "0.6.0"), + "geogrid.labelsize": ("grid.labelsize", "0.6.0"), + "geogrid.linestyle": ("grid.linestyle", "0.6.0"), + "geogrid.linewidth": ("grid.linewidth", "0.6.0"), + "share": ("subplots.share", "0.6.0"), + "small": ("font.smallsize", "0.6.0"), + "large": ("font.largesize", "0.6.0"), + "span": ("subplots.span", "0.6.0"), + "tight": ("subplots.tight", "0.6.0"), + "axes.formatter.timerotation": ("formatter.timerotation", "0.6.0"), + "axes.formatter.zerotrim": ("formatter.zerotrim", "0.6.0"), + "abovetop": ("title.above", "0.7.0"), + "subplots.pad": ("subplots.outerpad", "0.7.0"), + "subplots.axpad": ("subplots.innerpad", "0.7.0"), + "subplots.axwidth": ("subplots.refwidth", "0.7.0"), + "text.labelsize": ("font.smallsize", "0.8.0"), + "text.titlesize": ("font.largesize", "0.8.0"), + "alpha": ("axes.alpha", "0.8.0"), + "facecolor": ("axes.facecolor", "0.8.0"), + "edgecolor": ("meta.color", "0.8.0"), + "color": ("meta.color", "0.8.0"), + "linewidth": ("meta.width", "0.8.0"), + "lut": ("cmap.lut", "0.8.0"), + "image.levels": ("cmap.levels", "0.8.0"), + "image.inbounds": ("cmap.inbounds", "0.8.0"), + "image.discrete": ("cmap.discrete", "0.8.0"), + "image.edgefix": ("edgefix", "0.8.0"), + "tick.ratio": ("tick.widthratio", "0.8.0"), + "grid.ratio": ("grid.widthratio", "0.8.0"), + "abc.style": ("abc", "0.8.0"), + "grid.loninline": ("grid.inlinelabels", "0.8.0"), + "grid.latinline": ("grid.inlinelabels", "0.8.0"), + "cmap.edgefix": ("edgefix", "0.9.0"), + "basemap": ("geo.backend", "0.10.0"), + "inlinefmt": ("inlineformat", "0.10.0"), + "cartopy.circular": ("geo.round", "0.10.0"), + "cartopy.autoextent": ("geo.extent", "0.10.0"), + "colorbar.rasterize": ("colorbar.rasterized", "0.10.0"), } # Validate the default settings dictionaries using a custom proplot _RcParams # and the original matplotlib RcParams. Also surreptitiously add proplot # font settings to the font keys list (beoolean below always evalutes to True) # font keys list whlie initializing. -_rc_proplot_default = { - key: value for key, (value, _, _) in _rc_proplot_table.items() -} +_rc_proplot_default = {key: value for key, (value, _, _) in _rc_proplot_table.items()} _rc_proplot_validate = { - key: validator for key, (_, validator, _) in _rc_proplot_table.items() + key: validator + for key, (_, validator, _) in _rc_proplot_table.items() if not (validator is _validate_fontsize and FONT_KEYS.add(key)) } _rc_proplot_default = _RcParams(_rc_proplot_default, _rc_proplot_validate) @@ -2067,13 +1921,13 @@ def copy(self): # Important joint matplotlib proplot constants # NOTE: The 'nodots' dictionary should include removed and renamed settings _rc_categories = { - '.'.join(name.split('.')[:i + 1]) + ".".join(name.split(".")[: i + 1]) for dict_ in (_rc_proplot_default, _rc_matplotlib_native) for name in dict_ - for i in range(len(name.split('.')) - 1) + for i in range(len(name.split(".")) - 1) } _rc_nodots = { - name.replace('.', ''): name + name.replace(".", ""): name for dict_ in (_rc_proplot_default, _rc_matplotlib_native, _rc_renamed, _rc_removed) for name in dict_.keys() } diff --git a/proplot/internals/versions.py b/proplot/internals/versions.py index dfeec28d1..ce32f869f 100644 --- a/proplot/internals/versions.py +++ b/proplot/internals/versions.py @@ -11,20 +11,21 @@ class _version(list): Casual parser for ``major.minor`` style version strings. We do not want to add a 'packaging' dependency and only care about major and minor tags. """ + def __str__(self): return self._version def __repr__(self): - return f'version({self._version})' + return f"version({self._version})" def __init__(self, version): try: - major, minor, *_ = version.split('.') + major, minor, *_ = version.split(".") major, minor = int(major or 0), int(minor or 0) except Exception: - warnings._warn_proplot(f'Unexpected version {version!r}. Using 0.0.0.') + warnings._warn_proplot(f"Unexpected version {version!r}. Using 0.0.0.") major = minor = 0 - self._version = f'{major}.{minor}' + self._version = f"{major}.{minor}" super().__init__((major, minor)) # then use builtin python list sorting def __eq__(self, other): @@ -48,12 +49,13 @@ def __le__(self, other): # Matplotlib version import matplotlib # isort:skip + _version_mpl = _version(matplotlib.__version__) # Cartopy version try: import cartopy except ImportError: - _version_cartopy = _version('0.0.0') + _version_cartopy = _version("0.0.0") else: _version_cartopy = _version(cartopy.__version__) diff --git a/proplot/internals/warnings.py b/proplot/internals/warnings.py index 577800460..1d7ee9d4f 100644 --- a/proplot/internals/warnings.py +++ b/proplot/internals/warnings.py @@ -10,28 +10,31 @@ from . import ic # noqa: F401 # Internal modules omitted from warning message -REGEX_INTERNAL = re.compile(r'\A(matplotlib|mpl_toolkits|proplot)\.') +REGEX_INTERNAL = re.compile(r"\A(matplotlib|mpl_toolkits|proplot)\.") # Trivial warning class meant only to communicate the source of the warning -ProplotWarning = type('ProplotWarning', (UserWarning,), {}) +ProplotWarning = type("ProplotWarning", (UserWarning,), {}) # Add due to overwriting the module name catch_warnings = warnings.catch_warnings simplefilter = warnings.simplefilter -def _next_release(): +def next_release(): """ - Message indicating the next major release. + message indicating the next major release. """ from .. import __version__ + try: - num = int(__version__[0]) + 1 - except TypeError: - string = 'the next major release' + # Find the first digit in the version string + version_start = next(i for i, c in enumerate(__version__) if c.isdigit()) + num = int(__version__[version_start]) + 1 + except (StopIteration, ValueError, TypeError): + string = "the next major release" else: - which = 'first' if num == 1 else 'next' - string = f'the {which} major release (version {num}.0.0)' + which = "first" if num == 1 else "next" + string = f"the {which} major release (version {num}.0.0)" return string @@ -43,7 +46,7 @@ def _warn_proplot(message): frame = sys._getframe() stacklevel = 1 while frame is not None: - if not REGEX_INTERNAL.match(frame.f_globals.get('__name__', '')): + if not REGEX_INTERNAL.match(frame.f_globals.get("__name__", "")): break # this is the first external frame frame = frame.f_back stacklevel += 1 @@ -60,24 +63,28 @@ def _rename_objs(version, **kwargs): for old_name, new_obj in kwargs.items(): new_name = new_obj.__name__ message = ( - f'{old_name!r} was deprecated in version {version} and may be ' - f'removed in {_next_release()}. Please use {new_name!r} instead.' + f"{old_name!r} was deprecated in version {version} and may be " + f"removed in {next_release()}. Please use {new_name!r} instead." ) if isinstance(new_obj, type): + class _deprecated_class(new_obj): def __init__(self, *args, new_obj=new_obj, message=message, **kwargs): _warn_proplot(message) super().__init__(*args, **kwargs) + _deprecated_class.__name__ = old_name objs.append(_deprecated_class) elif callable(new_obj): + def _deprecated_function(*args, new_obj=new_obj, message=message, **kwargs): _warn_proplot(message) return new_obj(*args, **kwargs) + _deprecated_function.__name__ = old_name objs.append(_deprecated_function) else: - raise ValueError(f'Invalid deprecated object replacement {new_obj!r}.') + raise ValueError(f"Invalid deprecated object replacement {new_obj!r}.") if len(objs) == 1: return objs[0] else: @@ -90,6 +97,7 @@ def _rename_kwargs(version, **kwargs_rename): Each key should be an old keyword, and each argument should be the new keyword or *instructions* for what to use instead. """ + def _decorator(func_orig): @functools.wraps(func_orig) def _deprecate_kwargs_wrapper(*args, **kwargs): @@ -100,13 +108,15 @@ def _deprecate_kwargs_wrapper(*args, **kwargs): if key_new.isidentifier(): # Rename argument kwargs[key_new] = value - elif '{}' in key_new: + elif "{}" in key_new: # Nice warning message, but user's desired behavior fails key_new = key_new.format(value) _warn_proplot( - f'Keyword {key_old!r} was deprecated in version {version} and may ' - f'be removed in {_next_release()}. Please use {key_new!r} instead.' + f"Keyword {key_old!r} was deprecated in version {version} and may " + f"be removed in {next_release()}. Please use {key_new!r} instead." ) return func_orig(*args, **kwargs) + return _deprecate_kwargs_wrapper + return _decorator diff --git a/proplot/proj.py b/proplot/proj.py index a7a33636c..034f2f32e 100644 --- a/proplot/proj.py +++ b/proplot/proj.py @@ -21,16 +21,16 @@ _WarpedRectangularProjection = NorthPolarStereo = SouthPolarStereo = object __all__ = [ - 'Aitoff', - 'Hammer', - 'KavrayskiyVII', - 'WinkelTripel', - 'NorthPolarAzimuthalEquidistant', - 'SouthPolarAzimuthalEquidistant', - 'NorthPolarGnomonic', - 'SouthPolarGnomonic', - 'NorthPolarLambertAzimuthalEqualArea', - 'SouthPolarLambertAzimuthalEqualArea', + "Aitoff", + "Hammer", + "KavrayskiyVII", + "WinkelTripel", + "NorthPolarAzimuthalEquidistant", + "SouthPolarAzimuthalEquidistant", + "NorthPolarGnomonic", + "SouthPolarGnomonic", + "NorthPolarLambertAzimuthalEqualArea", + "SouthPolarLambertAzimuthalEqualArea", ] @@ -49,26 +49,27 @@ globe : `~cartopy.crs.Globe`, optional If omitted, a default globe is created. """ -docstring._snippet_manager['proj.reso'] = _reso_docstring -docstring._snippet_manager['proj.init'] = _init_docstring +docstring._snippet_manager["proj.reso"] = _reso_docstring +docstring._snippet_manager["proj.init"] = _init_docstring class Aitoff(_WarpedRectangularProjection): """ The `Aitoff `__ projection. """ + #: Registered projection name. - name = 'aitoff' + name = "aitoff" @docstring._snippet_manager def __init__( - self, central_longitude=0, globe=None, - false_easting=None, false_northing=None + self, central_longitude=0, globe=None, false_easting=None, false_northing=None ): """ %(proj.init)s """ from cartopy.crs import WGS84_SEMIMAJOR_AXIS, Globe + if globe is None: globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) @@ -76,15 +77,16 @@ def __init__( b = globe.semiminor_axis or a if b != a or globe.ellipse is not None: warnings.warn( - f'The {self.name!r} projection does not handle elliptical globes.' + f"The {self.name!r} projection does not handle elliptical globes." ) - proj4_params = {'proj': 'aitoff', 'lon_0': central_longitude} + proj4_params = {"proj": "aitoff", "lon_0": central_longitude} super().__init__( - proj4_params, central_longitude, + proj4_params, + central_longitude, false_easting=false_easting, false_northing=false_northing, - globe=globe + globe=globe, ) @docstring._snippet_manager @@ -100,18 +102,19 @@ class Hammer(_WarpedRectangularProjection): """ The `Hammer `__ projection. """ + #: Registered projection name. - name = 'hammer' + name = "hammer" @docstring._snippet_manager def __init__( - self, central_longitude=0, globe=None, - false_easting=None, false_northing=None + self, central_longitude=0, globe=None, false_easting=None, false_northing=None ): """ %(proj.init)s """ from cartopy.crs import WGS84_SEMIMAJOR_AXIS, Globe + if globe is None: globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) @@ -119,15 +122,16 @@ def __init__( b = globe.semiminor_axis or a if b != a or globe.ellipse is not None: warnings.warn( - f'The {self.name!r} projection does not handle elliptical globes.' + f"The {self.name!r} projection does not handle elliptical globes." ) - proj4_params = {'proj': 'hammer', 'lon_0': central_longitude} + proj4_params = {"proj": "hammer", "lon_0": central_longitude} super().__init__( - proj4_params, central_longitude, + proj4_params, + central_longitude, false_easting=false_easting, false_northing=false_northing, - globe=globe + globe=globe, ) @docstring._snippet_manager @@ -144,18 +148,19 @@ class KavrayskiyVII(_WarpedRectangularProjection): The `Kavrayskiy VII \ `__ projection. """ + #: Registered projection name. - name = 'kavrayskiyVII' + name = "kavrayskiyVII" @docstring._snippet_manager def __init__( - self, central_longitude=0, globe=None, - false_easting=None, false_northing=None + self, central_longitude=0, globe=None, false_easting=None, false_northing=None ): """ %(proj.init)s """ from cartopy.crs import WGS84_SEMIMAJOR_AXIS, Globe + if globe is None: globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) @@ -163,15 +168,16 @@ def __init__( b = globe.semiminor_axis or a if b != a or globe.ellipse is not None: warnings.warn( - f'The {self.name!r} projection does not handle elliptical globes.' + f"The {self.name!r} projection does not handle elliptical globes." ) - proj4_params = {'proj': 'kav7', 'lon_0': central_longitude} + proj4_params = {"proj": "kav7", "lon_0": central_longitude} super().__init__( - proj4_params, central_longitude, + proj4_params, + central_longitude, false_easting=false_easting, false_northing=false_northing, - globe=globe + globe=globe, ) @docstring._snippet_manager @@ -188,18 +194,19 @@ class WinkelTripel(_WarpedRectangularProjection): The `Winkel tripel (Winkel III) \ `__ projection. """ + #: Registered projection name. - name = 'winkeltripel' + name = "winkeltripel" @docstring._snippet_manager def __init__( - self, central_longitude=0, globe=None, - false_easting=None, false_northing=None + self, central_longitude=0, globe=None, false_easting=None, false_northing=None ): """ %(proj.init)s """ from cartopy.crs import WGS84_SEMIMAJOR_AXIS, Globe + if globe is None: globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) @@ -207,16 +214,16 @@ def __init__( b = globe.semiminor_axis or a if b != a or globe.ellipse is not None: warnings.warn( - f'The {self.name!r} projection does not handle ' - 'elliptical globes.' + f"The {self.name!r} projection does not handle " "elliptical globes." ) - proj4_params = {'proj': 'wintri', 'lon_0': central_longitude} + proj4_params = {"proj": "wintri", "lon_0": central_longitude} super().__init__( - proj4_params, central_longitude, + proj4_params, + central_longitude, false_easting=false_easting, false_northing=false_northing, - globe=globe + globe=globe, ) @docstring._snippet_manager @@ -232,14 +239,14 @@ class NorthPolarAzimuthalEquidistant(AzimuthalEquidistant): """ Analogous to `~cartopy.crs.NorthPolarStereo`. """ + @docstring._snippet_manager def __init__(self, central_longitude=0.0, globe=None): """ %(proj.init)s """ super().__init__( - central_latitude=90, - central_longitude=central_longitude, globe=globe + central_latitude=90, central_longitude=central_longitude, globe=globe ) @@ -247,14 +254,14 @@ class SouthPolarAzimuthalEquidistant(AzimuthalEquidistant): """ Analogous to `~cartopy.crs.SouthPolarStereo`. """ + @docstring._snippet_manager def __init__(self, central_longitude=0.0, globe=None): """ %(proj.init)s """ super().__init__( - central_latitude=-90, - central_longitude=central_longitude, globe=globe + central_latitude=-90, central_longitude=central_longitude, globe=globe ) @@ -262,14 +269,14 @@ class NorthPolarLambertAzimuthalEqualArea(LambertAzimuthalEqualArea): """ Analogous to `~cartopy.crs.NorthPolarStereo`. """ + @docstring._snippet_manager def __init__(self, central_longitude=0.0, globe=None): """ %(proj.init)s """ super().__init__( - central_latitude=90, - central_longitude=central_longitude, globe=globe + central_latitude=90, central_longitude=central_longitude, globe=globe ) @@ -277,14 +284,14 @@ class SouthPolarLambertAzimuthalEqualArea(LambertAzimuthalEqualArea): """ Analogous to `~cartopy.crs.SouthPolarStereo`. """ + @docstring._snippet_manager def __init__(self, central_longitude=0.0, globe=None): """ %(proj.init)s """ super().__init__( - central_latitude=-90, - central_longitude=central_longitude, globe=globe + central_latitude=-90, central_longitude=central_longitude, globe=globe ) @@ -292,14 +299,14 @@ class NorthPolarGnomonic(Gnomonic): """ Analogous to `~cartopy.crs.NorthPolarStereo`. """ + @docstring._snippet_manager def __init__(self, central_longitude=0.0, globe=None): """ %(proj.init)s """ super().__init__( - central_latitude=90, - central_longitude=central_longitude, globe=globe + central_latitude=90, central_longitude=central_longitude, globe=globe ) @@ -307,12 +314,12 @@ class SouthPolarGnomonic(Gnomonic): """ Analogous to `~cartopy.crs.SouthPolarStereo`. """ + @docstring._snippet_manager def __init__(self, central_longitude=0.0, globe=None): """ %(proj.init)s """ super().__init__( - central_latitude=-90, - central_longitude=central_longitude, globe=globe + central_latitude=-90, central_longitude=central_longitude, globe=globe ) diff --git a/proplot/scale.py b/proplot/scale.py index 7e5a0ef6e..4168cede0 100644 --- a/proplot/scale.py +++ b/proplot/scale.py @@ -15,17 +15,17 @@ from .internals import _not_none, _version_mpl, warnings __all__ = [ - 'CutoffScale', - 'ExpScale', - 'FuncScale', - 'InverseScale', - 'LinearScale', - 'LogitScale', - 'LogScale', - 'MercatorLatitudeScale', - 'PowerScale', - 'SineLatitudeScale', - 'SymmetricalLogScale', + "CutoffScale", + "ExpScale", + "FuncScale", + "InverseScale", + "LinearScale", + "LogitScale", + "LogScale", + "MercatorLatitudeScale", + "PowerScale", + "SineLatitudeScale", + "SymmetricalLogScale", ] @@ -38,13 +38,13 @@ def _parse_logscale_args(*keys, **kwargs): # NOTE: Scale classes ignore unused arguments with warnings, but matplotlib 3.3 # version changes the keyword args. Since we can't do a try except clause, only # way to avoid warnings with 3.3 upgrade is to test version string. - kwsuffix = '' if _version_mpl >= '3.3' else 'x' + kwsuffix = "" if _version_mpl >= "3.3" else "x" for key in keys: # Remove duplicates opts = { key: kwargs.pop(key, None), - key + 'x': kwargs.pop(key + 'x', None), - key + 'y': kwargs.pop(key + 'y', None), + key + "x": kwargs.pop(key + "x", None), + key + "y": kwargs.pop(key + "y", None), } value = _not_none(**opts) # issues warning if multiple values passed @@ -53,10 +53,10 @@ def _parse_logscale_args(*keys, **kwargs): # up with additional log-locator step inside the threshold, e.g. major # ticks on -10, -1, -0.1, 0.1, 1, 10 for linthresh of 1. Adding slight # offset to *desired* linthresh prevents this. - if key == 'subs': + if key == "subs": if value is None: value = np.arange(1, 10) - if key == 'linthresh': + if key == "linthresh": if value is None: value = 1 power = np.log10(value) @@ -76,9 +76,10 @@ class _Scale(object): `__init__` so you no longer have to instantiate scales with an `~matplotlib.axis.Axis` instance. """ + def __init__(self, *args, **kwargs): # Pass a dummy axis to the superclass - axis = type('Axis', (object,), {'axis_name': 'x'})() + axis = type("Axis", (object,), {"axis_name": "x"})() super().__init__(axis, *args, **kwargs) self._default_major_locator = mticker.AutoLocator() self._default_minor_locator = mticker.AutoMinorLocator() @@ -103,13 +104,14 @@ def set_default_locators_and_formatters(self, axis, only_if_default=False): # NOTE: We set isDefault_minloc to True when simply toggling minor ticks # on and off with CartesianAxes format command. from .config import rc + if not only_if_default or axis.isDefault_majloc: locator = copy.copy(self._default_major_locator) axis.set_major_locator(locator) axis.isDefault_majloc = True if not only_if_default or axis.isDefault_minloc: - x = axis.axis_name if axis.axis_name in 'xy' else 'x' - if rc[x + 'tick.minor.visible']: + x = axis.axis_name if axis.axis_name in "xy" else "x" + if rc[x + "tick.minor.visible"]: locator = copy.copy(self._default_minor_locator) else: locator = mticker.NullLocator() @@ -136,8 +138,9 @@ class LinearScale(_Scale, mscale.LinearScale): As with `~matplotlib.scale.LinearScale` but with `~proplot.ticker.AutoFormatter` as the default major formatter. """ + #: The registered scale name - name = 'linear' + name = "linear" def __init__(self, **kwargs): """ @@ -154,8 +157,9 @@ class LogitScale(_Scale, mscale.LogitScale): As with `~matplotlib.scale.LogitScale` but with `~proplot.ticker.AutoFormatter` as the default major formatter. """ + #: The registered scale name - name = 'logit' + name = "logit" def __init__(self, **kwargs): """ @@ -181,8 +185,9 @@ class LogScale(_Scale, mscale.LogScale): as the default major formatter. ``x`` and ``y`` versions of each keyword argument are no longer required. """ + #: The registered scale name - name = 'log' + name = "log" def __init__(self, **kwargs): """ @@ -205,7 +210,7 @@ def __init__(self, **kwargs): -------- proplot.constructor.Scale """ - keys = ('base', 'nonpos', 'subs') + keys = ("base", "nonpos", "subs") super().__init__(**_parse_logscale_args(*keys, **kwargs)) self._default_major_locator = mticker.LogLocator(self.base) self._default_minor_locator = mticker.LogLocator(self.base, self.subs) @@ -218,8 +223,9 @@ class SymmetricalLogScale(_Scale, mscale.SymmetricalLogScale): ``x`` and ``y`` versions of each keyword argument are no longer required. """ + #: The registered scale name - name = 'symlog' + name = "symlog" def __init__(self, **kwargs): """ @@ -249,19 +255,22 @@ def __init__(self, **kwargs): -------- proplot.constructor.Scale """ - keys = ('base', 'linthresh', 'linscale', 'subs') + keys = ("base", "linthresh", "linscale", "subs") super().__init__(**_parse_logscale_args(*keys, **kwargs)) transform = self.get_transform() self._default_major_locator = mticker.SymmetricalLogLocator(transform) - self._default_minor_locator = mticker.SymmetricalLogLocator(transform, self.subs) # noqa: E501 + self._default_minor_locator = mticker.SymmetricalLogLocator( + transform, self.subs + ) # noqa: E501 class FuncScale(_Scale, mscale.ScaleBase): """ Axis scale composed of arbitrary forward and inverse transformations. """ + #: The registered scale name - name = 'function' + name = "function" def __init__(self, transform=None, invert=False, parent_scale=None, **kwargs): """ @@ -314,25 +323,30 @@ def __init__(self, transform=None, invert=False, parent_scale=None, **kwargs): # NOTE: Permit *arbitrary* parent axis scales and infer default locators and # formatters from the input scale (if it was passed) or the parent scale. Use # case for latter is e.g. logarithmic scale with linear transformation. - if 'functions' in kwargs: # matplotlib compatibility (critical for >= 3.5) - functions = kwargs.pop('functions', None) + if "functions" in kwargs: # matplotlib compatibility (critical for >= 3.5) + functions = kwargs.pop("functions", None) if transform is None: transform = functions else: warnings._warn_proplot("Ignoring keyword argument 'functions'.") from .constructor import Formatter, Locator, Scale + super().__init__() if callable(transform): forward, inverse, inherit_scale = transform, transform, None - elif np.iterable(transform) and len(transform) == 2 and all(map(callable, transform)): # noqa: E501 + elif ( + np.iterable(transform) + and len(transform) == 2 + and all(map(callable, transform)) + ): # noqa: E501 forward, inverse, inherit_scale = *transform, None else: try: inherit_scale = Scale(transform) except ValueError: raise ValueError( - 'Expected a function, 2-tuple of forward and inverse functions, ' - f'or an axis scale specification. Got {transform!r}.' + "Expected a function, 2-tuple of forward and inverse functions, " + f"or an axis scale specification. Got {transform!r}." ) transform = inherit_scale.get_transform() forward, inverse = transform.transform, transform.inverted().transform @@ -345,15 +359,15 @@ def __init__(self, transform=None, invert=False, parent_scale=None, **kwargs): forward, inverse = inverse, forward parent_scale = _not_none(parent_scale, LinearScale()) if not isinstance(parent_scale, mscale.ScaleBase): - raise ValueError(f'Parent scale must be ScaleBase. Got {parent_scale!r}.') + raise ValueError(f"Parent scale must be ScaleBase. Got {parent_scale!r}.") if isinstance(parent_scale, CutoffScale): args = list(parent_scale.args) # mutable copy args[::2] = (inverse(arg) for arg in args[::2]) # transform cutoffs parent_scale = CutoffScale(*args) if isinstance(parent_scale, mscale.SymmetricalLogScale): - keys = ('base', 'linthresh', 'linscale', 'subs') + keys = ("base", "linthresh", "linscale", "subs") kwsym = {key: getattr(parent_scale, key) for key in keys} - kwsym['linthresh'] = inverse(kwsym['linthresh']) + kwsym["linthresh"] = inverse(kwsym["linthresh"]) parent_scale = SymmetricalLogScale(**kwsym) self.functions = (forward, inverse) self._transform = parent_scale.get_transform() + FuncTransform(forward, inverse) @@ -361,10 +375,10 @@ def __init__(self, transform=None, invert=False, parent_scale=None, **kwargs): # Apply default locators and formatters # NOTE: We pass these through contructor functions scale = inherit_scale or parent_scale - for which in ('major', 'minor'): - for type_, parser in (('locator', Locator), ('formatter', Formatter)): - key = which + '_' + type_ - attr = '_default_' + key + for which in ("major", "minor"): + for type_, parser in (("locator", Locator), ("formatter", Formatter)): + key = which + "_" + type_ + attr = "_default_" + key ticker = kwargs.pop(key, None) if ticker is None: ticker = getattr(scale, attr, None) @@ -373,7 +387,7 @@ def __init__(self, transform=None, invert=False, parent_scale=None, **kwargs): ticker = parser(ticker) setattr(self, attr, copy.copy(ticker)) if kwargs: - raise TypeError(f'FuncScale got unexpected arguments: {kwargs}') + raise TypeError(f"FuncScale got unexpected arguments: {kwargs}") class FuncTransform(mtransforms.Transform): @@ -388,13 +402,13 @@ def __init__(self, forward, inverse): self._forward = forward self._inverse = inverse else: - raise ValueError('arguments to FuncTransform must be functions') + raise ValueError("arguments to FuncTransform must be functions") def inverted(self): return FuncTransform(self._inverse, self._forward) def transform_non_affine(self, values): - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): return self._forward(values) @@ -407,8 +421,9 @@ class PowerScale(_Scale, mscale.ScaleBase): x^{c} """ + #: The registered scale name - name = 'power' + name = "power" def __init__(self, power=1, inverse=False): """ @@ -451,7 +466,7 @@ def inverted(self): return InvertedPowerTransform(self._power) def transform_non_affine(self, a): - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): return np.power(a, self._power) @@ -469,7 +484,7 @@ def inverted(self): return PowerTransform(self._power) def transform_non_affine(self, a): - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): return np.power(a, 1 / self._power) @@ -493,8 +508,9 @@ class ExpScale(_Scale, mscale.ScaleBase): which in appearance is equivalent to `LogScale` since it is just a linear transformation of the logarithm. """ + #: The registered scale name - name = 'exp' + name = "exp" def __init__(self, a=np.e, b=1, c=1, inverse=False): """ @@ -547,7 +563,7 @@ def inverted(self): return InvertedExpTransform(self._a, self._b, self._c) def transform_non_affine(self, a): - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): return self._c * np.power(self._a, self._b * np.array(a)) @@ -567,7 +583,7 @@ def inverted(self): return ExpTransform(self._a, self._b, self._c) def transform_non_affine(self, a): - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): return np.log(a / self._c) / (self._b * np.log(self._a)) @@ -589,8 +605,9 @@ class MercatorLatitudeScale(_Scale, mscale.ScaleBase): x = 180\\,\\arctan(\\sinh(y)) \\,/\\, \\pi """ + #: The registered scale name - name = 'mercator' + name = "mercator" def __init__(self, thresh=85.0): """ @@ -609,7 +626,7 @@ def __init__(self, thresh=85.0): raise ValueError("Mercator scale 'thresh' must be <= 90.") self._thresh = thresh self._transform = MercatorLatitudeTransform(thresh) - self._default_major_formatter = pticker.AutoFormatter(suffix='\N{DEGREE SIGN}') + self._default_major_formatter = pticker.AutoFormatter(suffix="\N{DEGREE SIGN}") def limit_range_for_scale(self, vmin, vmax, minpos): # noqa: U100 """ @@ -637,7 +654,7 @@ def transform_non_affine(self, a): # in limit_range_for_scale or get weird duplicate tick labels. This # is not necessary for positive-only scales because it is harder to # run up right against the scale boundaries. - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): m = ma.masked_where((a <= -90) | (a >= 90), a) if m.mask.any(): m = np.deg2rad(m) @@ -661,7 +678,7 @@ def inverted(self): return MercatorLatitudeTransform(self._thresh) def transform_non_affine(self, a): - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): return np.rad2deg(np.arctan2(1, np.sinh(a))) @@ -681,8 +698,9 @@ class SineLatitudeScale(_Scale, mscale.ScaleBase): x = 180\arcsin(y)/\pi """ + #: The registered scale name - name = 'sine' + name = "sine" def __init__(self): """ @@ -692,7 +710,7 @@ def __init__(self): """ super().__init__() self._transform = SineLatitudeTransform() - self._default_major_formatter = pticker.AutoFormatter(suffix='\N{DEGREE SIGN}') + self._default_major_formatter = pticker.AutoFormatter(suffix="\N{DEGREE SIGN}") def limit_range_for_scale(self, vmin, vmax, minpos): # noqa: U100 """ @@ -719,7 +737,7 @@ def transform_non_affine(self, a): # in limit_range_for_scale or get weird duplicate tick labels. This # is not necessary for positive-only scales because it is harder to # run up right against the scale boundaries. - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): m = ma.masked_where((a < -90) | (a > 90), a) if m.mask.any(): return ma.sin(np.deg2rad(m)) @@ -740,7 +758,7 @@ def inverted(self): return SineLatitudeTransform() def transform_non_affine(self, a): - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): return np.rad2deg(np.arcsin(a)) @@ -750,8 +768,9 @@ class CutoffScale(_Scale, mscale.ScaleBase): The axis can undergo discrete jumps, "accelerations", or "decelerations" between successive thresholds. """ + #: The registered scale name - name = 'cutoff' + name = "cutoff" def __init__(self, *args): """ @@ -810,21 +829,21 @@ def __init__(self, threshs, scales, zero_dists=None): scales = np.asarray(scales) threshs = np.asarray(threshs) if len(scales) != len(threshs): - raise ValueError(f'Got {len(threshs)} but {len(scales)} scales.') + raise ValueError(f"Got {len(threshs)} but {len(scales)} scales.") if any(scales < 0): - raise ValueError('Scales must be non negative.') + raise ValueError("Scales must be non negative.") if scales[-1] in (0, np.inf): - raise ValueError('Final scale must be finite.') + raise ValueError("Final scale must be finite.") if any(dists < 0): - raise ValueError('Thresholds must be monotonically increasing.') + raise ValueError("Thresholds must be monotonically increasing.") if any((dists == 0) | (scales == 0)): if zero_dists is None: - raise ValueError('Keyword zero_dists is required for discrete steps.') + raise ValueError("Keyword zero_dists is required for discrete steps.") if any((dists == 0) != (scales == 0)): - raise ValueError('Input scales disagree with discrete step locations.') + raise ValueError("Input scales disagree with discrete step locations.") self._scales = scales self._threshs = threshs - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): dists = np.concatenate((threshs[:1], dists / scales[:-1])) if zero_dists is not None: dists[scales[:-1] == 0] = zero_dists @@ -833,7 +852,7 @@ def __init__(self, threshs, scales, zero_dists=None): def inverted(self): # Use same algorithm for inversion! threshs = np.cumsum(self._dists) # thresholds in transformed space - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): scales = 1.0 / self._scales # new scales are inverse zero_dists = np.diff(self._threshs)[scales[:-1] == 0] return CutoffTransform(threshs, scales, zero_dists=zero_dists) @@ -845,7 +864,7 @@ def transform_non_affine(self, a): scales = self._scales threshs = self._threshs aa = np.array(a) # copy - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): for i, ai in np.ndenumerate(a): j = np.searchsorted(threshs, ai) if j > 0: @@ -863,8 +882,9 @@ class InverseScale(_Scale, mscale.ScaleBase): y = x^{-1} """ + #: The registered scale name - name = 'inverse' + name = "inverse" def __init__(self): """ @@ -906,7 +926,7 @@ def inverted(self): return InverseTransform() def transform_non_affine(self, a): - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): return 1.0 / a @@ -926,14 +946,15 @@ def _scale_factory(scale, axis, *args, **kwargs): # noqa: U100 mapping = mscale._scale_mapping if isinstance(scale, mscale.ScaleBase): if args or kwargs: - warnings._warn_proplot(f'Ignoring args {args} and keyword args {kwargs}.') + warnings._warn_proplot(f"Ignoring args {args} and keyword args {kwargs}.") return scale # do nothing else: scale = scale.lower() if scale not in mapping: raise ValueError( - f'Unknown axis scale {scale!r}. Options are ' - + ', '.join(map(repr, mapping)) + '.' + f"Unknown axis scale {scale!r}. Options are " + + ", ".join(map(repr, mapping)) + + "." ) return mapping[scale](*args, **kwargs) diff --git a/proplot/tests/baseline/test_colorbar.png b/proplot/tests/baseline/test_colorbar.png new file mode 100644 index 000000000..c5ce770f9 Binary files /dev/null and b/proplot/tests/baseline/test_colorbar.png differ diff --git a/proplot/tests/baseline/test_inbounds_data.png b/proplot/tests/baseline/test_inbounds_data.png new file mode 100644 index 000000000..ea95650fd Binary files /dev/null and b/proplot/tests/baseline/test_inbounds_data.png differ diff --git a/proplot/tests/baseline/test_inset_basic.png b/proplot/tests/baseline/test_inset_basic.png new file mode 100644 index 000000000..a2036b2b4 Binary files /dev/null and b/proplot/tests/baseline/test_inset_basic.png differ diff --git a/proplot/tests/baseline/test_panel_dist.png b/proplot/tests/baseline/test_panel_dist.png new file mode 100644 index 000000000..34dda7b59 Binary files /dev/null and b/proplot/tests/baseline/test_panel_dist.png differ diff --git a/proplot/tests/baseline/test_standardized_input.png b/proplot/tests/baseline/test_standardized_input.png new file mode 100644 index 000000000..2f6be9b85 Binary files /dev/null and b/proplot/tests/baseline/test_standardized_input.png differ diff --git a/proplot/tests/baseline/test_statistical_boxplot.png b/proplot/tests/baseline/test_statistical_boxplot.png new file mode 100644 index 000000000..c586bf0a5 Binary files /dev/null and b/proplot/tests/baseline/test_statistical_boxplot.png differ diff --git a/proplot/tests/test_1dplots.py b/proplot/tests/test_1dplots.py new file mode 100644 index 000000000..ed7e298a0 --- /dev/null +++ b/proplot/tests/test_1dplots.py @@ -0,0 +1,373 @@ +#!/usr/bin/env python3 +""" +Test 1D plotting overrides. +""" +import numpy as np +import numpy.ma as ma +import pandas as pd + +import proplot as pplt +import pytest + +state = np.random.RandomState(51423) + + +@pytest.mark.mpl_image_compare +def test_auto_reverse(): + """ + Test enabled and disabled auto reverse. + """ + x = np.arange(10)[::-1] + y = np.arange(10) + z = state.rand(10, 10) + fig, axs = pplt.subplots(ncols=2, nrows=3, share=0) + # axs[0].format(xreverse=False) # should fail + axs[0].plot(x, y) + axs[1].format(xlim=(0, 9)) # manual override + axs[1].plot(x, y) + axs[2].plotx(x, y) + axs[3].format(ylim=(0, 9)) # manual override + axs[3].plotx(x, y) + axs[4].pcolor(x, y[::-1], z) + axs[5].format(xlim=(0, 9), ylim=(0, 9)) # manual override! + axs[5].pcolor(x, y[::-1], z) + fig.format(suptitle="Auto-reverse test", collabels=["reverse", "fixed"]) + return fig + + +@pytest.mark.mpl_image_compare +def test_cmap_cycles(): + """ + Test sampling of multiple continuous colormaps. + """ + cycle = pplt.Cycle( + "Boreal", + "Grays", + "Fire", + "Glacial", + "yellow", + left=[0.4] * 5, + right=[0.6] * 5, + samples=[3, 4, 5, 2, 1], + ) + fig, ax = pplt.subplots() + data = state.rand(10, len(cycle)).cumsum(axis=1) + data = pd.DataFrame(data, columns=list("abcdefghijklmno")) + ax.plot(data, cycle=cycle, linewidth=2, legend="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_column_iteration(): + """ + Test scatter column iteration. + """ + fig, axs = pplt.subplots(ncols=2) + axs[0].plot(state.rand(5, 5), state.rand(5, 5), lw=5) + axs[1].scatter( + state.rand(5, 5), state.rand(5, 5), state.rand(5, 5), state.rand(5, 5) + ) + return fig + + +@pytest.mark.skip("TODO") +@pytest.mark.mpl_image_compare +def test_bar_stack(): + """ + Test bar and area stacking. + """ + # TODO: Add test here + + +@pytest.mark.mpl_image_compare +def test_bar_width(): + """ + Test relative and absolute widths. + """ + fig, axs = pplt.subplots(ncols=3) + x = np.arange(10) + y = state.rand(10, 2) + for i, ax in enumerate(axs): + ax.bar(x * (2 * i + 1), y, width=0.8, absolute_width=i == 1) + return fig + + +@pytest.mark.mpl_image_compare +def test_bar_vectors(): + """ + Test vector arguments to bar plots. + """ + facecolors = np.repeat(0.1, 3) * np.arange(1, 11)[:, None] + fig, ax = pplt.subplots() + ax.bar( + np.arange(10), + np.arange(1, 11), + linewidth=3, + edgecolor=[f"gray{i}" for i in range(9, -1, -1)], + alpha=np.linspace(0.1, 1, 10), + hatch=[None, "//"] * 5, + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_boxplot_colors(): + """ + Test box colors and cycle colors. + """ + fig = pplt.figure(share=False) + ax = fig.subplot(221) + box_data = state.uniform(-3, 3, size=(1000, 5)) + violin_data = state.normal(0, 1, size=(1000, 5)) + ax.box(box_data, fillcolor=["red", "blue", "green", "orange", "yellow"]) + ax = fig.subplot(222) + ax.violin( + violin_data, + fillcolor=["gray1", "gray7"], + hatches=[None, "//", None, None, "//"], + means=True, + barstds=2, + ) # noqa: E501 + ax = fig.subplot(223) + ax.boxh(box_data, cycle="pastel2") + ax = fig.subplot(224) + ax.violinh(violin_data, cycle="pastel1") + return fig + + +@pytest.mark.mpl_image_compare +def test_boxplot_vectors(): + """ + Test vector property arguments. + """ + coords = (0.5, 1, 2) + counts = (10, 20, 100) + labels = ["foo", "bar", "baz"] + datas = [] + for count in counts: + data = state.rand(count) + datas.append(data) + datas = np.array(datas, dtype=object) + assert len(datas) == len(coords) + fig, ax = pplt.subplot(refwidth=3) + ax.boxplot( + coords, + datas, + lw=2, + notch=False, + whis=(10, 90), + cycle="538", + fillalpha=[0.5, 0.5, 1], + hatch=[None, "//", "**"], + boxlw=[2, 1, 1], + ) + ax.format(xticklabels=labels) + return fig + + +@pytest.mark.mpl_image_compare +def test_histogram_types(): + """ + Test the different histogram types using basic keywords. + """ + fig, axs = pplt.subplots(ncols=2, nrows=2, share=False) + data = state.normal(size=(100, 5)) + data += np.arange(5) + kws = ({"stack": 0}, {"stack": 1}, {"fill": 0}, {"fill": 1, "alpha": 0.5}) + for ax, kw in zip(axs, kws): + ax.hist(data, ec="k", **kw) + return fig + + +@pytest.mark.mpl_image_compare +def test_invalid_plot(): + """ + Test lines with missing or invalid values. + """ + fig, axs = pplt.subplots(ncols=2) + data = state.normal(size=(100, 5)) + for j in range(5): + data[:, j] = np.sort(data[:, j]) + data[: 19 * (j + 1), j] = np.nan + # data[:20, :] = np.nan + data_masked = ma.masked_invalid(data) # should be same result + for ax, dat in zip(axs, (data, data_masked)): + ax.plot(dat, means=True, shade=True) + return fig + + +@pytest.mark.mpl_image_compare +def test_invalid_dist(): + """ + Test distributions with missing or invalid data. + """ + fig, axs = pplt.subplots(ncols=2, nrows=2) + data = state.normal(size=(100, 5)) + for i in range(5): # test uneven numbers of invalid values + data[: 10 * (i + 1), :] = np.nan + data_masked = ma.masked_invalid(data) # should be same result + for ax, dat in zip(axs[:2], (data, data_masked)): + ax.violin(dat, means=True) + for ax, dat in zip(axs[2:], (data, data_masked)): + ax.box(dat, fill=True, means=True) + return fig + + +@pytest.mark.mpl_image_compare +def test_pie_charts(): + """ + Test basic pie plots. No examples in user guide right now. + """ + pplt.rc.inlinefmt = "svg" + labels = ["foo", "bar", "baz", "biff", "buzz"] + array = np.arange(1, 6) + data = pd.Series(array, index=labels) + fig = pplt.figure() + ax = fig.subplot(121) + ax.pie(array, edgefix=True, labels=labels, ec="k", cycle="reds") + ax = fig.subplot(122) + ax.pie(data, ec="k", cycle="blues") + return fig + + +@pytest.mark.mpl_image_compare +def test_parametric_labels(): + """ + Test passing strings as parametric 'color values'. This is likely + a common use case. + """ + pplt.rc.inlinefmt = "svg" + fig, ax = pplt.subplots() + ax.parametric( + state.rand(5), c=list("abcde"), lw=20, colorbar="b", cmap_kw={"left": 0.2} + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_parametric_colors(): + """ + Test color input arguments. Should be able to make monochromatic + plots for case where we want `line` without sticky x/y edges. + """ + fig, axs = pplt.subplots(ncols=2, nrows=2) + colors = ( + [(0, 1, 1), (0, 1, 0), (1, 0, 0), (0, 0, 1), (1, 1, 0)], + ["b", "r", "g", "m", "c", "y"], + "black", + (0.5, 0.5, 0.5), + ) + for ax, color in zip(axs, colors): + ax.parametric( + state.rand(5), + state.rand(5), + linewidth=2, + label="label", + color=color, + colorbar="b", + legend="b", + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_args(): + """ + Test diverse scatter keyword parsing and RGB scaling. + """ + x, y = state.randn(50), state.randn(50) + data = state.rand(50, 3) + fig, axs = pplt.subplots(ncols=4, share=0) + ax = axs[0] + ax.scatter(x, y, s=80, fc="none", edgecolors="r") + ax = axs[1] + ax.scatter(data, c=data, cmap="reds") # column iteration + ax = axs[2] + with pytest.warns(pplt.internals.ProplotWarning) as record: + ax.scatter(data[:, 0], c=data, cmap="reds") # actual colors + assert len(record) == 1 + ax = axs[3] + ax.scatter(data, mean=True, shadestd=1, barstd=0.5) # distribution + ax.format(xlim=(-0.1, 2.1)) + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_inbounds(): + """ + Test in-bounds scatter plots. + """ + fig, axs = pplt.subplots(ncols=2, share=False) + N = 100 + fig.format(xlim=(0, 20)) + for i, ax in enumerate(axs): + c = ax.scatter(np.arange(N), np.arange(N), c=np.arange(N), inbounds=bool(i)) + ax.colorbar(c, loc="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_alpha(): + """ + Test behavior with multiple alpha values. + """ + fig, ax = pplt.subplots() + data = state.rand(10) + alpha = np.linspace(0.1, 1, data.size) + ax.scatter(data, alpha=alpha) + ax.scatter(data + 1, c=np.arange(data.size), cmap="BuRd", alpha=alpha) + ax.scatter(data + 2, color="k", alpha=alpha) + ax.scatter(data + 3, color=[f"red{i}" for i in range(data.size)], alpha=alpha) + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_cycle(): + """ + Test scatter property cycling. + """ + fig, ax = pplt.subplots() + cycle = pplt.Cycle( + "538", marker=["X", "o", "s", "d"], sizes=[20, 100], edgecolors=["r", "k"] + ) + ax.scatter( + state.rand(10, 4), + state.rand(10, 4), + cycle=cycle, + area_size=False, + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_sizes(): + """ + Test marker size scaling. + """ + # Compare setting size to input size + size = 20 + with pplt.rc.context({"lines.markersize": size}): + fig = pplt.figure() + ax = fig.subplot(121, margin=0.15) + for i in range(3): + kw = {"absolute_size": i == 2} + if i == 1: + kw["smin"] = 0 + kw["smax"] = size**2 # should be same as relying on lines.markersize + ax.scatter(np.arange(5), [0.25 * (1 + i)] * 5, size**2, **kw) + # Test various size arguments + ax = fig.subplot(122, margin=0.15) + data = state.rand(5) * 500 + ax.scatter( + np.arange(5), + [0.25] * 5, + c="blue7", + sizes=[5, 10, 15, 20, 25], + area_size=False, + absolute_size=True, + ) + ax.scatter(np.arange(5), [0.50] * 5, c="red7", sizes=data, absolute_size=True) + ax.scatter(np.arange(5), [0.75] * 5, c="red7", sizes=data, absolute_size=False) + for i, d in enumerate(data): + ax.text(i, 0.5, format(d, ".0f"), va="center", ha="center") + return fig diff --git a/proplot/tests/test_2dplots.py b/proplot/tests/test_2dplots.py new file mode 100644 index 000000000..026e47ec1 --- /dev/null +++ b/proplot/tests/test_2dplots.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +""" +Test 2D plotting overrides. +""" +import numpy as np +import pytest +import xarray as xr + +import proplot as pplt + +state = np.random.RandomState(51423) + + +@pytest.mark.skip("not sure what this does") +@pytest.mark.mpl_image_compare +def test_colormap_vcenter(): + """ + Test colormap vcenter. + """ + fig, axs = pplt.subplots(ncols=3) + data = 10 * state.rand(10, 10) - 3 + axs[0].pcolor(data, vcenter=0) + axs[1].pcolor(data, vcenter=1) + axs[2].pcolor(data, vcenter=2) + return fig + + +@pytest.mark.mpl_image_compare +def test_auto_diverging1(): + """ + Test that auto diverging works. + """ + # Test with basic data + fig = pplt.figure() + # fig.format(collabels=('Auto sequential', 'Auto diverging'), suptitle='Default') + ax = fig.subplot(121) + ax.pcolor(state.rand(10, 10) * 5, colorbar="b") + ax = fig.subplot(122) + ax.pcolor(state.rand(10, 10) * 5 - 3.5, colorbar="b") + fig.format(toplabels=("Sequential", "Diverging")) + return fig + + +@pytest.mark.skip("Not sure what this does") +@pytest.mark.mpl_image_compare +def test_autodiverging2(): + # Test with explicit vcenter + fig, axs = pplt.subplots(ncols=3) + data = 5 * state.rand(10, 10) + axs[0].pcolor(data, vcenter=0, colorbar="b") # otherwise should be disabled + axs[1].pcolor(data, vcenter=1.5, colorbar="b") + axs[2].pcolor(data, vcenter=4, colorbar="b", symmetric=True) + return fig + + +@pytest.mark.mpl_image_compare +def test_autodiverging3(): + # Test when cmap input disables auto diverging. + fig, axs = pplt.subplots(ncols=2, nrows=2, refwidth=2) + cmap = pplt.Colormap( + ("red7", "red3", "red1", "blue1", "blue3", "blue7"), listmode="discrete" + ) # noqa: E501 + data1 = 10 * state.rand(10, 10) + data2 = data1 - 2 + for i, cmap in enumerate(("RdBu_r", cmap)): + for j, data in enumerate((data1, data2)): + cmap = pplt.Colormap(pplt.Colormap(cmap)) + axs[i, j].pcolormesh(data, cmap=cmap, colorbar="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_autodiverging4(): + fig, axs = pplt.subplots(ncols=3) + data = state.rand(5, 5) * 10 - 5 + for i, ax in enumerate(axs[:2]): + ax.pcolor(data, sequential=bool(i), colorbar="b") + axs[2].pcolor(data, diverging=False, colorbar="b") # should have same effect + return fig + + +@pytest.mark.mpl_image_compare +def test_autodiverging5(): + fig, axs = pplt.subplots(ncols=2) + data = state.rand(5, 5) * 10 + 2 + for ax, norm in zip(axs, (None, "div")): + ax.pcolor(data, norm=norm, colorbar="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_colormap_mode(): + """ + Test auto extending, auto discrete. Should issue warnings. + """ + fig, axs = pplt.subplots(ncols=2, nrows=2, share=False) + axs[0].pcolor(state.rand(5, 5) % 0.3, extend="both", cyclic=True, colorbar="b") + axs[1].pcolor(state.rand(5, 5), sequential=True, diverging=True, colorbar="b") + axs[2].pcolor(state.rand(5, 5), discrete=False, qualitative=True, colorbar="b") + pplt.rc["cmap.discrete"] = False # should be ignored below + axs[3].contourf(state.rand(5, 5), colorbar="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_contour_labels(): + """ + Test contour labels. We use a separate `contour` object when adding labels to + filled contours or else weird stuff happens (see below). We could just modify + filled contour edges when not adding labels but that would be inconsistent with + behavior when labels are active. + """ + data = state.rand(5, 5) * 10 - 5 + fig, axs = pplt.subplots(ncols=2) + ax = axs[0] + ax.contourf( + data, + edgecolor="k", + linewidth=1.5, + labels=True, + labels_kw={"color": "k", "size": "large"}, + ) + ax = axs[1] + m = ax.contourf(data) + ax.clabel(m, colors="black", fontsize="large") # looks fine without this + for o in m.collections: + o.set_linewidth(1.5) + o.set_edgecolor("k") + return fig + + +@pytest.mark.mpl_image_compare +def test_contour_negative(): + """ + Ensure `cmap.monochrome` properly assigned. + """ + fig = pplt.figure(share=False) + ax = fig.subplot(131) + data = state.rand(10, 10) * 10 - 5 + ax.contour(data, color="k") + ax = fig.subplot(132) + ax.tricontour(*(state.rand(3, 20) * 10 - 5), color="k") + ax = fig.subplot(133) + ax.contour(data, cmap=["black"]) # fails but that's ok + return fig + + +@pytest.mark.mpl_image_compare +def test_contour_single(): + """ + Test whether single contour works. + """ + da = xr.DataArray( + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]]), dims=["y", "x"] + ) + fig, ax = pplt.subplots() + ax.contour(da, levels=[5.0], color="r") + return fig + + +@pytest.mark.mpl_image_compare +def test_edge_fix(): + """ + Test edge fix applied to 1D plotting utilities. + """ + # Test basic application + # TODO: This should make no difference for PNG plots? + pplt.rc.edgefix = 1 + fig, axs = pplt.subplots(ncols=2, share=False) + axs.format(grid=False) + axs[0].bar( + state.rand( + 10, + ) + * 10 + - 5, + width=1, + negpos=True, + ) + axs[1].area(state.rand(5, 3), stack=True) + + # Test whether ignored for transparent colorbars + data = state.rand(10, 10) + cmap = "magma" + fig, axs = pplt.subplots(nrows=3, ncols=2, refwidth=2.5, share=False) + for i, iaxs in enumerate((axs[:2], axs[2:4])): + if i == 0: + cmap = pplt.Colormap("magma", alpha=0.5) + alpha = None + iaxs.format(title="Colormap alpha") + else: + cmap = "magma" + alpha = 0.5 + iaxs.format(title="Single alpha") + iaxs[0].contourf(data, cmap=cmap, colorbar="b", alpha=alpha) + iaxs[1].pcolormesh(data, cmap=cmap, colorbar="b", alpha=alpha) + axs[4].bar(data[:3, :3], alpha=0.5) + axs[5].area(data[:3, :3], alpha=0.5, stack=True) + return fig + + +@pytest.mark.mpl_image_compare +def test_flow_functions(): + """ + These are seldom used and missing from documentation. Be careful + not to break anything basic. + """ + fig, ax = pplt.subplots() + for _ in range(2): + ax.streamplot(state.rand(10, 10), 5 * state.rand(10, 10), label="label") + + fig, axs = pplt.subplots(ncols=2) + ax = axs[0] + ax.quiver( + state.rand(10, 10), 5 * state.rand(10, 10), c=state.rand(10, 10), label="label" + ) + ax = axs[1] + ax.quiver(state.rand(10), state.rand(10), label="single") + return fig + + +@pytest.mark.mpl_image_compare +def test_gray_adjustment(): + """ + Test gray adjustments when creating segmented colormaps. + """ + fig, ax = pplt.subplots() + data = state.rand(5, 5) * 10 - 5 + cmap = pplt.Colormap(["blue", "grey3", "red"]) + ax.pcolor(data, cmap=cmap, colorbar="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_ignore_message(): + """ + Test various ignored argument warnings. + """ + warning = pplt.internals.ProplotWarning + fig, axs = pplt.subplots(ncols=2, nrows=2) + with pytest.warns(warning): + axs[0].contour(state.rand(5, 5) * 10, levels=pplt.arange(10), symmetric=True) + with pytest.warns(warning): + axs[1].contourf( + state.rand(10, 10), levels=np.linspace(0, 1, 10), locator=5, locator_kw={} + ) + with pytest.warns(warning): + axs[2].contourf( + state.rand(10, 10), + levels=pplt.arange(0, 1, 0.2), + vmin=0, + vmax=2, + locator=3, + colorbar="b", + ) + with pytest.warns(warning): + axs[3].hexbin( + state.rand(1000), + state.rand(1000), + levels=pplt.arange(0, 20), + gridsize=10, + locator=2, + colorbar="b", + cmap="blues", + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_levels_with_vmin_vmax(): + """ + Make sure `vmin` and `vmax` go into level generation algorithm. + """ + # Sample data + state = np.random.RandomState(51423) + x = y = np.array([-10, -5, 0, 5, 10]) + data = state.rand(y.size, x.size) + + # Figure + fig = pplt.figure(refwidth=2.3, share=False) + axs = fig.subplots() + m = axs.pcolormesh(x, y, data, vmax=1.35123) + axs.colorbar([m], loc="r") + return fig + + +@pytest.mark.mpl_image_compare +def test_level_restriction(): + """ + Test `negative`, `positive`, and `symmetric` with and without discrete. + """ + fig, axs = pplt.subplots(ncols=3, nrows=2) + data = 20 * state.rand(10, 10) - 5 + keys = ("negative", "positive", "symmetric") + for i, grp in enumerate((axs[:3], axs[3:])): + for j, ax in enumerate(grp): + kw = {keys[j]: True, "discrete": bool(1 - i)} + ax.pcolor(data, **kw, colorbar="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_qualitative_colormaps_1(): + """ + Test both `colors` and `cmap` input and ensure extend setting is used for + extreme only if unset. + """ + fig, axs = pplt.subplots(ncols=2) + data = state.rand(5, 5) + colors = pplt.get_colors("set3") + for ax, extend in zip(axs, ("both", "neither")): + ax.pcolor(data, extend=extend, colors=colors, colorbar="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_qualitative_colormaps_2(): + fig, axs = pplt.subplots(ncols=2) + data = state.rand(5, 5) + cmap = pplt.Colormap("set3") + cmap.set_under("black") # does not overwrite + for ax, extend in zip(axs, ("both", "neither")): + ax.pcolor(data, extend=extend, cmap=cmap, colorbar="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_segmented_norm(): + """ + Test segmented norm with non-discrete levels. + """ + fig, ax = pplt.subplots() + ax.pcolor( + state.rand(5, 5) * 10, + discrete=False, + norm="segmented", + norm_kw={"levels": [0, 2, 10]}, + colorbar="b", + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_triangular_functions(): + """ + Test triangular functions. Here there is no remotely sensible way to infer + """ + fig, ax = pplt.subplots() + N = 30 + y = state.rand(N) * 20 + x = state.rand(N) * 50 + da = xr.DataArray(state.rand(N), dims=("x",), coords={"x": x, "y": ("x", y)}) + ax.tricontour(da.x, da.y, da, labels=True) + return fig diff --git a/proplot/tests/test_axes.py b/proplot/tests/test_axes.py new file mode 100644 index 000000000..33b4aecfb --- /dev/null +++ b/proplot/tests/test_axes.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +""" +Test twin, inset, and panel axes. +""" +import numpy as np +import pytest +import proplot as pplt + +state = np.random.RandomState(51423) + + +def test_axis_access(): + # attempt to access the ax object 2d and linearly + fix, ax = pplt.subplots(ncols=2, nrows=2) + ax[0, 0] + ax[1, 0] + with pytest.raises(IndexError): + ax[0, 3] + ax[3] + + +@pytest.mark.mpl_image_compare +def test_inset_colors_1(): + """ + Test color application for zoom boxes. + """ + fig, ax = pplt.subplots() + ax.format(xlim=(0, 100), ylim=(0, 100)) + ix = ax.inset_axes( + (0.5, 0.5, 0.3, 0.3), zoom=True, zoom_kw={"color": "r", "fc": "r", "ec": "b"} + ) # zoom_kw={'alpha': 1}) + # ix = ax.inset_axes((40, 40, 20, 20), zoom=True, transform='data') + ix.format(xlim=(10, 20), ylim=(10, 20), grid=False) + return fig + + +@pytest.mark.mpl_image_compare +def test_inset_colors_2(): + fig, ax = pplt.subplots() + ax.format(xlim=(0, 100), ylim=(0, 100)) + ix = ax.inset_axes( + (0.3, 0.5, 0.5, 0.3), + zoom=True, + zoom_kw={"lw": 3, "ec": "red9", "a": 1, "c": pplt.set_alpha("red4", 0.5)}, + ) + ix.format(xlim=(10, 20), ylim=(10, 20)) + return fig + + +@pytest.mark.mpl_image_compare +def test_inset_zoom_update(): + """ + Test automatic limit adjusment with successive changes. Without the extra + lines in `draw()` and `get_tight_bbox()` this fails. + """ + fig, ax = pplt.subplots() + ax.format(xlim=(0, 100), ylim=(0, 100)) + ix = ax.inset_axes((40, 40, 20, 20), zoom=True, transform="data") + ix.format(xlim=(10, 20), ylim=(10, 20), grid=False) + ix.format(xlim=(10, 20), ylim=(10, 30)) + ax.format(ylim=(0, 300)) + return fig + + +@pytest.mark.mpl_image_compare +def test_panels_with_sharing(): + """ + Previously the below text would hide the second y label. + """ + fig, axs = pplt.subplots(ncols=2, share=False, refwidth=1.5) + axs.panel("left") + fig.format(ylabel="ylabel", xlabel="xlabel") + return fig + + +@pytest.mark.mpl_image_compare +def test_panels_without_sharing_1(): + """ + What should happen if `share=False` but figure-wide sharing enabled? + Strange use case but behavior appears "correct." + """ + fig, axs = pplt.subplots(ncols=2, share=True, refwidth=1.5, includepanels=False) + axs.panel("left", share=False) + fig.format(ylabel="ylabel", xlabel="xlabel") + return fig + + +@pytest.mark.mpl_image_compare +def test_panels_without_sharing_2(): + fig, axs = pplt.subplots(ncols=2, refwidth=1.5, includepanels=True) + for _ in range(3): + p = axs[0].panel("l", space=0) + p.format(xlabel="label") + fig.format(xlabel="xlabel") + return fig + + +@pytest.mark.mpl_image_compare +def test_panels_suplabels_three_hor_panels(): + """ + Test label sharing for `includepanels=True`. + Test for 1 subplot with 3 left panels + Include here centers the x label to include the panels + The xlabel should be centered along the main plot with the included side panels + """ + fig = pplt.figure() + ax = fig.subplots(refwidth=1.5, includepanels=True) + for _ in range(3): + ax[0].panel("l") + ax.format(xlabel="xlabel", ylabel="ylabel\nylabel\nylabel", suptitle="sup") + return fig + + +def test_panels_suplabels_three_hor_panels_donotinlcude(): + """ + Test label sharing for `includepanels=True`. + Test for 1 subplot with 3 left panels + The xlabel should be centered on the main plot + """ + fig = pplt.figure() + ax = fig.subplots(refwidth=1.5, includepanels=False) + for _ in range(3): + ax[0].panel("l") + ax.format( + xlabel="xlabel", + ylabel="ylabel\nylabel\nylabel", + suptitle="sup", + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_twin_axes_1(): + """ + Adjust twin axis positions. Should allow easily switching the location. + """ + # Test basic twin creation and tick, spine, label location changes + fig = pplt.figure() + ax = fig.subplot() + ax.format( + ycolor="blue", + ylabel="orig", + ylabelcolor="blue9", + yspineloc="l", + labelweight="bold", + xlabel="xlabel", + xtickloc="t", + xlabelloc="b", + ) + ax.alty(loc="r", color="r", labelcolor="red9", label="other", labelweight="bold") + return fig + + +@pytest.mark.mpl_image_compare +def test_twin_axes_2(): + # Simple example but doesn't quite work. Figure out how to specify left vs. right + # spines for 'offset' locations... maybe needs another keyword. + fig, ax = pplt.subplots() + ax.format(ymax=10, ylabel="Reference") + ax.alty(color="green", label="Green", max=8) + ax.alty(color="red", label="Red", max=15, loc=("axes", -0.2)) + ax.alty(color="blue", label="Blue", max=5, loc=("axes", 1.2), ticklabeldir="out") + return fig + + +@pytest.mark.mpl_image_compare +def test_twin_axes_3(): + # A worked example from Riley Brady + # Uses auto-adjusting limits + fig, ax = pplt.subplots() + axs = [ax, ax.twinx(), ax.twinx()] + axs[-1].spines["right"].set_position(("axes", 1.2)) + colors = ("Green", "Red", "Blue") + for ax, color in zip(axs, colors): + data = state.random(1) * state.random(10) + ax.plot(data, marker="o", linestyle="none", color=color) + ax.format(ylabel="%s Thing" % color, ycolor=color) + axs[0].format(xlabel="xlabel") + return fig diff --git a/proplot/tests/test_colorbar.py b/proplot/tests/test_colorbar.py new file mode 100644 index 000000000..a42d449cf --- /dev/null +++ b/proplot/tests/test_colorbar.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +""" +Test colorbars. +""" +import numpy as np +import pytest +import proplot as pplt + +state = np.random.RandomState(51423) + + +@pytest.mark.mpl_image_compare +def test_outer_align(): + """ + Test various align options. + """ + fig, ax = pplt.subplots() + ax.plot(np.empty((0, 4)), labels=list("abcd")) + ax.legend(loc="bottom", align="right", ncol=2) + ax.legend(loc="left", align="bottom", ncol=1) + ax.colorbar("magma", loc="r", align="top", shrink=0.5, label="label", extend="both") + ax.colorbar( + "magma", + loc="top", + ticklen=0, + tickloc="bottom", + align="left", + shrink=0.5, + label="Title", + extend="both", + labelloc="top", + labelweight="bold", + ) + ax.colorbar("magma", loc="right", extend="both", label="test extensions") + fig.suptitle("Align demo") + return fig + + +@pytest.mark.mpl_image_compare +def test_colorbar_ticks(): + """ + Test ticks modification. + """ + fig, axs = pplt.subplots(ncols=2) + ax = axs[0] + ax.colorbar("magma", loc="bottom", ticklen=10, linewidth=3, tickminor=True) + ax = axs[1] + ax.colorbar( + "magma", loc="bottom", ticklen=10, linewidth=3, tickwidth=1.5, tickminor=True + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_discrete_ticks(): + """ + Test `DiscreteLocator`. + """ + levels = pplt.arange(0, 2, 0.1) + data = state.rand(5, 5) * 2 + fig, axs = pplt.subplots(share=False, ncols=2, nrows=2, refwidth=2) + for i, ax in enumerate(axs): + cmd = ax.contourf if i // 2 == 0 else ax.pcolormesh + m = cmd(data, levels=levels, extend="both") + ax.colorbar(m, loc="t" if i // 2 == 0 else "b") + ax.colorbar(m, loc="l" if i % 2 == 0 else "r") + return fig + + +@pytest.mark.mpl_image_compare +def test_discrete_vs_fixed(): + """ + Test `DiscreteLocator` for numeric on-the-fly + mappable ticks and `FixedLocator` otherwise. + """ + fig, axs = pplt.subplots(ncols=2, nrows=3, refwidth=1.3, share=False) + axs[0].plot(state.rand(10, 5), labels=list("xyzpq"), colorbar="b") # fixed + axs[1].plot(state.rand(10, 5), labels=np.arange(5), colorbar="b") # discrete + axs[2].contourf( + state.rand(10, 10), + colorbar="b", + colorbar_kw={"ticklabels": list("xyzpq")}, # fixed + ) + axs[3].contourf(state.rand(10, 10), colorbar="b") # discrete + axs[4].pcolormesh( + state.rand(10, 10) * 20, colorbar="b", levels=[0, 2, 4, 6, 8, 10, 15, 20] + ) # fixed + axs[5].pcolormesh( + state.rand(10, 10) * 20, colorbar="b", levels=pplt.arange(0, 20, 2) + ) # discrete + return fig + + +@pytest.mark.mpl_image_compare +def test_uneven_levels(): + """ + Test even and uneven levels with discrete cmap. Ensure minor ticks are disabled. + """ + N = 20 + state = np.random.RandomState(51423) + data = np.cumsum(state.rand(N, N), axis=1) * 12 + colors = [ + "white", + "indigo1", + "indigo3", + "indigo5", + "indigo7", + "indigo9", + "yellow1", + "yellow3", + "yellow5", + "yellow7", + "yellow9", + "violet1", + "violet3", + ] + levels_even = pplt.arange(1, 12, 1) + levels_uneven = [1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 3.75, 4.5, 6.0, 7.5, 9.0, 12.0] + fig, axs = pplt.subplots(ncols=2, refwidth=3.0) + axs[0].pcolor( + data, levels=levels_uneven, colors=colors, colorbar="r", extend="both" + ) + axs[1].pcolor(data, levels=levels_even, colors=colors, colorbar="r", extend="both") + return fig + + +@pytest.mark.mpl_image_compare +def test_on_the_fly_mappable(): + """ + Test on-the-fly mappable generation. + """ + fig, axs = pplt.subplots(ncols=2, nrows=3, space=3) + axs.format(aspect=0.5) + axs[0].colorbar("magma", vmin=None, vmax=100, values=[0, 1, 2, 3, 4], loc="bottom") + axs[1].colorbar("magma", vmin=None, vmax=100, loc="bottom") + axs[2].colorbar("colorblind", vmin=None, vmax=None, values=[0, 1, 2], loc="bottom") + axs[3].colorbar("colorblind", vmin=None, vmax=None, loc="bottom") + axs[4].colorbar(["r", "b", "g", "k", "w"], values=[0, 1, 2], loc="b") + axs[5].colorbar(["r", "b", "g", "k", "w"], loc="bottom") + + # Passing labels to plot function. + fig, ax = pplt.subplots() + ax.scatter(state.rand(10, 4), labels=["foo", "bar", "baz", "xyz"], colorbar="b") + + # Passing string value lists. This helps complete the analogy with legend 'labels'. + fig, ax = pplt.subplots() + hs = ax.line(state.rand(20, 5)) + ax.colorbar(hs, loc="b", values=["abc", "def", "ghi", "pqr", "xyz"]) + return fig + + +@pytest.mark.mpl_image_compare +def test_inset_colorbars(): + """ + Test basic functionality. + """ + # Simple example + fig, ax = pplt.subplots() + ax.colorbar("magma", loc="ul") + + # Colorbars from lines + fig = pplt.figure(share=False, refwidth=2) + ax = fig.subplot(121) + state = np.random.RandomState(51423) + data = 1 + (state.rand(12, 10) - 0.45).cumsum(axis=0) + cycle = pplt.Cycle("algae") + hs = ax.line( + data, + lw=4, + cycle=cycle, + colorbar="lr", + colorbar_kw={"length": "8em", "label": "line colorbar"}, + ) + ax.colorbar(hs, loc="t", values=np.arange(0, 10), label="line colorbar", ticks=2) + + # Colorbars from a mappable + ax = fig.subplot(122) + m = ax.contourf(data.T, extend="both", cmap="algae", levels=pplt.arange(0, 3, 0.5)) + fig.colorbar( + m, + loc="r", + length=1, # length is relative + label="interior ticks", + tickloc="left", + ) + ax.colorbar( + m, + loc="ul", + length=6, # length is em widths + label="inset colorbar", + tickminor=True, + alpha=0.5, + ) + fig.format( + suptitle="Colorbar formatting demo", + xlabel="xlabel", + ylabel="ylabel", + titleabove=False, + ) + return fig + + +@pytest.mark.skip("not sure what this does") +@pytest.mark.mpl_image_compare +def test_segmented_norm_center(): + """ + Test various align options. + """ + fig, ax = pplt.subplots() + cmap = pplt.Colormap("NegPos", cut=0.1) + data = state.rand(10, 10) * 10 - 2 + levels = [-4, -3, -2, -1, 0, 1, 2, 4, 8, 16, 32, 64, 128] + norm = pplt.SegmentedNorm(levels, vcenter=0, fair=1) + ax.pcolormesh(data, levels=levels, norm=norm, cmap=cmap, colorbar="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_segmented_norm_ticks(): + """ + Ensure segmented norm ticks show up in center when `values` are passed. + """ + fig, ax = pplt.subplots() + data = state.rand(10, 10) * 10 + values = (1, 5, 5.5, 6, 10) + ax.contourf( + data, + values=values, + colorbar="ll", + colorbar_kw={"tickminor": True, "minorlocator": np.arange(-20, 20, 0.5)}, + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_reversed_levels(): + """ + Test negative levels with a discrete norm and segmented norm. + """ + fig, axs = pplt.subplots(ncols=4, nrows=2, refwidth=1.8) + data = state.rand(20, 20).cumsum(axis=0) + i = 0 + for stride in (1, -1): + for key in ("levels", "values"): + for levels in ( + np.arange(0, 15, 1), # with Normalizer + [0, 1, 2, 5, 10, 15], # with LinearSegmentedNorm + ): + ax = axs[i] + kw = {key: levels[::stride]} + ax.pcolormesh(data, colorbar="b", **kw) + i += 1 + return fig diff --git a/proplot/tests/test_docs.py b/proplot/tests/test_docs.py new file mode 100644 index 000000000..2ae0ad3d8 --- /dev/null +++ b/proplot/tests/test_docs.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +""" +Automatically build pytests from jupytext py:percent documentation files. +""" +# import glob +# import os + +# import jupytext +# import pytest + +# from proplot.config import rc +# from proplot.tests import SAVEFIG_KWARGS, TOLERANCE, VERSION_STRING + + +# def _init_tests(): +# """ +# Construct tests from the jupytext docs examples. +# """ +# # WARNING: This will only work if all jupytext examples consist of single code +# # cells or adjacent code cells without intervening markdown or ReST cells. +# # Alternative would be to define the entire file as a single test but that +# # would make testing and debugging more difficult. +# base = os.path.dirname(__file__) +# paths = glob.glob(f'{base}/../../docs/*.py') +# for path in sorted(paths): +# if os.path.basename(path) == 'conf.py': +# continue +# baseline, _ = os.path.splitext(os.path.basename(path)) +# result = jupytext.read(path, fmt='py:percent') +# cells = result['cells'] +# source = '' +# num = 1 +# for i, cell in enumerate(cells): +# type_ = cell['cell_type'] +# if type_ == 'code': +# source += '\n' + cell['source'] +# if not source: +# continue +# if i == len(cells) - 1 or type_ != 'code': # end of example definition +# _make_cell_test(num, baseline, source) +# num += 1 +# print(f'\nMade {num} tests from file: {path}', end='') + + +# def _make_cell_test(num, baseline, cell_source): +# """ +# Add a test using the jupytext docs cell. +# """ +# # WARNING: Ugly kludge to replace e.g. backend='basemap' with backend='cartopy' +# # for matplotlib versions incompatible with basemap. Generally these examples +# # test basemap and cartopy side-by-side so this will effectively duplicate the +# # cartopy tests but keep us from having to dump them. +# if baseline == 'test_projections' and VERSION_STRING == 'mpl32': +# cell_source = cell_source.replace("'basemap'", "'cartopy'") +# if 'pplt.Proj' in cell_source or 'Table' in cell_source: +# return # examples that cannot be naively converted + +# def run_test(): +# rc.reset() +# exec(cell_source) + +# name = f'{baseline}_cell_{num:02d}' +# decorator = pytest.mark.mpl_image_compare( +# run_test, +# filename=f'{name}_{VERSION_STRING}.png', +# baseline_dir='images_docs', +# savefig_kwargs=SAVEFIG_KWARGS, +# tolerance=TOLERANCE, +# style={}, # no mpl style +# ) +# test = decorator(run_test) +# name = f'test_{name}' +# test.__name__ = test.__qualname__ = name +# globals()[name] = test # for pytest detection + + +# # Initialize functions +# _init_tests() diff --git a/proplot/tests/test_format.py b/proplot/tests/test_format.py new file mode 100644 index 000000000..fcee8a4cd --- /dev/null +++ b/proplot/tests/test_format.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 +""" +Test format and rc behavior. +""" +import locale, numpy as np, proplot as pplt, pytest +import warnings + +state = np.random.RandomState(51423) + + +# def test_colormap_assign(): +# """ +# Test below line is possible and naming schemes. +# """ +# pplt.rc["image.cmap"] = pplt.Colormap("phase", shift=180, left=0.2) +# assert pplt.rc["cmap"] == pplt.rc["cmap.sequential"] == "_Phase_copy_s" +# pplt.rc["image.cmap"] = pplt.Colormap("magma", reverse=True, right=0.8) +# assert pplt.rc["image.cmap"] == pplt.rc["cmap.sequential"] == "_magma_copy_r" + + +def test_ignored_keywords(): + """ + Test ignored keywords and functions. + """ + with warnings.catch_warnings(record=True) as record: + fig, ax = pplt.subplots( + gridspec_kw={"left": 3}, + subplot_kw={"proj": "cart"}, + subplotpars={"left": 0.2}, + ) + assert len(record) == 3 + with warnings.catch_warnings(record=True) as record: + fig.subplots_adjust(left=0.2) + assert len(record) == 1 + + +@pytest.mark.mpl_image_compare +def test_init_format(): + """ + Test application of format args on initialization. + """ + fig, axs = pplt.subplots( + ncols=2, + xlim=(0, 10), + xlabel="xlabel", + abc=True, + title="Subplot title", + collabels=["Column 1", "Column 2"], + suptitle="Figure title", + ) + axs[0].format(hatch="xxx", hatchcolor="k", facecolor="blue3") + return fig + + +@pytest.mark.mpl_image_compare +def test_patch_format(): + """ + Test application of patch args on initialization. + """ + fig = pplt.figure(suptitle="Super title") + fig.subplot( + 121, proj="cyl", labels=True, land=True, latlines=20, abcloc="l", abc="[A]" + ) + fig.subplot( + 122, + facecolor="gray1", + color="red", + titleloc="l", + title="Hello", + abcloc="l", + abc="[A]", + xticks=0.1, + yformatter="scalar", + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_multi_formatting(): + """ + Support formatting in multiple projections. + """ + fig, axs = pplt.subplots(ncols=2, proj=("cart", "cyl")) + axs[0].pcolormesh(state.rand(5, 5)) + fig.format( + land=1, + labels=1, + lonlim=(0, 90), + latlim=(0, 90), + xlim=(0, 10), + ylim=(0, 10), + ) + axs[:1].format( + land=1, + labels=1, + lonlim=(0, 90), + latlim=(0, 90), + xlim=(0, 10), + ylim=(0, 10), + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_inner_title_zorder(): + """ + Test prominence of contour labels and whatnot. + """ + fig, ax = pplt.subplots() + ax.format( + title="TITLE", titleloc="upper center", titleweight="bold", titlesize="xx-large" + ) + ax.format(xlim=(0, 1), ylim=(0, 1)) + ax.text( + 0.5, + 0.95, + "text", + ha="center", + va="top", + color="red", + weight="bold", + size="xx-large", + ) + x = [[0.4, 0.6]] * 2 + y = z = [[0.9, 0.9], [1.0, 1.0]] + ax.contour( + x, + y, + z, + color="k", + labels=True, + levels=None, + labels_kw={"color": "blue", "weight": "bold", "size": "xx-large"}, + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_font_adjustments(): + """ + Test font name application. Somewhat hard to do. + """ + fig, axs = pplt.subplots(ncols=2) + axs.format( + abc="A.", + fontsize=15, + fontname="Fira Math", + xlabel="xlabel", + ylabel="ylabel", + title="Title", + figtitle="Figure title", + collabels=["Column 1", "Column 2"], + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_axes_colors(): + """ + Test behavior of passing color to format. + """ + fig, axs = pplt.subplots( + ncols=3, + nrows=2, + share=False, + proj=("cyl", "cart", "polar", "cyl", "cart", "polar"), + wratios=(2, 2, 1), + ) + axs[:, 0].format(labels=True) + axs[:3].format(edgecolor="red", gridlabelsize="med-large", gridlabelweight="bold") + axs[:3].format(color="red") # without this just colors the edge + axs[1].format(xticklabelcolor="gray") + # axs[2].format(ticklabelcolor='red') + axs[1].format(tickcolor="blue") + axs[3:].format(color="red") # ensure propagates + # axs[-1].format(gridlabelcolor='green') # should work + return fig + + +@pytest.mark.parametrize("loc", ["en_US.UTF-8"]) +@pytest.mark.mpl_image_compare +def test_locale_formatting(loc): + """ + Ensure locale formatting works. Also zerotrim should account + for non-period decimal separators. + """ + # dealing with read the docs + original_locale = locale.getlocale() + try: + try: + locale.setlocale(locale.LC_ALL, loc) + except locale.Error: + pytest.skip(f"Locale {loc} not available on this system") + + # Your test code that is sensitive to the locale settings + assert locale.getlocale() == (loc.split(".")[0], loc.split(".")[1]) + + pplt.rc["formatter.use_locale"] = False + pplt.rc["formatter.zerotrim"] = True + with pplt.rc.context({"formatter.use_locale": True}): + fig, ax = pplt.subplots() + ticks = pplt.arange(-1, 1, 0.1) + ax.format(ylim=(min(ticks), max(ticks)), yticks=ticks) + return fig + finally: + # Always reset to the original locale + locale.setlocale(locale.LC_ALL, original_locale) + pplt.rc["formatter.use_locale"] = False + pplt.rc["formatter.zerotrim"] = True + with pplt.rc.context({"formatter.use_locale": True}): + fig, ax = pplt.subplots() + ticks = pplt.arange(-1, 1, 0.1) + ax.format(ylim=(min(ticks), max(ticks)), yticks=ticks) + return fig + + +@pytest.mark.mpl_image_compare +def test_bounds_ticks(): + """ + Test spine bounds and location. Previously applied `fixticks` + automatically but no longer the case. + """ + fig, ax = pplt.subplots() + # ax.format(xlim=(-10, 10)) + ax.format(xloc="top") + ax.format(xlim=(-10, 15), xbounds=(0, 10)) + return fig + + +@pytest.mark.mpl_image_compare +def test_cutoff_ticks(): + """ + Test spine cutoff ticks. + """ + fig, ax = pplt.subplots() + # ax.format(xlim=(-10, 10)) + ax.format(xlim=(-10, 10), xscale=("cutoff", 0, 2), xloc="top", fixticks=True) + ax.axvspan(0, 100, facecolor="k", alpha=0.1) + return fig + + +@pytest.mark.mpl_image_compare +def test_spine_side(): + """ + Test automatic spine selection when passing `xspineloc` or `yspineloc`. + """ + fig, ax = pplt.subplots() + ax.plot(pplt.arange(-5, 5), (10 * state.rand(11, 5) - 5).cumsum(axis=0)) + ax.format(xloc="bottom", yloc="zero") + ax.alty(loc="right") + return fig + + +@pytest.mark.mpl_image_compare +def test_spine_offset(): + """ + Test offset axes. + """ + fig, ax = pplt.subplots() + ax.format(xloc="none") # test none instead of neither + ax.alty(loc=("axes", -0.2), color="red") + # ax.alty(loc=('axes', 1.2), color='blue') + ax.alty(loc=("axes", -0.4), color="blue") + ax.alty(loc=("axes", 1.1), color="green") + return fig + + +@pytest.mark.mpl_image_compare +def test_tick_direction(): + """ + Test tick direction arguments. + """ + fig, axs = pplt.subplots(ncols=2) + axs[0].format(tickdir="in") + axs[1].format(xtickdirection="inout", ytickdir="out") # rc setting should be used? + return fig + + +@pytest.mark.mpl_image_compare +def test_tick_length(): + """ + Test tick length args. Ensure ratios can be applied successively. + """ + fig, ax = pplt.subplots() + ax.format(yticklen=100) + ax.format(xticklen=50, yticklenratio=0.1) + return fig + + +@pytest.mark.mpl_image_compare +def test_tick_width(): + """ + Test tick width args. Ensure ratios can be applied successively, setting + width to `zero` adjusts length for label padding, and ticks can appear + without spines if requested. + """ + fig, axs = pplt.subplots(ncols=2, nrows=2, share=False) + ax = axs[0] + ax.format(linewidth=2, ticklen=20, xtickwidthratio=1) + ax.format(ytickwidthratio=0.3) + ax = axs[1] + ax.format(axeslinewidth=0, ticklen=20, tickwidth=2) # should permit ticks + ax = axs[2] + ax.format(tickwidth=0, ticklen=50) # should set length to zero + ax = axs[3] + ax.format(linewidth=0, ticklen=20, tickwidth="5em") # should override linewidth + return fig + + +@pytest.mark.mpl_image_compare +def test_tick_labels(): + """ + Test default and overwriting properties of auto tick labels. + """ + import pandas as pd + + data = state.rand(5, 3) + data = pd.DataFrame(data, index=["foo", "bar", "baz", "bat", "bot"]) + fig, axs = pplt.subplots(abc="A.", abcloc="ul", ncols=2, refwidth=3, span=False) + for i, ax in enumerate(axs): + data.index.name = "label" + if i == 1: + ax.format(xformatter="null") # overrides result + ax.bar(data, autoformat=True) + if i == 0: + data.index = ["abc", "def", "ghi", "jkl", "mno"] + data.index.name = "foobar" # label should be updated + ax.bar(-data, autoformat=True) + return fig + + +@pytest.mark.mpl_image_compare +def test_label_settings(): + """ + Test label colors and ensure color change does not erase labels. + """ + fig, ax = pplt.subplots() + ax.format(xlabel="xlabel", ylabel="ylabel") + ax.format(labelcolor="red") + return fig diff --git a/proplot/tests/test_geographic.py b/proplot/tests/test_geographic.py new file mode 100644 index 000000000..6b82a8a34 --- /dev/null +++ b/proplot/tests/test_geographic.py @@ -0,0 +1,116 @@ +import proplot as plt, numpy as np +import pytest + + +@pytest.mark.mpl_image_compare +def test_geographic_single_projection(): + fig = plt.figure(refwidth=3) + axs = fig.subplots(nrows=2, proj="robin", proj_kw={"lon_0": 180}) + # proj = pplt.Proj('robin', lon_0=180) + # axs = pplt.subplots(nrows=2, proj=proj) # equivalent to above + axs.format( + suptitle="Figure with single projection", + land=True, + latlines=30, + lonlines=60, + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_geographic_multiple_projections(): + fig = plt.figure() + # Add projections + gs = plt.GridSpec(ncols=2, nrows=3, hratios=(1, 1, 1.4)) + for i, proj in enumerate(("cyl", "hammer", "npstere")): + ax1 = fig.subplot(gs[i, 0], proj=proj, basemap=True) # basemap + ax2 = fig.subplot(gs[i, 1], proj=proj) # cartopy + + # Format projections + fig.format( + land=True, + suptitle="Figure with several projections", + toplabels=("Basemap projections", "Cartopy projections"), + toplabelweight="normal", + latlines=30, + lonlines=60, + lonlabels="b", + latlabels="r", # or lonlabels=True, labels=True, etc. + ) + fig.subplotgrid[-2:].format( + latlines=20, lonlines=30 + ) # dense gridlines for polar plots + plt.rc.reset() + return fig + + +@pytest.mark.mpl_image_compare +def test_drawing_in_projection_without_globe(): + # Fake data with unusual longitude seam location and without coverage over poles + offset = -40 + lon = plt.arange(offset, 360 + offset - 1, 60) + lat = plt.arange(-60, 60 + 1, 30) + state = np.random.RandomState(51423) + data = state.rand(len(lat), len(lon)) + + globe = False + string = "with" if globe else "without" + gs = plt.GridSpec(nrows=2, ncols=2) + fig = plt.figure(refwidth=2.5) + for i, ss in enumerate(gs): + ax = fig.subplot(ss, proj="kav7", basemap=(i % 2)) + cmap = ("sunset", "sunrise")[i % 2] + if i > 1: + ax.pcolor(lon, lat, data, cmap=cmap, globe=globe, extend="both") + else: + m = ax.contourf(lon, lat, data, cmap=cmap, globe=globe, extend="both") + fig.colorbar(m, loc="b", span=i + 1, label="values", extendsize="1.7em") + fig.format( + suptitle=f"Geophysical data {string} global coverage", + toplabels=("Cartopy example", "Basemap example"), + leftlabels=("Filled contours", "Grid boxes"), + toplabelweight="normal", + leftlabelweight="normal", + coast=True, + lonlines=90, + abc="A.", + abcloc="ul", + abcborder=False, + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_drawing_in_projection_with_globe(): + # Fake data with unusual longitude seam location and without coverage over poles + offset = -40 + lon = plt.arange(offset, 360 + offset - 1, 60) + lat = plt.arange(-60, 60 + 1, 30) + state = np.random.RandomState(51423) + data = state.rand(len(lat), len(lon)) + + globe = True + string = "with" if globe else "without" + gs = plt.GridSpec(nrows=2, ncols=2) + fig = plt.figure(refwidth=2.5) + for i, ss in enumerate(gs): + ax = fig.subplot(ss, proj="kav7", basemap=(i % 2)) + cmap = ("sunset", "sunrise")[i % 2] + if i > 1: + ax.pcolor(lon, lat, data, cmap=cmap, globe=globe, extend="both") + else: + m = ax.contourf(lon, lat, data, cmap=cmap, globe=globe, extend="both") + fig.colorbar(m, loc="b", span=i + 1, label="values", extendsize="1.7em") + fig.format( + suptitle=f"Geophysical data {string} global coverage", + toplabels=("Cartopy example", "Basemap example"), + leftlabels=("Filled contours", "Grid boxes"), + toplabelweight="normal", + leftlabelweight="normal", + coast=True, + lonlines=90, + abc="A.", + abcloc="ul", + abcborder=False, + ) + return fig diff --git a/proplot/tests/test_imshow.py b/proplot/tests/test_imshow.py new file mode 100644 index 000000000..0fcc623a9 --- /dev/null +++ b/proplot/tests/test_imshow.py @@ -0,0 +1,110 @@ +import pytest + +import proplot as plt, numpy as np +from matplotlib.testing import setup + + +@pytest.fixture() +def setup_mpl(): + setup() + plt.clf() + + +@pytest.mark.mpl_image_compare +def test_standardized_input(): + # Sample data + state = np.random.RandomState(51423) + x = y = np.array([-10, -5, 0, 5, 10]) + xedges = plt.edges(x) + yedges = plt.edges(y) + data = state.rand(y.size, x.size) # "center" coordinates + lim = (np.min(xedges), np.max(xedges)) + + with plt.rc.context({"cmap": "Grays", "cmap.levels": 21}): + # Figure + fig = plt.figure(refwidth=2.3, share=False) + axs = fig.subplots(ncols=2, nrows=2) + axs.format( + xlabel="xlabel", + ylabel="ylabel", + xlim=lim, + ylim=lim, + xlocator=5, + ylocator=5, + suptitle="Standardized input demonstration", + toplabels=("Coordinate centers", "Coordinate edges"), + ) + + # Plot using both centers and edges as coordinates + axs[0].pcolormesh(x, y, data) + axs[1].pcolormesh(xedges, yedges, data) + axs[2].contourf(x, y, data) + axs[3].contourf(xedges, yedges, data) + fig.show() + return fig + + +@pytest.mark.mpl_image_compare +def test_inbounds_data(): + # Sample data + cmap = "turku_r" + state = np.random.RandomState(51423) + N = 80 + x = y = np.arange(N + 1) + data = 10 + (state.normal(0, 3, size=(N, N))).cumsum(axis=0).cumsum(axis=1) + xlim = ylim = (0, 25) + + # Plot the data + fig, axs = plt.subplots( + [[0, 1, 1, 0], [2, 2, 3, 3]], + wratios=(1.3, 1, 1, 1.3), + span=False, + refwidth=2.2, + ) + axs[0].fill_between( + xlim, + *ylim, + zorder=3, + edgecolor="red", + facecolor=plt.set_alpha("red", 0.2), + ) + for i, ax in enumerate(axs): + inbounds = i == 1 + title = f"Restricted lims inbounds={inbounds}" + title += " (default)" if inbounds else "" + ax.format( + xlim=(None if i == 0 else xlim), + ylim=(None if i == 0 else ylim), + title=("Default axis limits" if i == 0 else title), + ) + ax.pcolor(x, y, data, cmap=cmap, inbounds=inbounds) + fig.format( + xlabel="xlabel", + ylabel="ylabel", + suptitle="Default vmin/vmax restricted to in-bounds data", + ) + fig.show() + return fig + + +@pytest.mark.mpl_image_compare +def test_colorbar(): + # Sample data + state = np.random.RandomState(51423) + data = 10 + state.normal(0, 1, size=(33, 33)).cumsum(axis=0).cumsum(axis=1) + + # Figure + fig, axs = plt.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], ref=3, refwidth=2.3) + axs.format(yformatter="none", suptitle="Discrete vs. smooth colormap levels") + + # Pcolor + axs[0].pcolor(data, cmap="viridis", colorbar="l") + axs[0].set_title("Pcolor plot\ndiscrete=True (default)") + axs[1].pcolor(data, discrete=False, cmap="viridis", colorbar="r") + axs[1].set_title("Pcolor plot\ndiscrete=False") + + # Imshow + m = axs[2].imshow(data, cmap="oslo", colorbar="b") + axs[2].format(title="Imshow plot\ndiscrete=False (default)", yformatter="auto") + fig.show() + return fig diff --git a/proplot/tests/test_inset.py b/proplot/tests/test_inset.py new file mode 100644 index 000000000..5a4c45a97 --- /dev/null +++ b/proplot/tests/test_inset.py @@ -0,0 +1,28 @@ +import proplot as pplt, numpy as np, pytest + + +@pytest.mark.mpl_image_compare +def test_inset_basic(): + # Demonstrate that complex arrangements preserve + # spacing, aspect ratios, and axis sharing + gs = pplt.GridSpec(nrows=2, ncols=2) + fig = pplt.figure(refwidth=1.5, share=False) + for ss, side in zip(gs, "tlbr"): + ax = fig.add_subplot(ss) + px = ax.panel_axes(side, width="3em") + fig.format( + xlim=(0, 1), + ylim=(0, 1), + xlabel="xlabel", + ylabel="ylabel", + xticks=0.2, + yticks=0.2, + title="Title", + suptitle="Complex arrangement of panels", + toplabels=("Column 1", "Column 2"), + abc=True, + abcloc="ul", + titleloc="uc", + titleabove=False, + ) + return fig diff --git a/proplot/tests/test_integration.py b/proplot/tests/test_integration.py new file mode 100644 index 000000000..1afd4c6b0 --- /dev/null +++ b/proplot/tests/test_integration.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Test xarray, pandas, pint, seaborn integration. +""" +import numpy as np, pandas as pd, seaborn as sns +import xarray as xr +import proplot as pplt, pytest +import pint + +state = np.random.RandomState(51423) + + +@pytest.mark.mpl_image_compare +def test_pint_quantities(): + """ + Ensure auto-formatting and column iteration both work. + """ + pplt.rc.unitformat = "~H" + ureg = pint.UnitRegistry() + fig, ax = pplt.subplots() + ax.plot( + np.arange(10), + state.rand(10) * ureg.km, + "C0", + np.arange(10), + state.rand(10) * ureg.m * 1e2, + "C1", + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_data_keyword(): + """ + Make sure `data` keywords work properly. + """ + N = 10 + M = 20 + ds = xr.Dataset( + {"z": (("x", "y"), state.rand(N, M))}, + coords={ + "x": ("x", np.arange(N) * 10, {"long_name": "longitude"}), + "y": ("y", np.arange(M) * 5, {"long_name": "latitude"}), + }, + ) + fig, ax = pplt.subplots() + # ax.pcolor('z', data=ds, order='F') + ax.pcolor(z="z", data=ds, transpose=True) + ax.format(xformatter="deglat", yformatter="deglon") + return fig + + +@pytest.mark.mpl_image_compare +def test_keep_guide_labels(): + """ + Preserve metadata when passing mappables and handles to colorbar and + legend subsequently. + """ + fig, ax = pplt.subplots() + df = pd.DataFrame(state.rand(5, 5)) + df.name = "variable" + m = ax.pcolor(df) + ax.colorbar(m) + + fig, ax = pplt.subplots() + for k in ("foo", "bar", "baz"): + s = pd.Series(state.rand(5), index=list("abcde"), name=k) + ax.plot( + s, + legend="ul", + legend_kw={ + "lw": 5, + "ew": 2, + "ec": "r", + "fc": "w", + "handle_kw": {"marker": "d"}, + }, + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_seaborn_swarmplot(): + """ + Test seaborn swarm plots. + """ + tips = sns.load_dataset("tips") + fig = pplt.figure(refwidth=3) + ax = fig.subplot() + sns.swarmplot(ax=ax, x="day", y="total_bill", data=tips, palette="cubehelix") + # fig, ax = pplt.subplots() + # sns.swarmplot(y=state.normal(size=100), ax=ax) + return fig + + +@pytest.mark.mpl_image_compare +def test_seaborn_hist(): + """ + Test seaborn histograms. + """ + fig, axs = pplt.subplots(ncols=2, nrows=2) + sns.histplot(state.normal(size=100), ax=axs[0]) + sns.kdeplot(x=state.rand(100), y=state.rand(100), ax=axs[1]) + penguins = sns.load_dataset("penguins") + sns.histplot( + data=penguins, x="flipper_length_mm", hue="species", multiple="stack", ax=axs[2] + ) + sns.kdeplot( + data=penguins, x="flipper_length_mm", hue="species", multiple="stack", ax=axs[3] + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_seaborn_relational(): + """ + Test scatter plots. Disabling seaborn detection creates mismatch between marker + sizes and legend. + """ + fig = pplt.figure() + ax = fig.subplot() + sns.set_theme(style="white") + # Load the example mpg dataset + mpg = sns.load_dataset("mpg") + # Plot miles per gallon against horsepower with other semantics + sns.scatterplot( + x="horsepower", + y="mpg", + hue="origin", + size="weight", + sizes=(40, 400), + alpha=0.5, + palette="muted", + # legend='bottom', + # height=6, + data=mpg, + ax=ax, + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_seaborn_heatmap(): + """ + Test seaborn heatmaps. This should work thanks to backwards compatibility support. + """ + fig, ax = pplt.subplots() + sns.heatmap(state.normal(size=(50, 50)), ax=ax[0]) + return fig diff --git a/proplot/tests/test_journals.py b/proplot/tests/test_journals.py deleted file mode 100644 index 5fe002027..000000000 --- a/proplot/tests/test_journals.py +++ /dev/null @@ -1,11 +0,0 @@ -import pytest - -import proplot as pplt -from proplot.subplots import JOURNAL_SPECS - - -# Loop through all available journals. -@pytest.mark.parametrize('journal', JOURNAL_SPECS.keys()) -def test_journal_subplots(journal): - """Tests that subplots can be generated with journal specifications.""" - f, axs = pplt.subplots(journal=journal) diff --git a/proplot/tests/test_legend.py b/proplot/tests/test_legend.py new file mode 100644 index 000000000..4b3826ad1 --- /dev/null +++ b/proplot/tests/test_legend.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +""" +Test legends. +""" +import numpy as np, pandas as pd, proplot as pplt, pytest + +state = np.random.RandomState(51423) + + +@pytest.mark.mpl_image_compare +def test_auto_legend(): + """ + Test retrieval of legends from panels, insets, etc. + """ + fig, ax = pplt.subplots() + ax.line(state.rand(5, 3), labels=list("abc")) + px = ax.panel_axes("right", share=False) + px.linex(state.rand(5, 3), labels=list("xyz")) + # px.legend(loc='r') + ix = ax.inset_axes((-0.2, 0.8, 0.5, 0.5), zoom=False) + ix.line(state.rand(5, 2), labels=list("pq")) + ax.legend(loc="b", order="F", edgecolor="red9", edgewidth=3) + return fig + + +@pytest.mark.mpl_image_compare +def test_singleton_legend(): + """ + Test behavior when singleton lists are passed. + Ensure this does not trigger centered-row legends. + """ + fig, ax = pplt.subplots() + h1 = ax.plot([0, 1, 2], label="a") + h2 = ax.plot([0, 1, 1], label="b") + ax.legend(loc="best") + ax.legend([h1, h2], loc="bottom") + return fig + + +@pytest.mark.mpl_image_compare +def test_centered_legends(): + """ + Test success of algorithm. + """ + # Basic centered legends + fig, axs = pplt.subplots(ncols=2, nrows=2, axwidth=2) + hs = axs[0].plot(state.rand(10, 6)) + locs = ["l", "t", "r", "uc", "ul", "ll"] + locs = ["l", "t", "uc", "ll"] + labels = ["a", "bb", "ccc", "ddddd", "eeeeeeee", "fffffffff"] + for ax, loc in zip(axs, locs): + ax.legend(hs, loc=loc, ncol=3, labels=labels, center=True) + + # Pass centered legends with keywords or list-of-list input. + fig, ax = pplt.subplots() + hs = ax.plot(state.rand(10, 5), labels=list("abcde")) + ax.legend(hs, center=True, loc="b") + ax.legend(hs + hs[:1], loc="r", ncol=1) + ax.legend([hs[:2], hs[2:], hs[0]], loc="t") + return fig + + +@pytest.mark.mpl_image_compare +def test_manual_labels(): + """ + Test mixed auto and manual labels. Passing labels but no handles does nothing + This is breaking change but probably best. We should not be "guessing" the + order objects were drawn in then assigning labels to them. Similar to using + OO interface and rejecting pyplot "current axes" and "current figure". + """ + fig, ax = pplt.subplots() + (h1,) = ax.plot([0, 1, 2], label="label1") + (h2,) = ax.plot([0, 1, 1], label="label2") + for loc in ("best", "bottom"): + ax.legend([h1, h2], loc=loc, labels=[None, "override"]) + fig, ax = pplt.subplots() + ax.plot([0, 1, 2]) + ax.plot([0, 1, 1]) + for loc in ("best", "bottom"): + # ax.legend(loc=loc, labels=['a', 'b']) + ax.legend(["a", "b"], loc=loc) # same as above + return fig + + +@pytest.mark.mpl_image_compare +def test_contour_legend_with_label(): + """ + Support contour element labels. If has no label should trigger warning. + """ + figs = [] + label = "label" + + fig, axs = pplt.subplots(ncols=2) + ax = axs[0] + ax.contour(state.rand(5, 5), color="k", label=label, legend="b") + ax = axs[1] + ax.pcolor(state.rand(5, 5), label=label, legend="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_contour_legend_without_label(): + """ + Support contour element labels. If has no label should trigger warning. + """ + figs = [] + label = None + + fig, axs = pplt.subplots(ncols=2) + ax = axs[0] + ax.contour(state.rand(5, 5), color="k", label=label, legend="b") + ax = axs[1] + ax.pcolor(state.rand(5, 5), label=label, legend="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_histogram_legend(): + """ + Support complex histogram legends. + """ + pplt.rc.inlinefmt = "svg" + fig, ax = pplt.subplots() + res = ax.hist( + state.rand(500, 2), 4, labels=("label", "other"), edgefix=True, legend="b" + ) + ax.legend(res, loc="r", ncol=1) # should issue warning after ignoring numpy arrays + df = pd.DataFrame( + {"length": [1.5, 0.5, 1.2, 0.9, 3], "width": [0.7, 0.2, 0.15, 0.2, 1.1]}, + index=["pig", "rabbit", "duck", "chicken", "horse"], + ) + fig, axs = pplt.subplots(ncols=3) + ax = axs[0] + res = ax.hist(df, bins=3, legend=True, lw=3) + ax.legend(loc="b") + for ax, meth in zip(axs[1:], ("bar", "area")): + hs = getattr(ax, meth)(df, legend="ul", lw=3) + ax.legend(hs, loc="b") + return fig + + +@pytest.mark.mpl_image_compare +def test_multiple_calls(): + """ + Test successive plotting additions to guides. + """ + fig, ax = pplt.subplots() + ax.pcolor(state.rand(10, 10), colorbar="b") + ax.pcolor(state.rand(10, 5), cmap="grays", colorbar="b") + ax.pcolor(state.rand(10, 5), cmap="grays", colorbar="b") + + fig, ax = pplt.subplots() + data = state.rand(10, 5) + for i in range(data.shape[1]): + ax.plot(data[:, i], colorbar="b", label=f"x{i}", colorbar_kw={"label": "hello"}) + return fig + + +@pytest.mark.mpl_image_compare +def test_tuple_handles(): + """ + Test tuple legend handles. + """ + from matplotlib import legend_handler + + fig, ax = pplt.subplots(refwidth=3, abc="A.", abcloc="ul", span=False) + patches = ax.fill_between(state.rand(10, 3), stack=True) + lines = ax.line(1 + 0.5 * (state.rand(10, 3) - 0.5).cumsum(axis=0)) + # ax.legend([(handles[0], lines[1])], ['joint label'], loc='bottom', queue=True) + for hs in (lines, patches): + ax.legend( + [tuple(hs[:3]) if len(hs) == 3 else hs], + ["joint label"], + loc="bottom", + queue=True, + ncol=1, + handlelength=4.5, + handleheight=1.5, + handler_map={tuple: legend_handler.HandlerTuple(pad=0, ndivide=3)}, + ) + return fig diff --git a/proplot/tests/test_projections.py b/proplot/tests/test_projections.py new file mode 100644 index 000000000..2f597b352 --- /dev/null +++ b/proplot/tests/test_projections.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Test projection features. +""" +import cartopy.crs as ccrs +import matplotlib.pyplot as plt +import numpy as np +import proplot as pplt +import pytest + +state = np.random.RandomState(51423) + + +@pytest.mark.mpl_image_compare +def test_aspect_ratios(): + """ + Test aspect ratio adjustments. + """ + fig, axs = pplt.subplots(ncols=2) + axs[0].format(aspect=1.5) + fig, axs = pplt.subplots(ncols=2, proj=("cart", "cyl"), aspect=2) + axs[0].set_aspect(1) + return fig + + +if pplt.internals._version_mpl <= "3.2": + + @pytest.mark.mpl_image_compare + def test_basemap_labels(): + """ + Add basemap labels. + """ + fig, axs = pplt.subplots(ncols=2, proj="robin", refwidth=3, basemap=True) + axs.format(coast=True, labels="rt") + return fig + + +@pytest.mark.mpl_image_compare +def test_cartopy_labels(): + """ + Add cartopy labels. + """ + fig, axs = pplt.subplots(ncols=2, proj="robin", refwidth=3) + axs.format(coast=True, labels=True) + axs[0].format(inlinelabels=True) + axs[1].format(rotatelabels=True) + return fig + + +@pytest.mark.mpl_image_compare +def test_cartopy_contours(): + """ + Test bug with cartopy contours. Sometimes results in global coverage + with single color sometimes not. + """ + N = 10 + fig = plt.figure(figsize=(5, 2.5)) + ax = fig.add_subplot(projection=ccrs.Mollweide()) + ax.coastlines() + x = np.linspace(-180, 180, N) + y = np.linspace(-90, 90, N) + z = state.rand(N, N) * 10 - 5 + m = ax.contourf( + x, + y, + z, + transform=ccrs.PlateCarree(), + cmap="RdBu_r", + vmin=-5, + vmax=5, + ) + fig.colorbar(m, ax=ax) + fig = pplt.figure() + ax = fig.add_subplot(projection=pplt.Mollweide(), extent="auto") + ax.coastlines() + N = 10 + m = ax.contourf( + np.linspace(0, 180, N), + np.linspace(0, 90, N)[1::2], + state.rand(N // 2, N) * 10 + 5, + cmap="BuRd", + transform=pplt.PlateCarree(), + edgefix=False, + ) + fig.colorbar(m, ax=ax) + return fig + + +@pytest.mark.mpl_image_compare +def test_cartopy_manual(): + """ + Test alternative workflow without classes. + """ + fig = pplt.figure() + proj = pplt.Proj("npstere") + # fig.add_axes([0.1, 0.1, 0.9, 0.9], proj='geo', map_projection=proj) + fig.add_subplot(111, proj="geo", land=True, map_projection=proj) + return fig + + +@pytest.mark.mpl_image_compare +def test_three_axes(): + """ + Test basic 3D axes here. + """ + pplt.rc["tick.minor"] = False + fig, ax = pplt.subplots(proj="3d", outerpad=3) + return fig + + +@pytest.mark.mpl_image_compare +def test_projection_dicts(): + """ + Test projection dictionaries. + """ + fig = pplt.figure(refnum=1) + a = [[1, 0], [1, 4], [2, 4], [2, 4], [3, 4], [3, 0]] + fig.subplots(a, proj={1: "cyl", 2: "cart", 3: "cart", 4: "cart"}) + return fig + + +@pytest.mark.mpl_image_compare +def test_polar_projections(): + """ + Rigorously test polar features here. + """ + fig, ax = pplt.subplots(proj="polar") + ax.format( + rlabelpos=45, + thetadir=-1, + thetalines=90, + thetalim=(0, 270), + theta0="N", + r0=0, + rlim=(0.5, 1), + rlines=0.25, + ) + return fig diff --git a/proplot/tests/test_statistical_plotting.py b/proplot/tests/test_statistical_plotting.py new file mode 100644 index 000000000..620cd4ac0 --- /dev/null +++ b/proplot/tests/test_statistical_plotting.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# import proplot as pplt +import numpy as np, pandas as pd, proplot as pplt +import pytest + + +@pytest.mark.mpl_image_compare +def test_statistical_boxplot(): + # Sample data + N = 500 + state = np.random.RandomState(51423) + data1 = state.normal(size=(N, 5)) + 2 * (state.rand(N, 5) - 0.5) * np.arange(5) + data1 = pd.DataFrame(data1, columns=pd.Index(list("abcde"), name="label")) + data2 = state.rand(100, 7) + data2 = pd.DataFrame(data2, columns=pd.Index(list("abcdefg"), name="label")) + + # Figure + fig, axs = pplt.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], span=False) + axs.format(abc="A.", titleloc="l", grid=False, suptitle="Boxes and violins demo") + + # Box plots + ax = axs[0] + obj1 = ax.box(data1, means=True, marker="x", meancolor="r", fillcolor="gray4") + ax.format(title="Box plots") + + # Violin plots + ax = axs[1] + obj2 = ax.violin(data1, fillcolor="gray6", means=True, points=100) + ax.format(title="Violin plots") + + # Boxes with different colors + ax = axs[2] + ax.boxh(data2, cycle="pastel2") + ax.format(title="Multiple colors", ymargin=0.15) + return fig + + +@pytest.mark.mpl_image_compare +def test_panel_dist(): + # Sample data + N = 500 + state = np.random.RandomState(51423) + x = state.normal(size=(N,)) + y = state.normal(size=(N,)) + bins = pplt.arange(-3, 3, 0.25) + + # Histogram with marginal distributions + fig, axs = pplt.subplots(ncols=2, refwidth=2.3) + axs.format( + abc="A.", + abcloc="l", + titleabove=True, + ylabel="y axis", + suptitle="Histograms with marginal distributions", + ) + colors = ("indigo9", "red9") + titles = ("Group 1", "Group 2") + for ax, which, color, title in zip(axs, "lr", colors, titles): + ax.hist2d( + x, + y, + bins, + vmin=0, + vmax=10, + levels=50, + cmap=color, + colorbar="b", + colorbar_kw={"label": "count"}, + ) + color = pplt.scale_luminance(color, 1.5) # histogram colors + px = ax.panel(which, space=0) + px.histh(y, bins, color=color, fill=True, ec="k") + px.format(grid=False, xlocator=[], xreverse=(which == "l")) + px = ax.panel("t", space=0) + px.hist(x, bins, color=color, fill=True, ec="k") + px.format(grid=False, ylocator=[], title=title, titleloc="l") + return fig diff --git a/proplot/tests/test_subplots.py b/proplot/tests/test_subplots.py new file mode 100644 index 000000000..55b125083 --- /dev/null +++ b/proplot/tests/test_subplots.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +Test subplot layout. +""" +import numpy as np, proplot as pplt, pytest + + +@pytest.mark.mpl_image_compare +def test_align_labels(): + """ + Test spanning and aligned labels. + """ + fig, axs = pplt.subplots( + [[2, 1, 4], [2, 3, 5]], refnum=2, refwidth=1.5, align=1, span=0 + ) + fig.format(xlabel="xlabel", ylabel="ylabel", abc="A.", abcloc="ul") + axs[0].format(ylim=(10000, 20000)) + axs[-1].panel_axes("bottom", share=False) + return fig + + +@pytest.mark.mpl_image_compare +def test_share_all_basic(): + """ + Test sharing level all. + """ + # Simple example + N = 10 + fig, axs = pplt.subplots(nrows=1, ncols=2, refwidth=1.5, share="all") + axs[0].plot(np.arange(N) * 1e2, np.arange(N) * 1e4) + # Complex example + fig, axs = pplt.subplots(nrows=2, ncols=2, refwidth=1.5, share="all") + axs[0].panel("b") + pax = axs[0].panel("r") + pax.format(ylabel="label") + axs[0].plot(np.arange(N) * 1e2, np.arange(N) * 1e4) + return fig + + +@pytest.mark.mpl_image_compare +def test_span_labels(): + """ + Rigorous tests of spanning and aligned labels feature. + """ + fig, axs = pplt.subplots([[1, 2, 4], [1, 3, 5]], refwidth=1.5, share=0, span=1) + fig.format(xlabel="xlabel", ylabel="ylabel", abc="A.", abcloc="ul") + axs[1].format() # xlabel='xlabel') + axs[2].format() + return fig + + +@pytest.mark.mpl_image_compare +def test_title_deflection(): + """ + Test the deflection of titles above and below panels. + """ + fig, ax = pplt.subplots() + # ax.format(abc='A.', title='Title', titleloc='left', titlepad=30) + tax = ax.panel_axes("top") + ax.format(titleabove=False) # redirects to bottom + ax.format(abc="A.", title="Title", titleloc="left", titlepad=50) + ax.format(xlabel="xlabel", ylabel="ylabel", ylabelpad=50) + tax.format(title="Fear Me", title_kw={"size": "x-large"}) + tax.format(ultitle="Inner", titlebbox=True, title_kw={"size": "med-large"}) + return fig + + +@pytest.mark.mpl_image_compare +def test_complex_ticks(): + """ + Normally title offset with these different tick arrangements is tricky + but `_update_title_position` accounts for edge cases. + """ + fig, axs = pplt.subplots(ncols=2) + axs[0].format( + xtickloc="both", + xticklabelloc="top", + xlabelloc="top", + title="title", + xlabel="xlabel", + suptitle="Test", + ) + axs[1].format( + xtickloc="both", + xticklabelloc="top", + # xlabelloc='top', + xlabel="xlabel", + title="title", + suptitle="Test", + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_both_ticklabels(): + """ + Test both tick labels. + """ + fig, ax = pplt.subplots() # when both, have weird bug + ax.format(xticklabelloc="both", title="title", suptitle="Test") + fig, ax = pplt.subplots() # when *just top*, bug disappears + ax.format(xtickloc="top", xticklabelloc="top", title="title", suptitle="Test") + fig, ax = pplt.subplots() # not sure here + ax.format(xtickloc="both", xticklabelloc="neither", suptitle="Test") + fig, ax = pplt.subplots() # doesn't seem to change the title offset though + ax.format(xtickloc="top", xticklabelloc="neither", suptitle="Test") + return fig + + +def test_gridspec_copies(): + """ + Test whether gridspec copies work. + """ + fig1, ax = pplt.subplots(ncols=2) + gs = fig1.gridspec.copy(left=5, wspace=0, right=5) + return fig1 + fig2 = pplt.figure() + fig2.add_subplots(gs) + fig = pplt.figure() + with pytest.raises(ValueError): + fig.add_subplots(gs) # should raise error + + +@pytest.mark.mpl_image_compare +def test_aligned_outer_guides(): + """ + Test alignment adjustment. + """ + fig, ax = pplt.subplot() + h1 = ax.plot(np.arange(5), label="foo") + h2 = ax.plot(np.arange(5) + 1, label="bar") + h3 = ax.plot(np.arange(5) + 2, label="baz") + ax.legend(h1, loc="bottom", align="left") + ax.legend(h2, loc="bottom", align="right") + ax.legend(h3, loc="b", align="c") + ax.colorbar("magma", loc="right", align="top", shrink=0.4) # same as length + ax.colorbar("magma", loc="right", align="bottom", shrink=0.4) + ax.colorbar("magma", loc="left", align="top", length=0.6) # should offset + ax.colorbar("magma", loc="left", align="bottom", length=0.6) + ax.legend(h1, loc="top", align="right", pad="4pt", frame=False) + ax.format(title="Very long title", titlepad=6, titleloc="left") + return fig + + +def test_reference_aspect(): + """ + Rigorous test of reference aspect ratio accuracy. + """ + # A simple test + refwidth = 1.5 + fig, axs = pplt.subplots(ncols=2, refwidth=refwidth) + fig.auto_layout() + + assert np.isclose(refwidth, axs[fig._refnum - 1]._get_size_inches()[0]) + + # A test with funky layout + refwidth = 1.5 + fig, axs = pplt.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], ref=3, refwidth=refwidth) + axs[1].panel_axes("left") + axs.format(xlocator=0.2, ylocator=0.2) + fig.auto_layout() + assert np.isclose(refwidth, axs[fig._refnum - 1]._get_size_inches()[0]) + + # A test with panels + refwidth = 2.0 + fig, axs = pplt.subplots( + [[1, 1, 2], [3, 4, 5], [3, 4, 6]], hratios=(2, 1, 1), refwidth=refwidth + ) + axs[2].panel_axes("right", width=0.5) + axs[0].panel_axes("bottom", width=0.5) + axs[3].panel_axes("left", width=0.5) + fig.auto_layout() + assert np.isclose(refwidth, axs[fig._refnum - 1]._get_size_inches()[0]) + return fig diff --git a/proplot/ticker.py b/proplot/ticker.py index 7dd734890..27ac1652d 100644 --- a/proplot/ticker.py +++ b/proplot/ticker.py @@ -26,25 +26,25 @@ LatitudeFormatter = LongitudeFormatter = _PlateCarreeFormatter = object __all__ = [ - 'IndexLocator', - 'DiscreteLocator', - 'DegreeLocator', - 'LongitudeLocator', - 'LatitudeLocator', - 'AutoFormatter', - 'SimpleFormatter', - 'IndexFormatter', - 'SciFormatter', - 'SigFigFormatter', - 'FracFormatter', - 'DegreeFormatter', - 'LongitudeFormatter', - 'LatitudeFormatter', + "IndexLocator", + "DiscreteLocator", + "DegreeLocator", + "LongitudeLocator", + "LatitudeLocator", + "AutoFormatter", + "SimpleFormatter", + "IndexFormatter", + "SciFormatter", + "SigFigFormatter", + "FracFormatter", + "DegreeFormatter", + "LongitudeFormatter", + "LatitudeFormatter", ] -REGEX_ZERO = re.compile('\\A[-\N{MINUS SIGN}]?0(.0*)?\\Z') -REGEX_MINUS = re.compile('\\A[-\N{MINUS SIGN}]\\Z') -REGEX_MINUS_ZERO = re.compile('\\A[-\N{MINUS SIGN}]0(.0*)?\\Z') +REGEX_ZERO = re.compile("\\A[-\N{MINUS SIGN}]?0(.0*)?\\Z") +REGEX_MINUS = re.compile("\\A[-\N{MINUS SIGN}]\\Z") +REGEX_MINUS_ZERO = re.compile("\\A[-\N{MINUS SIGN}]0(.0*)?\\Z") _precision_docstring = """ precision : int, default: {6, 2} @@ -79,10 +79,10 @@ pos : float, optional The position. """ -docstring._snippet_manager['ticker.precision'] = _precision_docstring -docstring._snippet_manager['ticker.zerotrim'] = _zerotrim_docstring -docstring._snippet_manager['ticker.auto'] = _auto_docstring -docstring._snippet_manager['ticker.call'] = _formatter_call +docstring._snippet_manager["ticker.precision"] = _precision_docstring +docstring._snippet_manager["ticker.zerotrim"] = _zerotrim_docstring +docstring._snippet_manager["ticker.auto"] = _auto_docstring +docstring._snippet_manager["ticker.call"] = _formatter_call _dms_docstring = """ Parameters @@ -91,14 +91,14 @@ Locate the ticks on clean degree-minute-second intervals and format the ticks with minutes and seconds instead of decimals. """ -docstring._snippet_manager['ticker.dms'] = _dms_docstring +docstring._snippet_manager["ticker.dms"] = _dms_docstring def _default_precision_zerotrim(precision=None, zerotrim=None): """ Return the default zerotrim and precision. Shared by several formatters. """ - zerotrim = _not_none(zerotrim, rc['formatter.zerotrim']) + zerotrim = _not_none(zerotrim, rc["formatter.zerotrim"]) if precision is None: precision = 6 if zerotrim else 2 return precision, zerotrim @@ -109,6 +109,7 @@ class IndexLocator(mticker.Locator): Format numbers by assigning fixed strings to non-negative indices. The ticks are restricted to the extent of plotted content when content is present. """ + def __init__(self, base=1, offset=0): self._base = base self._offset = offset @@ -144,11 +145,12 @@ class DiscreteLocator(mticker.Locator): location list, and step sizes along the location list are restricted to "nice" intervals by default. """ + default_params = { - 'nbins': None, - 'minor': False, - 'steps': np.array([1, 2, 3, 4, 5, 6, 8, 10]), - 'min_n_ticks': 2 + "nbins": None, + "minor": False, + "steps": np.array([1, 2, 3, 4, 5, 6, 8, 10]), + "min_n_ticks": 2, } @docstring._snippet_manager @@ -187,7 +189,7 @@ def set_params(self, steps=None, nbins=None, minor=None, min_n_ticks=None): if steps is not None: steps = np.unique(np.array(steps, dtype=int)) # also sorts, makes 1D if np.any(steps < 1) or np.any(steps > 10): - raise ValueError('Steps must fall between one and ten (inclusive).') + raise ValueError("Steps must fall between one and ten (inclusive).") if steps[0] != 1: steps = np.concatenate([[1], steps]) if steps[-1] != 10: @@ -238,10 +240,10 @@ def tick_values(self, vmin, vmax): # noqa: U100 if step % i == 0: step = step // i break - diff = np.abs(np.diff(locs[:step + 1:step])) - offset, = np.where(np.isclose(locs % diff if diff.size else 0.0, 0.0)) + diff = np.abs(np.diff(locs[: step + 1 : step])) + (offset,) = np.where(np.isclose(locs % diff if diff.size else 0.0, 0.0)) offset = offset[0] if offset.size else np.argmin(np.abs(locs)) - return locs[offset % step::step] # even multiples from zero or zero-close + return locs[offset % step :: step] # even multiples from zero or zero-close class DegreeLocator(mticker.MaxNLocator): @@ -249,6 +251,7 @@ class DegreeLocator(mticker.MaxNLocator): Locate geographic gridlines with degree-minute-second support. Adapted from cartopy. """ + # NOTE: This is identical to cartopy except they only define LongitutdeLocator # for common methods whereas we use DegreeLocator. More intuitive this way in # case users need degree-minute-seconds for non-specific degree axis. @@ -266,8 +269,8 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def set_params(self, **kwargs): - if 'dms' in kwargs: - self._dms = kwargs.pop('dms') + if "dms" in kwargs: + self._dms = kwargs.pop("dms") super().set_params(**kwargs) def _guess_steps(self, vmin, vmax): @@ -295,6 +298,7 @@ class LongitudeLocator(DegreeLocator): Locate longitude gridlines with degree-minute-second support. Adapted from cartopy. """ + @docstring._snippet_manager def __init__(self, *args, **kwargs): """ @@ -308,6 +312,7 @@ class LatitudeLocator(DegreeLocator): Locate latitude gridlines with degree-minute-second support. Adapted from cartopy. """ + @docstring._snippet_manager def __init__(self, *args, **kwargs): """ @@ -335,12 +340,17 @@ class AutoFormatter(mticker.ScalarFormatter): The default formatter used for proplot tick labels. Replaces `~matplotlib.ticker.ScalarFormatter`. """ + @docstring._snippet_manager def __init__( self, - zerotrim=None, tickrange=None, wraprange=None, - prefix=None, suffix=None, negpos=None, - **kwargs + zerotrim=None, + tickrange=None, + wraprange=None, + prefix=None, + suffix=None, + negpos=None, + **kwargs, ): """ Parameters @@ -369,13 +379,13 @@ def __init__( """ tickrange = tickrange or (-np.inf, np.inf) super().__init__(**kwargs) - zerotrim = _not_none(zerotrim, rc['formatter.zerotrim']) + zerotrim = _not_none(zerotrim, rc["formatter.zerotrim"]) self._zerotrim = zerotrim self._tickrange = tickrange self._wraprange = wraprange - self._prefix = prefix or '' - self._suffix = suffix or '' - self._negpos = negpos or '' + self._prefix = prefix or "" + self._suffix = suffix or "" + self._negpos = negpos or "" @docstring._snippet_manager def __call__(self, x, pos=None): @@ -385,7 +395,7 @@ def __call__(self, x, pos=None): # Tick range limitation x = self._wrap_tick_range(x, self._wraprange) if self._outside_tick_range(x, self._tickrange): - return '' + return "" # Negative positive handling x, tail = self._neg_pos_format(x, self._negpos, wraprange=self._wraprange) @@ -418,9 +428,9 @@ def _add_prefix_suffix(string, prefix=None, suffix=None): """ Add prefix and suffix to string. """ - sign = '' - prefix = prefix or '' - suffix = suffix or '' + sign = "" + prefix = prefix or "" + suffix = suffix or "" if string and REGEX_MINUS.match(string[0]): sign, string = string[0], string[1:] return sign + prefix + string + suffix @@ -441,23 +451,23 @@ def _fix_small_number(self, x, string, precision_offset=2): if m and x != 0: # Get initial precision spit out by algorithm - decimals, = m.groups() + (decimals,) = m.groups() precision_init = len(decimals.lstrip(decimal_point)) if decimals else 0 # Format with precision below floating point error - x -= getattr(self, 'offset', 0) # guard against API change - x /= 10 ** getattr(self, 'orderOfMagnitude', 0) # guard against API change + x -= getattr(self, "offset", 0) # guard against API change + x /= 10 ** getattr(self, "orderOfMagnitude", 0) # guard against API change precision_true = max(0, self._decimal_place(x)) precision_max = max(0, np.finfo(type(x)).precision - precision_offset) precision = min(precision_true, precision_max) - string = ('{:.%df}' % precision).format(x) + string = ("{:.%df}" % precision).format(x) # If zero ignoring floating point error then match original precision if REGEX_ZERO.match(string): - string = ('{:.%df}' % precision_init).format(0) + string = ("{:.%df}" % precision_init).format(0) # Fix decimal point - string = string.replace('.', decimal_point) + string = string.replace(".", decimal_point) return string @@ -473,8 +483,8 @@ def _get_default_decimal_point(use_locale=None): """ Get decimal point symbol for current locale. Called externally. """ - use_locale = _not_none(use_locale, rc['formatter.use_locale']) - return locale.localeconv()['decimal_point'] if use_locale else '.' + use_locale = _not_none(use_locale, rc["formatter.use_locale"]) + return locale.localeconv()["decimal_point"] if use_locale else "." @staticmethod def _decimal_place(x): @@ -492,8 +502,8 @@ def _minus_format(string): """ Format the minus sign and avoid "negative zero," e.g. ``-0.000``. """ - if rc['axes.unicode_minus'] and not rc['text.usetex']: - string = string.replace('-', '\N{MINUS SIGN}') + if rc["axes.unicode_minus"] and not rc["text.usetex"]: + string = string.replace("-", "\N{MINUS SIGN}") if REGEX_MINUS_ZERO.match(string): string = string[1:] return string @@ -506,14 +516,14 @@ def _neg_pos_format(x, negpos, wraprange=None): # NOTE: If input is a symmetric wraprange, the value conceptually has # no "sign", so trim tail and format as absolute value. if not negpos or x == 0: - tail = '' + tail = "" elif ( wraprange is not None and np.isclose(-wraprange[0], wraprange[1]) and np.any(np.isclose(x, wraprange)) ): x = abs(x) - tail = '' + tail = "" elif x > 0: tail = negpos[1] else: @@ -530,12 +540,12 @@ def _outside_tick_range(x, tickrange): return (x + eps) < tickrange[0] or (x - eps) > tickrange[1] @staticmethod - def _trim_trailing_zeros(string, decimal_point='.'): + def _trim_trailing_zeros(string, decimal_point="."): """ Sanitize tick label strings. """ if decimal_point in string: - string = string.rstrip('0').rstrip(decimal_point) + string = string.rstrip("0").rstrip(decimal_point) return string @staticmethod @@ -556,11 +566,17 @@ class SimpleFormatter(mticker.Formatter): but suitable for arbitrary formatting not necessarily associated with an `~matplotlib.axis.Axis` instance. """ + @docstring._snippet_manager def __init__( - self, precision=None, zerotrim=None, - tickrange=None, wraprange=None, - prefix=None, suffix=None, negpos=None, + self, + precision=None, + zerotrim=None, + tickrange=None, + wraprange=None, + prefix=None, + suffix=None, + negpos=None, ): """ Parameters @@ -576,9 +592,9 @@ def __init__( """ precision, zerotrim = _default_precision_zerotrim(precision, zerotrim) self._precision = precision - self._prefix = prefix or '' - self._suffix = suffix or '' - self._negpos = negpos or '' + self._prefix = prefix or "" + self._suffix = suffix or "" + self._negpos = negpos or "" self._tickrange = tickrange or (-np.inf, np.inf) self._wraprange = wraprange self._zerotrim = zerotrim @@ -591,7 +607,7 @@ def __call__(self, x, pos=None): # noqa: U100 # Tick range limitation x = AutoFormatter._wrap_tick_range(x, self._wraprange) if AutoFormatter._outside_tick_range(x, self._tickrange): - return '' + return "" # Negative positive handling x, tail = AutoFormatter._neg_pos_format( @@ -600,8 +616,8 @@ def __call__(self, x, pos=None): # noqa: U100 # Default string formatting decimal_point = AutoFormatter._get_default_decimal_point() - string = ('{:.%df}' % self._precision).format(x) - string = string.replace('.', decimal_point) + string = ("{:.%df}" % self._precision).format(x) + string = string.replace(".", decimal_point) # Custom string formatting string = AutoFormatter._minus_format(string) @@ -619,6 +635,7 @@ class IndexFormatter(mticker.Formatter): Format numbers by assigning fixed strings to non-negative indices. Generally paired with `IndexLocator` or `~matplotlib.ticker.FixedLocator`. """ + # NOTE: This was deprecated in matplotlib 3.3. For details check out # https://github.com/matplotlib/matplotlib/issues/16631 and bring some popcorn. def __init__(self, labels): @@ -628,7 +645,7 @@ def __init__(self, labels): def __call__(self, x, pos=None): # noqa: U100 i = int(round(x)) if i < 0 or i >= self.n: - return '' + return "" else: return self.labels[i] @@ -637,6 +654,7 @@ class SciFormatter(mticker.Formatter): """ Format numbers with scientific notation. """ + @docstring._snippet_manager def __init__(self, precision=None, zerotrim=None): """ @@ -661,8 +679,8 @@ def __call__(self, x, pos=None): # noqa: U100 """ # Get string decimal_point = AutoFormatter._get_default_decimal_point() - string = ('{:.%de}' % self._precision).format(x) - parts = string.split('e') + string = ("{:.%de}" % self._precision).format(x) + parts = string.split("e") # Trim trailing zeros significand = parts[0].rstrip(decimal_point) @@ -670,26 +688,27 @@ def __call__(self, x, pos=None): # noqa: U100 significand = AutoFormatter._trim_trailing_zeros(significand, decimal_point) # Get sign and exponent - sign = parts[1][0].replace('+', '') - exponent = parts[1][1:].lstrip('0') + sign = parts[1][0].replace("+", "") + exponent = parts[1][1:].lstrip("0") if exponent: - exponent = f'10^{{{sign}{exponent}}}' + exponent = f"10^{{{sign}{exponent}}}" if significand and exponent: - string = rf'{significand}{{\times}}{exponent}' + string = rf"{significand}{{\times}}{exponent}" else: - string = rf'{significand}{exponent}' + string = rf"{significand}{exponent}" # Ensure unicode minus sign string = AutoFormatter._minus_format(string) # Return TeX string - return f'${string}$' + return f"${string}$" class SigFigFormatter(mticker.Formatter): """ Format numbers by retaining the specified number of significant digits. """ + @docstring._snippet_manager def __init__(self, sigfig=None, zerotrim=None, base=None): """ @@ -708,7 +727,7 @@ def __init__(self, sigfig=None, zerotrim=None, base=None): proplot.ticker.AutoFormatter """ self._sigfig = _not_none(sigfig, 3) - self._zerotrim = _not_none(zerotrim, rc['formatter.zerotrim']) + self._zerotrim = _not_none(zerotrim, rc["formatter.zerotrim"]) self._base = _not_none(base, 1) @docstring._snippet_manager @@ -718,14 +737,14 @@ def __call__(self, x, pos=None): # noqa: U100 """ # Limit to significant figures digits = AutoFormatter._decimal_place(x) + self._sigfig - 1 - scale = self._base * 10 ** -digits + scale = self._base * 10**-digits x = scale * round(x / scale) # Create the string decimal_point = AutoFormatter._get_default_decimal_point() precision = max(0, digits) + max(0, AutoFormatter._decimal_place(self._base)) - string = ('{:.%df}' % precision).format(x) - string = string.replace('.', decimal_point) + string = ("{:.%df}" % precision).format(x) + string = string.replace(".", decimal_point) # Custom string formatting string = AutoFormatter._minus_format(string) @@ -739,7 +758,8 @@ class FracFormatter(mticker.Formatter): Format numbers as integers or integer fractions. Optionally express the values relative to some constant like `numpy.pi`. """ - def __init__(self, symbol='', number=1): + + def __init__(self, symbol="", number=1): r""" Parameters ---------- @@ -770,21 +790,21 @@ def __call__(self, x, pos=None): # noqa: U100 frac = Fraction(x / self._number).limit_denominator() symbol = self._symbol if x == 0: - string = '0' + string = "0" elif frac.denominator == 1: # denominator is one if frac.numerator == 1 and symbol: - string = f'{symbol:s}' + string = f"{symbol:s}" elif frac.numerator == -1 and symbol: - string = f'-{symbol:s}' + string = f"-{symbol:s}" else: - string = f'{frac.numerator:d}{symbol:s}' + string = f"{frac.numerator:d}{symbol:s}" else: if frac.numerator == 1 and symbol: # numerator is +/-1 - string = f'{symbol:s}/{frac.denominator:d}' + string = f"{symbol:s}/{frac.denominator:d}" elif frac.numerator == -1 and symbol: - string = f'-{symbol:s}/{frac.denominator:d}' + string = f"-{symbol:s}/{frac.denominator:d}" else: # and again make sure we use unicode minus! - string = f'{frac.numerator:d}{symbol:s}/{frac.denominator:d}' + string = f"{frac.numerator:d}{symbol:s}/{frac.denominator:d}" string = AutoFormatter._minus_format(string) return string @@ -793,12 +813,14 @@ class _CartopyFormatter(object): """ Mixin class for cartopy formatters. """ + # NOTE: Cartopy formatters pre 0.18 required axis, and *always* translated # input values from map projection coordinates to Plate Carrée coordinates. # After 0.18 you can avoid this behavior by not setting axis but really # dislike that inconsistency. Solution is temporarily assign PlateCarre(). def __init__(self, *args, **kwargs): import cartopy # noqa: F401 (ensure available) + super().__init__(*args, **kwargs) def __call__(self, value, pos=None): @@ -814,6 +836,7 @@ class DegreeFormatter(_CartopyFormatter, _PlateCarreeFormatter): Formatter for longitude and latitude gridline labels. Adapted from cartopy. """ + @docstring._snippet_manager def __init__(self, *args, **kwargs): """ @@ -825,7 +848,7 @@ def _apply_transform(self, value, *args, **kwargs): # noqa: U100 return value def _hemisphere(self, value, *args, **kwargs): # noqa: U100 - return '' + return "" class LongitudeFormatter(_CartopyFormatter, LongitudeFormatter): @@ -833,6 +856,7 @@ class LongitudeFormatter(_CartopyFormatter, LongitudeFormatter): Format longitude gridline labels. Adapted from `cartopy.mpl.ticker.LongitudeFormatter`. """ + @docstring._snippet_manager def __init__(self, *args, **kwargs): """ @@ -846,6 +870,7 @@ class LatitudeFormatter(_CartopyFormatter, LatitudeFormatter): Format latitude gridline labels. Adapted from `cartopy.mpl.ticker.LatitudeFormatter`. """ + @docstring._snippet_manager def __init__(self, *args, **kwargs): """ diff --git a/proplot/ui.py b/proplot/ui.py index b1e046376..6f72c57cb 100644 --- a/proplot/ui.py +++ b/proplot/ui.py @@ -11,15 +11,15 @@ from .internals import _not_none, _pop_params, _pop_props, _pop_rc, docstring __all__ = [ - 'figure', - 'subplot', - 'subplots', - 'show', - 'close', - 'switch_backend', - 'ion', - 'ioff', - 'isinteractive', + "figure", + "subplot", + "subplots", + "show", + "close", + "switch_backend", + "ion", + "ioff", + "isinteractive", ] @@ -27,7 +27,7 @@ _pyplot_docstring = """ This is included so you don't have to import `~matplotlib.pyplot`. """ -docstring._snippet_manager['ui.pyplot'] = _pyplot_docstring +docstring._snippet_manager["ui.pyplot"] = _pyplot_docstring def _parse_figsize(kwargs): @@ -36,15 +36,15 @@ def _parse_figsize(kwargs): """ # WARNING: Cannot have Figure.__init__() interpret figsize() because # the figure manager fills it with the matplotlib default. - figsize = kwargs.pop('figsize', None) - figwidth = kwargs.pop('figwidth', None) - figheight = kwargs.pop('figheight', None) + figsize = kwargs.pop("figsize", None) + figwidth = kwargs.pop("figwidth", None) + figheight = kwargs.pop("figheight", None) if figsize is not None: figsize_width, figsize_height = figsize figwidth = _not_none(figwidth=figwidth, figsize_width=figsize_width) figheight = _not_none(figheight=figheight, figsize_height=figsize_height) - kwargs['figwidth'] = figwidth - kwargs['figheight'] = figheight + kwargs["figwidth"] = figwidth + kwargs["figheight"] = figheight @docstring._snippet_manager @@ -174,11 +174,11 @@ def subplot(**kwargs): """ _parse_figsize(kwargs) rc_kw, rc_mode = _pop_rc(kwargs) - kwsub = _pop_props(kwargs, 'patch') # e.g. 'color' + kwsub = _pop_props(kwargs, "patch") # e.g. 'color' kwsub.update(_pop_params(kwargs, pfigure.Figure._parse_proj)) for sig in paxes.Axes._format_signatures.values(): kwsub.update(_pop_params(kwargs, sig)) - kwargs['aspect'] = kwsub.pop('aspect', None) # keyword conflict + kwargs["aspect"] = kwsub.pop("aspect", None) # keyword conflict fig = figure(rc_kw=rc_kw, **kwargs) ax = fig.add_subplot(rc_kw=rc_kw, **kwsub) return fig, ax @@ -219,15 +219,15 @@ def subplots(*args, **kwargs): """ _parse_figsize(kwargs) rc_kw, rc_mode = _pop_rc(kwargs) - kwsubs = _pop_props(kwargs, 'patch') # e.g. 'color' + kwsubs = _pop_props(kwargs, "patch") # e.g. 'color' kwsubs.update(_pop_params(kwargs, pfigure.Figure._add_subplots)) kwsubs.update(_pop_params(kwargs, pgridspec.GridSpec._update_params)) for sig in paxes.Axes._format_signatures.values(): kwsubs.update(_pop_params(kwargs, sig)) - for key in ('subplot_kw', 'gridspec_kw'): # deprecated args + for key in ("subplot_kw", "gridspec_kw"): # deprecated args if key in kwargs: kwsubs[key] = kwargs.pop(key) - kwargs['aspect'] = kwsubs.pop('aspect', None) # keyword conflict + kwargs["aspect"] = kwsubs.pop("aspect", None) # keyword conflict fig = figure(rc_kw=rc_kw, **kwargs) axs = fig.add_subplots(*args, rc_kw=rc_kw, **kwsubs) return fig, axs diff --git a/proplot/utils.py b/proplot/utils.py index 2b0da3c1f..783bbdf45 100644 --- a/proplot/utils.py +++ b/proplot/utils.py @@ -18,41 +18,41 @@ from .internals import _not_none, docstring, warnings __all__ = [ - 'arange', - 'edges', - 'edges2d', - 'get_colors', - 'set_hue', - 'set_saturation', - 'set_luminance', - 'set_alpha', - 'shift_hue', - 'scale_saturation', - 'scale_luminance', - 'to_hex', - 'to_rgb', - 'to_xyz', - 'to_rgba', - 'to_xyza', - 'units', - 'shade', # deprecated - 'saturate', # deprecated + "arange", + "edges", + "edges2d", + "get_colors", + "set_hue", + "set_saturation", + "set_luminance", + "set_alpha", + "shift_hue", + "scale_saturation", + "scale_luminance", + "to_hex", + "to_rgb", + "to_xyz", + "to_rgba", + "to_xyza", + "units", + "shade", # deprecated + "saturate", # deprecated ] UNIT_REGEX = re.compile( - r'\A([-+]?[0-9._]+(?:[eE][-+]?[0-9_]+)?)(.*)\Z' # float with trailing units + r"\A([-+]?[0-9._]+(?:[eE][-+]?[0-9_]+)?)(.*)\Z" # float with trailing units ) UNIT_DICT = { - 'in': 1.0, - 'ft': 12.0, - 'yd': 36.0, - 'm': 39.37, - 'dm': 3.937, - 'cm': 0.3937, - 'mm': 0.03937, - 'pc': 1 / 6.0, - 'pt': 1 / 72.0, - 'ly': 3.725e17, + "in": 1.0, + "ft": 12.0, + "yd": 36.0, + "m": 39.37, + "dm": 3.937, + "cm": 0.3937, + "mm": 0.03937, + "pc": 1 / 6.0, + "pt": 1 / 72.0, + "ly": 3.725e17, } @@ -91,25 +91,27 @@ An 8-digit HEX string indicating the red, green, blue, and alpha channel values. """ -docstring._snippet_manager['utils.color'] = _docstring_rgba -docstring._snippet_manager['utils.hex'] = _docstring_hex -docstring._snippet_manager['utils.space'] = _docstring_space -docstring._snippet_manager['utils.to'] = _docstring_to_rgb +docstring._snippet_manager["utils.color"] = _docstring_rgba +docstring._snippet_manager["utils.hex"] = _docstring_hex +docstring._snippet_manager["utils.space"] = _docstring_space +docstring._snippet_manager["utils.to"] = _docstring_to_rgb def _keep_units(func): """ Very simple decorator to strip and re-apply the same units. """ + # NOTE: Native UnitRegistry.wraps() is not sufficient since it enforces # unit types rather than arbitrary units. This wrapper is similar. @functools.wraps(func) def _with_stripped_units(data, *args, **kwargs): units = 1 - if hasattr(data, 'units') and hasattr(data, 'magnitude'): + if hasattr(data, "units") and hasattr(data, "magnitude"): data, units = data.magnitude, data.units result = func(data, *args, **kwargs) return result * units + return _with_stripped_units @@ -155,7 +157,7 @@ def arange(min_, *args): max_ = args[0] step = args[1] else: - raise ValueError('Function takes from one to three arguments.') + raise ValueError("Function takes from one to three arguments.") # All input is integer if all(isinstance(val, Integral) for val in (min_, max_, step)): min_, max_, step = np.int64(min_), np.int64(max_), np.int64(step) @@ -237,7 +239,7 @@ def edges2d(z): """ z = np.asarray(z) if z.ndim != 2: - raise ValueError(f'Input must be a 2D array, but got {z.ndim}D.') + raise ValueError(f"Input must be a 2D array, but got {z.ndim}D.") ny, nx = z.shape zb = np.zeros((ny + 1, nx + 1)) @@ -275,8 +277,9 @@ def get_colors(*args, **kwargs): proplot.constructor.Colormap """ from .constructor import Cycle # delayed to avoid cyclic imports + cycle = Cycle(*args, **kwargs) - colors = [to_hex(dict_['color']) for dict_ in cycle] + colors = [to_hex(dict_["color"]) for dict_ in cycle] return colors @@ -291,7 +294,7 @@ def _transform_color(func, color, space): @docstring._snippet_manager -def shift_hue(color, shift=0, space='hcl'): +def shift_hue(color, shift=0, space="hcl"): """ Shift the hue channel of a color. @@ -315,6 +318,7 @@ def shift_hue(color, shift=0, space='hcl'): scale_saturation scale_luminance """ + def func(channels): channels[0] += shift channels[0] %= 360 @@ -324,7 +328,7 @@ def func(channels): @docstring._snippet_manager -def scale_saturation(color, scale=1, space='hcl'): +def scale_saturation(color, scale=1, space="hcl"): """ Scale the saturation channel of a color. @@ -348,6 +352,7 @@ def scale_saturation(color, scale=1, space='hcl'): shift_hue scale_luminance """ + def func(channels): channels[1] *= scale return channels @@ -356,7 +361,7 @@ def func(channels): @docstring._snippet_manager -def scale_luminance(color, scale=1, space='hcl'): +def scale_luminance(color, scale=1, space="hcl"): """ Scale the luminance channel of a color. @@ -380,6 +385,7 @@ def scale_luminance(color, scale=1, space='hcl'): shift_hue scale_saturation """ + def func(channels): channels[2] *= scale return channels @@ -388,7 +394,7 @@ def func(channels): @docstring._snippet_manager -def set_hue(color, hue, space='hcl'): +def set_hue(color, hue, space="hcl"): """ Return a color with a different hue and the same luminance and saturation as the input color. @@ -413,6 +419,7 @@ def set_hue(color, hue, space='hcl'): scale_saturation scale_luminance """ + def func(channels): channels[0] = hue return channels @@ -421,7 +428,7 @@ def func(channels): @docstring._snippet_manager -def set_saturation(color, saturation, space='hcl'): +def set_saturation(color, saturation, space="hcl"): """ Return a color with a different saturation and the same hue and luminance as the input color. @@ -446,6 +453,7 @@ def set_saturation(color, saturation, space='hcl'): scale_saturation scale_luminance """ + def func(channels): channels[1] = saturation return channels @@ -454,7 +462,7 @@ def func(channels): @docstring._snippet_manager -def set_luminance(color, luminance, space='hcl'): +def set_luminance(color, luminance, space="hcl"): """ Return a color with a different luminance and the same hue and saturation as the input color. @@ -479,6 +487,7 @@ def set_luminance(color, luminance, space='hcl'): scale_saturation scale_luminance """ + def func(channels): channels[2] = luminance return channels @@ -521,6 +530,7 @@ def _translate_cycle_color(color, cycle=None): """ if isinstance(cycle, str): from .colors import _cmap_database + try: cycle = _cmap_database[cycle].colors except (KeyError, AttributeError): @@ -530,24 +540,24 @@ def _translate_cycle_color(color, cycle=None): if isinstance(cmap, mcolors.ListedColormap) ) raise ValueError( - f'Invalid color cycle {cycle!r}. Options are: ' - + ', '.join(map(repr, cycles)) - + '.' + f"Invalid color cycle {cycle!r}. Options are: " + + ", ".join(map(repr, cycles)) + + "." ) elif cycle is None: - cycle = rc_matplotlib['axes.prop_cycle'].by_key() - if 'color' not in cycle: - cycle = ['k'] + cycle = rc_matplotlib["axes.prop_cycle"].by_key() + if "color" not in cycle: + cycle = ["k"] else: - cycle = cycle['color'] + cycle = cycle["color"] else: - raise ValueError(f'Invalid cycle {cycle!r}.') + raise ValueError(f"Invalid cycle {cycle!r}.") return cycle[int(color[-1]) % len(cycle)] @docstring._snippet_manager -def to_hex(color, space='rgb', cycle=None, keep_alpha=True): +def to_hex(color, space="rgb", cycle=None, keep_alpha=True): """ Translate the color from an arbitrary colorspace to a HEX string. This is a generalization of `matplotlib.colors.to_hex`. @@ -575,7 +585,7 @@ def to_hex(color, space='rgb', cycle=None, keep_alpha=True): @docstring._snippet_manager -def to_rgb(color, space='rgb', cycle=None): +def to_rgb(color, space="rgb", cycle=None): """ Translate the color from an arbitrary colorspace to an RGB tuple. This is a generalization of `matplotlib.colors.to_rgb` and the inverse of `to_xyz`. @@ -600,7 +610,7 @@ def to_rgb(color, space='rgb', cycle=None): @docstring._snippet_manager -def to_rgba(color, space='rgb', cycle=None, clip=True): +def to_rgba(color, space="rgb", cycle=None, clip=True): """ Translate the color from an arbitrary colorspace to an RGBA tuple. This is a generalization of `matplotlib.colors.to_rgba` and the inverse of `to_xyz`. @@ -622,42 +632,39 @@ def to_rgba(color, space='rgb', cycle=None, clip=True): to_xyza """ # Translate color cycle strings - if isinstance(color, str) and re.match(r'\AC[0-9]\Z', color): + if isinstance(color, str) and re.match(r"\AC[0-9]\Z", color): color = _translate_cycle_color(color, cycle=cycle) # Translate RGB strings and (colormap, index) tuples # NOTE: Cannot use is_color_like because might have HSL channel values opacity = 1 - if ( - isinstance(color, str) - or np.iterable(color) and len(color) == 2 - ): + if isinstance(color, str) or np.iterable(color) and len(color) == 2: color = mcolors.to_rgba(color) # also enforced validity if ( not np.iterable(color) or len(color) not in (3, 4) or not all(isinstance(c, Real) for c in color) ): - raise ValueError(f'Invalid color-spec {color!r}.') + raise ValueError(f"Invalid color-spec {color!r}.") if len(color) == 4: *color, opacity = color # Translate arbitrary colorspaces - if space == 'rgb': + if space == "rgb": if any(c > 2 for c in color): color = tuple(c / 255 for c in color) # scale to within 0-1 else: pass - elif space == 'hsv': + elif space == "hsv": color = hsluv.hsl_to_rgb(*color) - elif space == 'hcl': + elif space == "hcl": color = hsluv.hcl_to_rgb(*color) - elif space == 'hsl': + elif space == "hsl": color = hsluv.hsluv_to_rgb(*color) - elif space == 'hpl': + elif space == "hpl": color = hsluv.hpluv_to_rgb(*color) else: - raise ValueError(f'Invalid colorspace {space!r}.') + raise ValueError(f"Invalid colorspace {space!r}.") # Clip values. This should only be disabled when testing # translation functions. @@ -669,7 +676,7 @@ def to_rgba(color, space='rgb', cycle=None, clip=True): @docstring._snippet_manager -def to_xyz(color, space='hcl'): +def to_xyz(color, space="hcl"): """ Translate color in *any* format to a tuple of channel values in *any* colorspace. This is the inverse of `to_rgb`. @@ -696,7 +703,7 @@ def to_xyz(color, space='hcl'): @docstring._snippet_manager -def to_xyza(color, space='hcl'): +def to_xyza(color, space="hcl"): """ Translate color in *any* format to a tuple of channel values in *any* colorspace. This is the inverse of `to_rgba`. @@ -723,18 +730,18 @@ def to_xyza(color, space='hcl'): # NOTE: Don't pass color tuple, because we may want to permit # out-of-bounds RGB values to invert conversion *color, opacity = to_rgba(color) - if space == 'rgb': + if space == "rgb": pass - elif space == 'hsv': + elif space == "hsv": color = hsluv.rgb_to_hsl(*color) # rgb_to_hsv would also work - elif space == 'hcl': + elif space == "hcl": color = hsluv.rgb_to_hcl(*color) - elif space == 'hsl': + elif space == "hsl": color = hsluv.rgb_to_hsluv(*color) - elif space == 'hpl': + elif space == "hpl": color = hsluv.rgb_to_hpluv(*color) else: - raise ValueError(f'Invalid colorspace {space}.') + raise ValueError(f"Invalid colorspace {space}.") return (*color, opacity) @@ -746,18 +753,18 @@ def _fontsize_to_pt(size): if not isinstance(size, str): return size if size in mfonts.font_scalings: - return rc_matplotlib['font.size'] * scalings[size] + return rc_matplotlib["font.size"] * scalings[size] try: - return units(size, 'pt') + return units(size, "pt") except ValueError: raise KeyError( - f'Invalid font size {size!r}. Can be points or one of the preset scalings: ' - + ', '.join(f'{key!r} ({value})' for key, value in scalings.items()) - + '.' + f"Invalid font size {size!r}. Can be points or one of the preset scalings: " + + ", ".join(f"{key!r} ({value})" for key, value in scalings.items()) + + "." ) -@warnings._rename_kwargs('0.6.0', units='dest') +@warnings._rename_kwargs("0.6.0", units="dest") def units( value, numeric=None, dest=None, *, fontsize=None, figure=None, axes=None, width=None ): @@ -822,51 +829,51 @@ def units( relative coordinates. """ # Scales for converting physical units to inches - fontsize_small = _not_none(fontsize, rc_matplotlib['font.size']) # always absolute + fontsize_small = _not_none(fontsize, rc_matplotlib["font.size"]) # always absolute fontsize_small = _fontsize_to_pt(fontsize_small) - fontsize_large = _not_none(fontsize, rc_matplotlib['axes.titlesize']) + fontsize_large = _not_none(fontsize, rc_matplotlib["axes.titlesize"]) fontsize_large = _fontsize_to_pt(fontsize_large) unit_dict = UNIT_DICT.copy() unit_dict.update( { - 'em': fontsize_small / 72.0, - 'en': 0.5 * fontsize_small / 72.0, - 'Em': fontsize_large / 72.0, - 'En': 0.5 * fontsize_large / 72.0, + "em": fontsize_small / 72.0, + "en": 0.5 * fontsize_small / 72.0, + "Em": fontsize_large / 72.0, + "En": 0.5 * fontsize_large / 72.0, } ) # Scales for converting display units to inches # WARNING: In ipython shell these take the value 'figure' - if not isinstance(rc_matplotlib['figure.dpi'], str): - unit_dict['px'] = 1 / rc_matplotlib['figure.dpi'] # once generated by backend - if not isinstance(rc_matplotlib['savefig.dpi'], str): - unit_dict['pp'] = 1 / rc_matplotlib['savefig.dpi'] # once 'printed' i.e. saved + if not isinstance(rc_matplotlib["figure.dpi"], str): + unit_dict["px"] = 1 / rc_matplotlib["figure.dpi"] # once generated by backend + if not isinstance(rc_matplotlib["savefig.dpi"], str): + unit_dict["pp"] = 1 / rc_matplotlib["savefig.dpi"] # once 'printed' i.e. saved # Scales relative to axes and figure objects - if axes is not None and hasattr(axes, '_get_size_inches'): # proplot axes - unit_dict['ax'] = axes._get_size_inches()[1 - int(width)] + if axes is not None and hasattr(axes, "_get_size_inches"): # proplot axes + unit_dict["ax"] = axes._get_size_inches()[1 - int(width)] if figure is None: - figure = getattr(axes, 'figure', None) - if figure is not None and hasattr(figure, 'get_size_inches'): - unit_dict['fig'] = figure.get_size_inches()[1 - int(width)] + figure = getattr(axes, "figure", None) + if figure is not None and hasattr(figure, "get_size_inches"): + unit_dict["fig"] = figure.get_size_inches()[1 - int(width)] # Scale for converting inches to arbitrary other unit if numeric is None and dest is None: - numeric = dest = 'in' + numeric = dest = "in" elif numeric is None: numeric = dest elif dest is None: dest = numeric - options = 'Valid units are ' + ', '.join(map(repr, unit_dict)) + '.' + options = "Valid units are " + ", ".join(map(repr, unit_dict)) + "." try: nscale = unit_dict[numeric] except KeyError: - raise ValueError(f'Invalid numeric units {numeric!r}. ' + options) + raise ValueError(f"Invalid numeric units {numeric!r}. " + options) try: dscale = unit_dict[dest] except KeyError: - raise ValueError(f'Invalid destination units {dest!r}. ' + options) + raise ValueError(f"Invalid destination units {dest!r}. " + options) # Convert units for each value in list result = [] @@ -884,22 +891,22 @@ def units( if regex: number, units = regex.groups() # second group is exponential else: - raise ValueError(f'Invalid unit size spec {val!r}.') + raise ValueError(f"Invalid unit size spec {val!r}.") else: - raise ValueError(f'Invalid unit size spec {val!r}.') + raise ValueError(f"Invalid unit size spec {val!r}.") # Convert with units if not units: result.append(float(number) * nscale / dscale) elif units in unit_dict: result.append(float(number) * unit_dict[units] / dscale) else: - raise ValueError(f'Invalid input units {units!r}. ' + options) + raise ValueError(f"Invalid input units {units!r}. " + options) return result[0] if singleton else result # Deprecations shade, saturate = warnings._rename_objs( - '0.6.0', + "0.6.0", shade=scale_luminance, saturate=scale_saturation, ) diff --git a/pyproject.toml b/pyproject.toml index acfffc06b..c301b7531 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,59 @@ [build-system] -requires = ["setuptools>=44", "wheel", "setuptools_scm[toml]>=3.4.3"] +requires = ["setuptools>=64", + "setuptools_scm[toml]>=8", + "wheel", + "numpy>=1.26.0", + "matplotlib>=3.9.1", + ] build-backend = "setuptools.build_meta" +[project] +name = "proplot" +authors = [ + {name = "Luke Davis", email = "lukelbd@gmail.com"}, +] +maintainers = [ + {name = "Luke Davis", email = "lukelbd@gmail.com"}, +] +description = "A succinct matplotlib wrapper for making beautiful, publication-quality graphics." +readme = "README.rst" +requires-python = ">=3.6.0" +license = {text = "MIT"} +classifiers = [ + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies= [ + 'importlib-metadata; python_version>"3.8"', + +] +dynamic = ["version"] + + +[project.urls] +"Documentation" = "https://proplot.readthedocs.io" +"Issue Tracker" = "https://github.com/proplot-dev/proplot/issues" +"Source Code" = "https://github.com/proplot-dev/proplot" + +[project.entry-points."setuptools.finalize_distribution_options"] +setuptools_scm = "setuptools_scm._integration.setuptools:infer_version" + + + +[tool.setuptools] +packages = ["proplot"] +include-package-data = true + [tool.setuptools_scm] version_scheme = "post-release" local_scheme = "dirty-tag" + diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a30e3693b..000000000 --- a/setup.cfg +++ /dev/null @@ -1,32 +0,0 @@ -[metadata] -name = proplot -author = Luke Davis -author_email = lukelbd@gmail.com -maintainer = Luke Davis -maintainer_email = lukelbd@gmail.com -license = MIT -description = A succinct matplotlib wrapper for making beautiful, publication-quality graphics. -url = https://proplot.readthedocs.io -long_description = file: README.rst -long_description_content_type = text/x-rst -classifiers = - License :: OSI Approved :: MIT License - Operating System :: OS Independent - Intended Audience :: Science/Research - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -project_urls = - Documentation = https://proplot.readthedocs.io - Issue Tracker = https://github.com/proplot-dev/proplot/issues - Source Code = https://github.com/proplot-dev/proplot - -[options] -packages = proplot -install_requires = matplotlib>=3.0.0,<3.6.0 -include_package_data = True -python_requires = >=3.6.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 6b40b52bf..000000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -from setuptools import setup - -if __name__ == '__main__': - setup()