1736 lines
57 KiB
Python
1736 lines
57 KiB
Python
|
import copy
|
||
|
from datetime import datetime
|
||
|
import io
|
||
|
import pickle
|
||
|
import platform
|
||
|
from threading import Timer
|
||
|
from types import SimpleNamespace
|
||
|
import warnings
|
||
|
|
||
|
import numpy as np
|
||
|
import pytest
|
||
|
from PIL import Image
|
||
|
|
||
|
import matplotlib as mpl
|
||
|
from matplotlib import gridspec
|
||
|
from matplotlib.testing.decorators import image_comparison, check_figures_equal
|
||
|
from matplotlib.axes import Axes
|
||
|
from matplotlib.backend_bases import KeyEvent, MouseEvent
|
||
|
from matplotlib.figure import Figure, FigureBase
|
||
|
from matplotlib.layout_engine import (ConstrainedLayoutEngine,
|
||
|
TightLayoutEngine,
|
||
|
PlaceHolderLayoutEngine)
|
||
|
from matplotlib.ticker import AutoMinorLocator, FixedFormatter, ScalarFormatter
|
||
|
import matplotlib.pyplot as plt
|
||
|
import matplotlib.dates as mdates
|
||
|
|
||
|
|
||
|
@image_comparison(['figure_align_labels'], extensions=['png', 'svg'],
|
||
|
tol=0 if platform.machine() == 'x86_64' else 0.01)
|
||
|
def test_align_labels():
|
||
|
fig = plt.figure(layout='tight')
|
||
|
gs = gridspec.GridSpec(3, 3)
|
||
|
|
||
|
ax = fig.add_subplot(gs[0, :2])
|
||
|
ax.plot(np.arange(0, 1e6, 1000))
|
||
|
ax.set_ylabel('Ylabel0 0')
|
||
|
ax = fig.add_subplot(gs[0, -1])
|
||
|
ax.plot(np.arange(0, 1e4, 100))
|
||
|
|
||
|
for i in range(3):
|
||
|
ax = fig.add_subplot(gs[1, i])
|
||
|
ax.set_ylabel('YLabel1 %d' % i)
|
||
|
ax.set_xlabel('XLabel1 %d' % i)
|
||
|
if i in [0, 2]:
|
||
|
ax.xaxis.set_label_position("top")
|
||
|
ax.xaxis.tick_top()
|
||
|
if i == 0:
|
||
|
for tick in ax.get_xticklabels():
|
||
|
tick.set_rotation(90)
|
||
|
if i == 2:
|
||
|
ax.yaxis.set_label_position("right")
|
||
|
ax.yaxis.tick_right()
|
||
|
|
||
|
for i in range(3):
|
||
|
ax = fig.add_subplot(gs[2, i])
|
||
|
ax.set_xlabel(f'XLabel2 {i}')
|
||
|
ax.set_ylabel(f'YLabel2 {i}')
|
||
|
|
||
|
if i == 2:
|
||
|
ax.plot(np.arange(0, 1e4, 10))
|
||
|
ax.yaxis.set_label_position("right")
|
||
|
ax.yaxis.tick_right()
|
||
|
for tick in ax.get_xticklabels():
|
||
|
tick.set_rotation(90)
|
||
|
|
||
|
fig.align_labels()
|
||
|
|
||
|
|
||
|
@image_comparison(['figure_align_titles_tight.png',
|
||
|
'figure_align_titles_constrained.png'],
|
||
|
tol=0 if platform.machine() == 'x86_64' else 0.022,
|
||
|
style='mpl20')
|
||
|
def test_align_titles():
|
||
|
for layout in ['tight', 'constrained']:
|
||
|
fig, axs = plt.subplots(1, 2, layout=layout, width_ratios=[2, 1])
|
||
|
|
||
|
ax = axs[0]
|
||
|
ax.plot(np.arange(0, 1e6, 1000))
|
||
|
ax.set_title('Title0 left', loc='left')
|
||
|
ax.set_title('Title0 center', loc='center')
|
||
|
ax.set_title('Title0 right', loc='right')
|
||
|
|
||
|
ax = axs[1]
|
||
|
ax.plot(np.arange(0, 1e4, 100))
|
||
|
ax.set_title('Title1')
|
||
|
ax.set_xlabel('Xlabel0')
|
||
|
ax.xaxis.set_label_position("top")
|
||
|
ax.xaxis.tick_top()
|
||
|
for tick in ax.get_xticklabels():
|
||
|
tick.set_rotation(90)
|
||
|
|
||
|
fig.align_titles()
|
||
|
|
||
|
|
||
|
def test_align_labels_stray_axes():
|
||
|
fig, axs = plt.subplots(2, 2)
|
||
|
for nn, ax in enumerate(axs.flat):
|
||
|
ax.set_xlabel('Boo')
|
||
|
ax.set_xlabel('Who')
|
||
|
ax.plot(np.arange(4)**nn, np.arange(4)**nn)
|
||
|
fig.align_ylabels()
|
||
|
fig.align_xlabels()
|
||
|
fig.draw_without_rendering()
|
||
|
xn = np.zeros(4)
|
||
|
yn = np.zeros(4)
|
||
|
for nn, ax in enumerate(axs.flat):
|
||
|
yn[nn] = ax.xaxis.label.get_position()[1]
|
||
|
xn[nn] = ax.yaxis.label.get_position()[0]
|
||
|
np.testing.assert_allclose(xn[:2], xn[2:])
|
||
|
np.testing.assert_allclose(yn[::2], yn[1::2])
|
||
|
|
||
|
fig, axs = plt.subplots(2, 2, constrained_layout=True)
|
||
|
for nn, ax in enumerate(axs.flat):
|
||
|
ax.set_xlabel('Boo')
|
||
|
ax.set_xlabel('Who')
|
||
|
pc = ax.pcolormesh(np.random.randn(10, 10))
|
||
|
fig.colorbar(pc, ax=ax)
|
||
|
fig.align_ylabels()
|
||
|
fig.align_xlabels()
|
||
|
fig.draw_without_rendering()
|
||
|
xn = np.zeros(4)
|
||
|
yn = np.zeros(4)
|
||
|
for nn, ax in enumerate(axs.flat):
|
||
|
yn[nn] = ax.xaxis.label.get_position()[1]
|
||
|
xn[nn] = ax.yaxis.label.get_position()[0]
|
||
|
np.testing.assert_allclose(xn[:2], xn[2:])
|
||
|
np.testing.assert_allclose(yn[::2], yn[1::2])
|
||
|
|
||
|
|
||
|
def test_figure_label():
|
||
|
# pyplot figure creation, selection, and closing with label/number/instance
|
||
|
plt.close('all')
|
||
|
fig_today = plt.figure('today')
|
||
|
plt.figure(3)
|
||
|
plt.figure('tomorrow')
|
||
|
plt.figure()
|
||
|
plt.figure(0)
|
||
|
plt.figure(1)
|
||
|
plt.figure(3)
|
||
|
assert plt.get_fignums() == [0, 1, 3, 4, 5]
|
||
|
assert plt.get_figlabels() == ['', 'today', '', 'tomorrow', '']
|
||
|
plt.close(10)
|
||
|
plt.close()
|
||
|
plt.close(5)
|
||
|
plt.close('tomorrow')
|
||
|
assert plt.get_fignums() == [0, 1]
|
||
|
assert plt.get_figlabels() == ['', 'today']
|
||
|
plt.figure(fig_today)
|
||
|
assert plt.gcf() == fig_today
|
||
|
with pytest.raises(ValueError):
|
||
|
plt.figure(Figure())
|
||
|
|
||
|
|
||
|
def test_fignum_exists():
|
||
|
# pyplot figure creation, selection and closing with fignum_exists
|
||
|
plt.figure('one')
|
||
|
plt.figure(2)
|
||
|
plt.figure('three')
|
||
|
plt.figure()
|
||
|
assert plt.fignum_exists('one')
|
||
|
assert plt.fignum_exists(2)
|
||
|
assert plt.fignum_exists('three')
|
||
|
assert plt.fignum_exists(4)
|
||
|
plt.close('one')
|
||
|
plt.close(4)
|
||
|
assert not plt.fignum_exists('one')
|
||
|
assert not plt.fignum_exists(4)
|
||
|
|
||
|
|
||
|
def test_clf_keyword():
|
||
|
# test if existing figure is cleared with figure() and subplots()
|
||
|
text1 = 'A fancy plot'
|
||
|
text2 = 'Really fancy!'
|
||
|
|
||
|
fig0 = plt.figure(num=1)
|
||
|
fig0.suptitle(text1)
|
||
|
assert [t.get_text() for t in fig0.texts] == [text1]
|
||
|
|
||
|
fig1 = plt.figure(num=1, clear=False)
|
||
|
fig1.text(0.5, 0.5, text2)
|
||
|
assert fig0 is fig1
|
||
|
assert [t.get_text() for t in fig1.texts] == [text1, text2]
|
||
|
|
||
|
fig2, ax2 = plt.subplots(2, 1, num=1, clear=True)
|
||
|
assert fig0 is fig2
|
||
|
assert [t.get_text() for t in fig2.texts] == []
|
||
|
|
||
|
|
||
|
@image_comparison(['figure_today'],
|
||
|
tol=0.015 if platform.machine() == 'arm64' else 0)
|
||
|
def test_figure():
|
||
|
# named figure support
|
||
|
fig = plt.figure('today')
|
||
|
ax = fig.add_subplot()
|
||
|
ax.set_title(fig.get_label())
|
||
|
ax.plot(np.arange(5))
|
||
|
# plot red line in a different figure.
|
||
|
plt.figure('tomorrow')
|
||
|
plt.plot([0, 1], [1, 0], 'r')
|
||
|
# Return to the original; make sure the red line is not there.
|
||
|
plt.figure('today')
|
||
|
plt.close('tomorrow')
|
||
|
|
||
|
|
||
|
@image_comparison(['figure_legend'])
|
||
|
def test_figure_legend():
|
||
|
fig, axs = plt.subplots(2)
|
||
|
axs[0].plot([0, 1], [1, 0], label='x', color='g')
|
||
|
axs[0].plot([0, 1], [0, 1], label='y', color='r')
|
||
|
axs[0].plot([0, 1], [0.5, 0.5], label='y', color='k')
|
||
|
|
||
|
axs[1].plot([0, 1], [1, 0], label='_y', color='r')
|
||
|
axs[1].plot([0, 1], [0, 1], label='z', color='b')
|
||
|
fig.legend()
|
||
|
|
||
|
|
||
|
def test_gca():
|
||
|
fig = plt.figure()
|
||
|
|
||
|
# test that gca() picks up Axes created via add_axes()
|
||
|
ax0 = fig.add_axes([0, 0, 1, 1])
|
||
|
assert fig.gca() is ax0
|
||
|
|
||
|
# test that gca() picks up Axes created via add_subplot()
|
||
|
ax1 = fig.add_subplot(111)
|
||
|
assert fig.gca() is ax1
|
||
|
|
||
|
# add_axes on an existing Axes should not change stored order, but will
|
||
|
# make it current.
|
||
|
fig.add_axes(ax0)
|
||
|
assert fig.axes == [ax0, ax1]
|
||
|
assert fig.gca() is ax0
|
||
|
|
||
|
# sca() should not change stored order of Axes, which is order added.
|
||
|
fig.sca(ax0)
|
||
|
assert fig.axes == [ax0, ax1]
|
||
|
|
||
|
# add_subplot on an existing Axes should not change stored order, but will
|
||
|
# make it current.
|
||
|
fig.add_subplot(ax1)
|
||
|
assert fig.axes == [ax0, ax1]
|
||
|
assert fig.gca() is ax1
|
||
|
|
||
|
|
||
|
def test_add_subplot_subclass():
|
||
|
fig = plt.figure()
|
||
|
fig.add_subplot(axes_class=Axes)
|
||
|
with pytest.raises(ValueError):
|
||
|
fig.add_subplot(axes_class=Axes, projection="3d")
|
||
|
with pytest.raises(ValueError):
|
||
|
fig.add_subplot(axes_class=Axes, polar=True)
|
||
|
with pytest.raises(ValueError):
|
||
|
fig.add_subplot(projection="3d", polar=True)
|
||
|
with pytest.raises(TypeError):
|
||
|
fig.add_subplot(projection=42)
|
||
|
|
||
|
|
||
|
def test_add_subplot_invalid():
|
||
|
fig = plt.figure()
|
||
|
with pytest.raises(ValueError,
|
||
|
match='Number of columns must be a positive integer'):
|
||
|
fig.add_subplot(2, 0, 1)
|
||
|
with pytest.raises(ValueError,
|
||
|
match='Number of rows must be a positive integer'):
|
||
|
fig.add_subplot(0, 2, 1)
|
||
|
with pytest.raises(ValueError, match='num must be an integer with '
|
||
|
'1 <= num <= 4'):
|
||
|
fig.add_subplot(2, 2, 0)
|
||
|
with pytest.raises(ValueError, match='num must be an integer with '
|
||
|
'1 <= num <= 4'):
|
||
|
fig.add_subplot(2, 2, 5)
|
||
|
with pytest.raises(ValueError, match='num must be an integer with '
|
||
|
'1 <= num <= 4'):
|
||
|
fig.add_subplot(2, 2, 0.5)
|
||
|
|
||
|
with pytest.raises(ValueError, match='must be a three-digit integer'):
|
||
|
fig.add_subplot(42)
|
||
|
with pytest.raises(ValueError, match='must be a three-digit integer'):
|
||
|
fig.add_subplot(1000)
|
||
|
|
||
|
with pytest.raises(TypeError, match='takes 1 or 3 positional arguments '
|
||
|
'but 2 were given'):
|
||
|
fig.add_subplot(2, 2)
|
||
|
with pytest.raises(TypeError, match='takes 1 or 3 positional arguments '
|
||
|
'but 4 were given'):
|
||
|
fig.add_subplot(1, 2, 3, 4)
|
||
|
with pytest.raises(ValueError,
|
||
|
match="Number of rows must be a positive integer, "
|
||
|
"not '2'"):
|
||
|
fig.add_subplot('2', 2, 1)
|
||
|
with pytest.raises(ValueError,
|
||
|
match='Number of columns must be a positive integer, '
|
||
|
'not 2.0'):
|
||
|
fig.add_subplot(2, 2.0, 1)
|
||
|
_, ax = plt.subplots()
|
||
|
with pytest.raises(ValueError,
|
||
|
match='The Axes must have been created in the '
|
||
|
'present figure'):
|
||
|
fig.add_subplot(ax)
|
||
|
|
||
|
|
||
|
@image_comparison(['figure_suptitle'])
|
||
|
def test_suptitle():
|
||
|
fig, _ = plt.subplots()
|
||
|
fig.suptitle('hello', color='r')
|
||
|
fig.suptitle('title', color='g', rotation=30)
|
||
|
|
||
|
|
||
|
def test_suptitle_fontproperties():
|
||
|
fig, ax = plt.subplots()
|
||
|
fps = mpl.font_manager.FontProperties(size='large', weight='bold')
|
||
|
txt = fig.suptitle('fontprops title', fontproperties=fps)
|
||
|
assert txt.get_fontsize() == fps.get_size_in_points()
|
||
|
assert txt.get_weight() == fps.get_weight()
|
||
|
|
||
|
|
||
|
def test_suptitle_subfigures():
|
||
|
fig = plt.figure(figsize=(4, 3))
|
||
|
sf1, sf2 = fig.subfigures(1, 2)
|
||
|
sf2.set_facecolor('white')
|
||
|
sf1.subplots()
|
||
|
sf2.subplots()
|
||
|
fig.suptitle("This is a visible suptitle.")
|
||
|
|
||
|
# verify the first subfigure facecolor is the default transparent
|
||
|
assert sf1.get_facecolor() == (0.0, 0.0, 0.0, 0.0)
|
||
|
# verify the second subfigure facecolor is white
|
||
|
assert sf2.get_facecolor() == (1.0, 1.0, 1.0, 1.0)
|
||
|
|
||
|
|
||
|
def test_get_suptitle_supxlabel_supylabel():
|
||
|
fig, ax = plt.subplots()
|
||
|
assert fig.get_suptitle() == ""
|
||
|
assert fig.get_supxlabel() == ""
|
||
|
assert fig.get_supylabel() == ""
|
||
|
fig.suptitle('suptitle')
|
||
|
assert fig.get_suptitle() == 'suptitle'
|
||
|
fig.supxlabel('supxlabel')
|
||
|
assert fig.get_supxlabel() == 'supxlabel'
|
||
|
fig.supylabel('supylabel')
|
||
|
assert fig.get_supylabel() == 'supylabel'
|
||
|
|
||
|
|
||
|
@image_comparison(['alpha_background'],
|
||
|
# only test png and svg. The PDF output appears correct,
|
||
|
# but Ghostscript does not preserve the background color.
|
||
|
extensions=['png', 'svg'],
|
||
|
savefig_kwarg={'facecolor': (0, 1, 0.4),
|
||
|
'edgecolor': 'none'})
|
||
|
def test_alpha():
|
||
|
# We want an image which has a background color and an alpha of 0.4.
|
||
|
fig = plt.figure(figsize=[2, 1])
|
||
|
fig.set_facecolor((0, 1, 0.4))
|
||
|
fig.patch.set_alpha(0.4)
|
||
|
fig.patches.append(mpl.patches.CirclePolygon(
|
||
|
[20, 20], radius=15, alpha=0.6, facecolor='red'))
|
||
|
|
||
|
|
||
|
def test_too_many_figures():
|
||
|
with pytest.warns(RuntimeWarning):
|
||
|
for i in range(mpl.rcParams['figure.max_open_warning'] + 1):
|
||
|
plt.figure()
|
||
|
|
||
|
|
||
|
def test_iterability_axes_argument():
|
||
|
|
||
|
# This is a regression test for matplotlib/matplotlib#3196. If one of the
|
||
|
# arguments returned by _as_mpl_axes defines __getitem__ but is not
|
||
|
# iterable, this would raise an exception. This is because we check
|
||
|
# whether the arguments are iterable, and if so we try and convert them
|
||
|
# to a tuple. However, the ``iterable`` function returns True if
|
||
|
# __getitem__ is present, but some classes can define __getitem__ without
|
||
|
# being iterable. The tuple conversion is now done in a try...except in
|
||
|
# case it fails.
|
||
|
|
||
|
class MyAxes(Axes):
|
||
|
def __init__(self, *args, myclass=None, **kwargs):
|
||
|
Axes.__init__(self, *args, **kwargs)
|
||
|
|
||
|
class MyClass:
|
||
|
|
||
|
def __getitem__(self, item):
|
||
|
if item != 'a':
|
||
|
raise ValueError("item should be a")
|
||
|
|
||
|
def _as_mpl_axes(self):
|
||
|
return MyAxes, {'myclass': self}
|
||
|
|
||
|
fig = plt.figure()
|
||
|
fig.add_subplot(1, 1, 1, projection=MyClass())
|
||
|
plt.close(fig)
|
||
|
|
||
|
|
||
|
def test_set_fig_size():
|
||
|
fig = plt.figure()
|
||
|
|
||
|
# check figwidth
|
||
|
fig.set_figwidth(5)
|
||
|
assert fig.get_figwidth() == 5
|
||
|
|
||
|
# check figheight
|
||
|
fig.set_figheight(1)
|
||
|
assert fig.get_figheight() == 1
|
||
|
|
||
|
# check using set_size_inches
|
||
|
fig.set_size_inches(2, 4)
|
||
|
assert fig.get_figwidth() == 2
|
||
|
assert fig.get_figheight() == 4
|
||
|
|
||
|
# check using tuple to first argument
|
||
|
fig.set_size_inches((1, 3))
|
||
|
assert fig.get_figwidth() == 1
|
||
|
assert fig.get_figheight() == 3
|
||
|
|
||
|
|
||
|
def test_axes_remove():
|
||
|
fig, axs = plt.subplots(2, 2)
|
||
|
axs[-1, -1].remove()
|
||
|
for ax in axs.ravel()[:-1]:
|
||
|
assert ax in fig.axes
|
||
|
assert axs[-1, -1] not in fig.axes
|
||
|
assert len(fig.axes) == 3
|
||
|
|
||
|
|
||
|
def test_figaspect():
|
||
|
w, h = plt.figaspect(np.float64(2) / np.float64(1))
|
||
|
assert h / w == 2
|
||
|
w, h = plt.figaspect(2)
|
||
|
assert h / w == 2
|
||
|
w, h = plt.figaspect(np.zeros((1, 2)))
|
||
|
assert h / w == 0.5
|
||
|
w, h = plt.figaspect(np.zeros((2, 2)))
|
||
|
assert h / w == 1
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize('which', ['both', 'major', 'minor'])
|
||
|
def test_autofmt_xdate(which):
|
||
|
date = ['3 Jan 2013', '4 Jan 2013', '5 Jan 2013', '6 Jan 2013',
|
||
|
'7 Jan 2013', '8 Jan 2013', '9 Jan 2013', '10 Jan 2013',
|
||
|
'11 Jan 2013', '12 Jan 2013', '13 Jan 2013', '14 Jan 2013']
|
||
|
|
||
|
time = ['16:44:00', '16:45:00', '16:46:00', '16:47:00', '16:48:00',
|
||
|
'16:49:00', '16:51:00', '16:52:00', '16:53:00', '16:55:00',
|
||
|
'16:56:00', '16:57:00']
|
||
|
|
||
|
angle = 60
|
||
|
minors = [1, 2, 3, 4, 5, 6, 7]
|
||
|
|
||
|
x = mdates.datestr2num(date)
|
||
|
y = mdates.datestr2num(time)
|
||
|
|
||
|
fig, ax = plt.subplots()
|
||
|
|
||
|
ax.plot(x, y)
|
||
|
ax.yaxis_date()
|
||
|
ax.xaxis_date()
|
||
|
|
||
|
ax.xaxis.set_minor_locator(AutoMinorLocator(2))
|
||
|
with warnings.catch_warnings():
|
||
|
warnings.filterwarnings(
|
||
|
'ignore',
|
||
|
'FixedFormatter should only be used together with FixedLocator')
|
||
|
ax.xaxis.set_minor_formatter(FixedFormatter(minors))
|
||
|
|
||
|
fig.autofmt_xdate(0.2, angle, 'right', which)
|
||
|
|
||
|
if which in ('both', 'major'):
|
||
|
for label in fig.axes[0].get_xticklabels(False, 'major'):
|
||
|
assert int(label.get_rotation()) == angle
|
||
|
|
||
|
if which in ('both', 'minor'):
|
||
|
for label in fig.axes[0].get_xticklabels(True, 'minor'):
|
||
|
assert int(label.get_rotation()) == angle
|
||
|
|
||
|
|
||
|
@mpl.style.context('default')
|
||
|
def test_change_dpi():
|
||
|
fig = plt.figure(figsize=(4, 4))
|
||
|
fig.draw_without_rendering()
|
||
|
assert fig.canvas.renderer.height == 400
|
||
|
assert fig.canvas.renderer.width == 400
|
||
|
fig.dpi = 50
|
||
|
fig.draw_without_rendering()
|
||
|
assert fig.canvas.renderer.height == 200
|
||
|
assert fig.canvas.renderer.width == 200
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize('width, height', [
|
||
|
(1, np.nan),
|
||
|
(-1, 1),
|
||
|
(np.inf, 1)
|
||
|
])
|
||
|
def test_invalid_figure_size(width, height):
|
||
|
with pytest.raises(ValueError):
|
||
|
plt.figure(figsize=(width, height))
|
||
|
|
||
|
fig = plt.figure()
|
||
|
with pytest.raises(ValueError):
|
||
|
fig.set_size_inches(width, height)
|
||
|
|
||
|
|
||
|
def test_invalid_figure_add_axes():
|
||
|
fig = plt.figure()
|
||
|
with pytest.raises(TypeError,
|
||
|
match="missing 1 required positional argument: 'rect'"):
|
||
|
fig.add_axes()
|
||
|
|
||
|
with pytest.raises(ValueError):
|
||
|
fig.add_axes((.1, .1, .5, np.nan))
|
||
|
|
||
|
with pytest.raises(TypeError, match="multiple values for argument 'rect'"):
|
||
|
fig.add_axes([0, 0, 1, 1], rect=[0, 0, 1, 1])
|
||
|
|
||
|
fig2, ax = plt.subplots()
|
||
|
with pytest.raises(ValueError,
|
||
|
match="The Axes must have been created in the present "
|
||
|
"figure"):
|
||
|
fig.add_axes(ax)
|
||
|
|
||
|
fig2.delaxes(ax)
|
||
|
with pytest.warns(mpl.MatplotlibDeprecationWarning,
|
||
|
match="Passing more than one positional argument"):
|
||
|
fig2.add_axes(ax, "extra positional argument")
|
||
|
|
||
|
with pytest.warns(mpl.MatplotlibDeprecationWarning,
|
||
|
match="Passing more than one positional argument"):
|
||
|
fig.add_axes([0, 0, 1, 1], "extra positional argument")
|
||
|
|
||
|
|
||
|
def test_subplots_shareax_loglabels():
|
||
|
fig, axs = plt.subplots(2, 2, sharex=True, sharey=True, squeeze=False)
|
||
|
for ax in axs.flat:
|
||
|
ax.plot([10, 20, 30], [10, 20, 30])
|
||
|
|
||
|
ax.set_yscale("log")
|
||
|
ax.set_xscale("log")
|
||
|
|
||
|
for ax in axs[0, :]:
|
||
|
assert 0 == len(ax.xaxis.get_ticklabels(which='both'))
|
||
|
|
||
|
for ax in axs[1, :]:
|
||
|
assert 0 < len(ax.xaxis.get_ticklabels(which='both'))
|
||
|
|
||
|
for ax in axs[:, 1]:
|
||
|
assert 0 == len(ax.yaxis.get_ticklabels(which='both'))
|
||
|
|
||
|
for ax in axs[:, 0]:
|
||
|
assert 0 < len(ax.yaxis.get_ticklabels(which='both'))
|
||
|
|
||
|
|
||
|
def test_savefig():
|
||
|
fig = plt.figure()
|
||
|
msg = r"savefig\(\) takes 2 positional arguments but 3 were given"
|
||
|
with pytest.raises(TypeError, match=msg):
|
||
|
fig.savefig("fname1.png", "fname2.png")
|
||
|
|
||
|
|
||
|
def test_savefig_warns():
|
||
|
fig = plt.figure()
|
||
|
for format in ['png', 'pdf', 'svg', 'tif', 'jpg']:
|
||
|
with pytest.raises(TypeError):
|
||
|
fig.savefig(io.BytesIO(), format=format, non_existent_kwarg=True)
|
||
|
|
||
|
|
||
|
def test_savefig_backend():
|
||
|
fig = plt.figure()
|
||
|
# Intentionally use an invalid module name.
|
||
|
with pytest.raises(ModuleNotFoundError, match="No module named '@absent'"):
|
||
|
fig.savefig("test", backend="module://@absent")
|
||
|
with pytest.raises(ValueError,
|
||
|
match="The 'pdf' backend does not support png output"):
|
||
|
fig.savefig("test.png", backend="pdf")
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize('backend', [
|
||
|
pytest.param('Agg', marks=[pytest.mark.backend('Agg')]),
|
||
|
pytest.param('Cairo', marks=[pytest.mark.backend('Cairo')]),
|
||
|
])
|
||
|
def test_savefig_pixel_ratio(backend):
|
||
|
fig, ax = plt.subplots()
|
||
|
ax.plot([1, 2, 3])
|
||
|
with io.BytesIO() as buf:
|
||
|
fig.savefig(buf, format='png')
|
||
|
ratio1 = Image.open(buf)
|
||
|
ratio1.load()
|
||
|
|
||
|
fig, ax = plt.subplots()
|
||
|
ax.plot([1, 2, 3])
|
||
|
fig.canvas._set_device_pixel_ratio(2)
|
||
|
with io.BytesIO() as buf:
|
||
|
fig.savefig(buf, format='png')
|
||
|
ratio2 = Image.open(buf)
|
||
|
ratio2.load()
|
||
|
|
||
|
assert ratio1 == ratio2
|
||
|
|
||
|
|
||
|
def test_savefig_preserve_layout_engine():
|
||
|
fig = plt.figure(layout='compressed')
|
||
|
fig.savefig(io.BytesIO(), bbox_inches='tight')
|
||
|
|
||
|
assert fig.get_layout_engine()._compress
|
||
|
|
||
|
|
||
|
def test_savefig_locate_colorbar():
|
||
|
fig, ax = plt.subplots()
|
||
|
pc = ax.pcolormesh(np.random.randn(2, 2))
|
||
|
cbar = fig.colorbar(pc, aspect=40)
|
||
|
fig.savefig(io.BytesIO(), bbox_inches=mpl.transforms.Bbox([[0, 0], [4, 4]]))
|
||
|
|
||
|
# Check that an aspect ratio has been applied.
|
||
|
assert (cbar.ax.get_position(original=True).bounds !=
|
||
|
cbar.ax.get_position(original=False).bounds)
|
||
|
|
||
|
|
||
|
@mpl.rc_context({"savefig.transparent": True})
|
||
|
@check_figures_equal(extensions=["png"])
|
||
|
def test_savefig_transparent(fig_test, fig_ref):
|
||
|
# create two transparent subfigures with corresponding transparent inset
|
||
|
# axes. the entire background of the image should be transparent.
|
||
|
gs1 = fig_test.add_gridspec(3, 3, left=0.05, wspace=0.05)
|
||
|
f1 = fig_test.add_subfigure(gs1[:, :])
|
||
|
f2 = f1.add_subfigure(gs1[0, 0])
|
||
|
|
||
|
ax12 = f2.add_subplot(gs1[:, :])
|
||
|
|
||
|
ax1 = f1.add_subplot(gs1[:-1, :])
|
||
|
iax1 = ax1.inset_axes([.1, .2, .3, .4])
|
||
|
iax2 = iax1.inset_axes([.1, .2, .3, .4])
|
||
|
|
||
|
ax2 = fig_test.add_subplot(gs1[-1, :-1])
|
||
|
ax3 = fig_test.add_subplot(gs1[-1, -1])
|
||
|
|
||
|
for ax in [ax12, ax1, iax1, iax2, ax2, ax3]:
|
||
|
ax.set(xticks=[], yticks=[])
|
||
|
ax.spines[:].set_visible(False)
|
||
|
|
||
|
|
||
|
def test_figure_repr():
|
||
|
fig = plt.figure(figsize=(10, 20), dpi=10)
|
||
|
assert repr(fig) == "<Figure size 100x200 with 0 Axes>"
|
||
|
|
||
|
|
||
|
def test_valid_layouts():
|
||
|
fig = Figure(layout=None)
|
||
|
assert not fig.get_tight_layout()
|
||
|
assert not fig.get_constrained_layout()
|
||
|
|
||
|
fig = Figure(layout='tight')
|
||
|
assert fig.get_tight_layout()
|
||
|
assert not fig.get_constrained_layout()
|
||
|
|
||
|
fig = Figure(layout='constrained')
|
||
|
assert not fig.get_tight_layout()
|
||
|
assert fig.get_constrained_layout()
|
||
|
|
||
|
|
||
|
def test_invalid_layouts():
|
||
|
fig, ax = plt.subplots(layout="constrained")
|
||
|
with pytest.warns(UserWarning):
|
||
|
# this should warn,
|
||
|
fig.subplots_adjust(top=0.8)
|
||
|
assert isinstance(fig.get_layout_engine(), ConstrainedLayoutEngine)
|
||
|
|
||
|
# Using layout + (tight|constrained)_layout warns, but the former takes
|
||
|
# precedence.
|
||
|
wst = "The Figure parameters 'layout' and 'tight_layout'"
|
||
|
with pytest.warns(UserWarning, match=wst):
|
||
|
fig = Figure(layout='tight', tight_layout=False)
|
||
|
assert isinstance(fig.get_layout_engine(), TightLayoutEngine)
|
||
|
wst = "The Figure parameters 'layout' and 'constrained_layout'"
|
||
|
with pytest.warns(UserWarning, match=wst):
|
||
|
fig = Figure(layout='constrained', constrained_layout=False)
|
||
|
assert not isinstance(fig.get_layout_engine(), TightLayoutEngine)
|
||
|
assert isinstance(fig.get_layout_engine(), ConstrainedLayoutEngine)
|
||
|
|
||
|
with pytest.raises(ValueError,
|
||
|
match="Invalid value for 'layout'"):
|
||
|
Figure(layout='foobar')
|
||
|
|
||
|
# test that layouts can be swapped if no colorbar:
|
||
|
fig, ax = plt.subplots(layout="constrained")
|
||
|
fig.set_layout_engine("tight")
|
||
|
assert isinstance(fig.get_layout_engine(), TightLayoutEngine)
|
||
|
fig.set_layout_engine("constrained")
|
||
|
assert isinstance(fig.get_layout_engine(), ConstrainedLayoutEngine)
|
||
|
|
||
|
# test that layouts cannot be swapped if there is a colorbar:
|
||
|
fig, ax = plt.subplots(layout="constrained")
|
||
|
pc = ax.pcolormesh(np.random.randn(2, 2))
|
||
|
fig.colorbar(pc)
|
||
|
with pytest.raises(RuntimeError, match='Colorbar layout of new layout'):
|
||
|
fig.set_layout_engine("tight")
|
||
|
fig.set_layout_engine("none")
|
||
|
with pytest.raises(RuntimeError, match='Colorbar layout of new layout'):
|
||
|
fig.set_layout_engine("tight")
|
||
|
|
||
|
fig, ax = plt.subplots(layout="tight")
|
||
|
pc = ax.pcolormesh(np.random.randn(2, 2))
|
||
|
fig.colorbar(pc)
|
||
|
with pytest.raises(RuntimeError, match='Colorbar layout of new layout'):
|
||
|
fig.set_layout_engine("constrained")
|
||
|
fig.set_layout_engine("none")
|
||
|
assert isinstance(fig.get_layout_engine(), PlaceHolderLayoutEngine)
|
||
|
|
||
|
with pytest.raises(RuntimeError, match='Colorbar layout of new layout'):
|
||
|
fig.set_layout_engine("constrained")
|
||
|
|
||
|
|
||
|
@check_figures_equal(extensions=["png"])
|
||
|
def test_tightlayout_autolayout_deconflict(fig_test, fig_ref):
|
||
|
for fig, autolayout in zip([fig_ref, fig_test], [False, True]):
|
||
|
with mpl.rc_context({'figure.autolayout': autolayout}):
|
||
|
axes = fig.subplots(ncols=2)
|
||
|
fig.tight_layout(w_pad=10)
|
||
|
assert isinstance(fig.get_layout_engine(), PlaceHolderLayoutEngine)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize('layout', ['constrained', 'compressed'])
|
||
|
def test_layout_change_warning(layout):
|
||
|
"""
|
||
|
Raise a warning when a previously assigned layout changes to tight using
|
||
|
plt.tight_layout().
|
||
|
"""
|
||
|
fig, ax = plt.subplots(layout=layout)
|
||
|
with pytest.warns(UserWarning, match='The figure layout has changed to'):
|
||
|
plt.tight_layout()
|
||
|
|
||
|
|
||
|
def test_repeated_tightlayout():
|
||
|
fig = Figure()
|
||
|
fig.tight_layout()
|
||
|
# subsequent calls should not warn
|
||
|
fig.tight_layout()
|
||
|
fig.tight_layout()
|
||
|
|
||
|
|
||
|
@check_figures_equal(extensions=["png", "pdf"])
|
||
|
def test_add_artist(fig_test, fig_ref):
|
||
|
fig_test.dpi = 100
|
||
|
fig_ref.dpi = 100
|
||
|
|
||
|
fig_test.subplots()
|
||
|
l1 = plt.Line2D([.2, .7], [.7, .7], gid='l1')
|
||
|
l2 = plt.Line2D([.2, .7], [.8, .8], gid='l2')
|
||
|
r1 = plt.Circle((20, 20), 100, transform=None, gid='C1')
|
||
|
r2 = plt.Circle((.7, .5), .05, gid='C2')
|
||
|
r3 = plt.Circle((4.5, .8), .55, transform=fig_test.dpi_scale_trans,
|
||
|
facecolor='crimson', gid='C3')
|
||
|
for a in [l1, l2, r1, r2, r3]:
|
||
|
fig_test.add_artist(a)
|
||
|
l2.remove()
|
||
|
|
||
|
ax2 = fig_ref.subplots()
|
||
|
l1 = plt.Line2D([.2, .7], [.7, .7], transform=fig_ref.transFigure,
|
||
|
gid='l1', zorder=21)
|
||
|
r1 = plt.Circle((20, 20), 100, transform=None, clip_on=False, zorder=20,
|
||
|
gid='C1')
|
||
|
r2 = plt.Circle((.7, .5), .05, transform=fig_ref.transFigure, gid='C2',
|
||
|
zorder=20)
|
||
|
r3 = plt.Circle((4.5, .8), .55, transform=fig_ref.dpi_scale_trans,
|
||
|
facecolor='crimson', clip_on=False, zorder=20, gid='C3')
|
||
|
for a in [l1, r1, r2, r3]:
|
||
|
ax2.add_artist(a)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("fmt", ["png", "pdf", "ps", "eps", "svg"])
|
||
|
def test_fspath(fmt, tmp_path):
|
||
|
out = tmp_path / f"test.{fmt}"
|
||
|
plt.savefig(out)
|
||
|
with out.open("rb") as file:
|
||
|
# All the supported formats include the format name (case-insensitive)
|
||
|
# in the first 100 bytes.
|
||
|
assert fmt.encode("ascii") in file.read(100).lower()
|
||
|
|
||
|
|
||
|
def test_tightbbox():
|
||
|
fig, ax = plt.subplots()
|
||
|
ax.set_xlim(0, 1)
|
||
|
t = ax.text(1., 0.5, 'This dangles over end')
|
||
|
renderer = fig.canvas.get_renderer()
|
||
|
x1Nom0 = 9.035 # inches
|
||
|
assert abs(t.get_tightbbox(renderer).x1 - x1Nom0 * fig.dpi) < 2
|
||
|
assert abs(ax.get_tightbbox(renderer).x1 - x1Nom0 * fig.dpi) < 2
|
||
|
assert abs(fig.get_tightbbox(renderer).x1 - x1Nom0) < 0.05
|
||
|
assert abs(fig.get_tightbbox(renderer).x0 - 0.679) < 0.05
|
||
|
# now exclude t from the tight bbox so now the bbox is quite a bit
|
||
|
# smaller
|
||
|
t.set_in_layout(False)
|
||
|
x1Nom = 7.333
|
||
|
assert abs(ax.get_tightbbox(renderer).x1 - x1Nom * fig.dpi) < 2
|
||
|
assert abs(fig.get_tightbbox(renderer).x1 - x1Nom) < 0.05
|
||
|
|
||
|
t.set_in_layout(True)
|
||
|
x1Nom = 7.333
|
||
|
assert abs(ax.get_tightbbox(renderer).x1 - x1Nom0 * fig.dpi) < 2
|
||
|
# test bbox_extra_artists method...
|
||
|
assert abs(ax.get_tightbbox(renderer, bbox_extra_artists=[]).x1
|
||
|
- x1Nom * fig.dpi) < 2
|
||
|
|
||
|
|
||
|
def test_axes_removal():
|
||
|
# Check that units can set the formatter after an Axes removal
|
||
|
fig, axs = plt.subplots(1, 2, sharex=True)
|
||
|
axs[1].remove()
|
||
|
axs[0].plot([datetime(2000, 1, 1), datetime(2000, 2, 1)], [0, 1])
|
||
|
assert isinstance(axs[0].xaxis.get_major_formatter(),
|
||
|
mdates.AutoDateFormatter)
|
||
|
|
||
|
# Check that manually setting the formatter, then removing Axes keeps
|
||
|
# the set formatter.
|
||
|
fig, axs = plt.subplots(1, 2, sharex=True)
|
||
|
axs[1].xaxis.set_major_formatter(ScalarFormatter())
|
||
|
axs[1].remove()
|
||
|
axs[0].plot([datetime(2000, 1, 1), datetime(2000, 2, 1)], [0, 1])
|
||
|
assert isinstance(axs[0].xaxis.get_major_formatter(),
|
||
|
ScalarFormatter)
|
||
|
|
||
|
|
||
|
def test_removed_axis():
|
||
|
# Simple smoke test to make sure removing a shared axis works
|
||
|
fig, axs = plt.subplots(2, sharex=True)
|
||
|
axs[0].remove()
|
||
|
fig.canvas.draw()
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize('clear_meth', ['clear', 'clf'])
|
||
|
def test_figure_clear(clear_meth):
|
||
|
# we test the following figure clearing scenarios:
|
||
|
fig = plt.figure()
|
||
|
|
||
|
# a) an empty figure
|
||
|
fig.clear()
|
||
|
assert fig.axes == []
|
||
|
|
||
|
# b) a figure with a single unnested axes
|
||
|
ax = fig.add_subplot(111)
|
||
|
getattr(fig, clear_meth)()
|
||
|
assert fig.axes == []
|
||
|
|
||
|
# c) a figure multiple unnested axes
|
||
|
axes = [fig.add_subplot(2, 1, i+1) for i in range(2)]
|
||
|
getattr(fig, clear_meth)()
|
||
|
assert fig.axes == []
|
||
|
|
||
|
# d) a figure with a subfigure
|
||
|
gs = fig.add_gridspec(ncols=2, nrows=1)
|
||
|
subfig = fig.add_subfigure(gs[0])
|
||
|
subaxes = subfig.add_subplot(111)
|
||
|
getattr(fig, clear_meth)()
|
||
|
assert subfig not in fig.subfigs
|
||
|
assert fig.axes == []
|
||
|
|
||
|
# e) a figure with a subfigure and a subplot
|
||
|
subfig = fig.add_subfigure(gs[0])
|
||
|
subaxes = subfig.add_subplot(111)
|
||
|
mainaxes = fig.add_subplot(gs[1])
|
||
|
|
||
|
# e.1) removing just the axes leaves the subplot
|
||
|
mainaxes.remove()
|
||
|
assert fig.axes == [subaxes]
|
||
|
|
||
|
# e.2) removing just the subaxes leaves the subplot
|
||
|
# and subfigure
|
||
|
mainaxes = fig.add_subplot(gs[1])
|
||
|
subaxes.remove()
|
||
|
assert fig.axes == [mainaxes]
|
||
|
assert subfig in fig.subfigs
|
||
|
|
||
|
# e.3) clearing the subfigure leaves the subplot
|
||
|
subaxes = subfig.add_subplot(111)
|
||
|
assert mainaxes in fig.axes
|
||
|
assert subaxes in fig.axes
|
||
|
getattr(subfig, clear_meth)()
|
||
|
assert subfig in fig.subfigs
|
||
|
assert subaxes not in subfig.axes
|
||
|
assert subaxes not in fig.axes
|
||
|
assert mainaxes in fig.axes
|
||
|
|
||
|
# e.4) clearing the whole thing
|
||
|
subaxes = subfig.add_subplot(111)
|
||
|
getattr(fig, clear_meth)()
|
||
|
assert fig.axes == []
|
||
|
assert fig.subfigs == []
|
||
|
|
||
|
# f) multiple subfigures
|
||
|
subfigs = [fig.add_subfigure(gs[i]) for i in [0, 1]]
|
||
|
subaxes = [sfig.add_subplot(111) for sfig in subfigs]
|
||
|
assert all(ax in fig.axes for ax in subaxes)
|
||
|
assert all(sfig in fig.subfigs for sfig in subfigs)
|
||
|
|
||
|
# f.1) clearing only one subfigure
|
||
|
getattr(subfigs[0], clear_meth)()
|
||
|
assert subaxes[0] not in fig.axes
|
||
|
assert subaxes[1] in fig.axes
|
||
|
assert subfigs[1] in fig.subfigs
|
||
|
|
||
|
# f.2) clearing the whole thing
|
||
|
getattr(subfigs[1], clear_meth)()
|
||
|
subfigs = [fig.add_subfigure(gs[i]) for i in [0, 1]]
|
||
|
subaxes = [sfig.add_subplot(111) for sfig in subfigs]
|
||
|
assert all(ax in fig.axes for ax in subaxes)
|
||
|
assert all(sfig in fig.subfigs for sfig in subfigs)
|
||
|
getattr(fig, clear_meth)()
|
||
|
assert fig.subfigs == []
|
||
|
assert fig.axes == []
|
||
|
|
||
|
|
||
|
def test_clf_not_redefined():
|
||
|
for klass in FigureBase.__subclasses__():
|
||
|
# check that subclasses do not get redefined in our Figure subclasses
|
||
|
assert 'clf' not in klass.__dict__
|
||
|
|
||
|
|
||
|
@mpl.style.context('mpl20')
|
||
|
def test_picking_does_not_stale():
|
||
|
fig, ax = plt.subplots()
|
||
|
ax.scatter([0], [0], [1000], picker=True)
|
||
|
fig.canvas.draw()
|
||
|
assert not fig.stale
|
||
|
|
||
|
mouse_event = SimpleNamespace(x=ax.bbox.x0 + ax.bbox.width / 2,
|
||
|
y=ax.bbox.y0 + ax.bbox.height / 2,
|
||
|
inaxes=ax, guiEvent=None)
|
||
|
fig.pick(mouse_event)
|
||
|
assert not fig.stale
|
||
|
|
||
|
|
||
|
def test_add_subplot_twotuple():
|
||
|
fig = plt.figure()
|
||
|
ax1 = fig.add_subplot(3, 2, (3, 5))
|
||
|
assert ax1.get_subplotspec().rowspan == range(1, 3)
|
||
|
assert ax1.get_subplotspec().colspan == range(0, 1)
|
||
|
ax2 = fig.add_subplot(3, 2, (4, 6))
|
||
|
assert ax2.get_subplotspec().rowspan == range(1, 3)
|
||
|
assert ax2.get_subplotspec().colspan == range(1, 2)
|
||
|
ax3 = fig.add_subplot(3, 2, (3, 6))
|
||
|
assert ax3.get_subplotspec().rowspan == range(1, 3)
|
||
|
assert ax3.get_subplotspec().colspan == range(0, 2)
|
||
|
ax4 = fig.add_subplot(3, 2, (4, 5))
|
||
|
assert ax4.get_subplotspec().rowspan == range(1, 3)
|
||
|
assert ax4.get_subplotspec().colspan == range(0, 2)
|
||
|
with pytest.raises(IndexError):
|
||
|
fig.add_subplot(3, 2, (6, 3))
|
||
|
|
||
|
|
||
|
@image_comparison(['tightbbox_box_aspect.svg'], style='mpl20',
|
||
|
savefig_kwarg={'bbox_inches': 'tight',
|
||
|
'facecolor': 'teal'},
|
||
|
remove_text=True)
|
||
|
def test_tightbbox_box_aspect():
|
||
|
fig = plt.figure()
|
||
|
gs = fig.add_gridspec(1, 2)
|
||
|
ax1 = fig.add_subplot(gs[0, 0])
|
||
|
ax2 = fig.add_subplot(gs[0, 1], projection='3d')
|
||
|
ax1.set_box_aspect(.5)
|
||
|
ax2.set_box_aspect((2, 1, 1))
|
||
|
|
||
|
|
||
|
@check_figures_equal(extensions=["svg", "pdf", "eps", "png"])
|
||
|
def test_animated_with_canvas_change(fig_test, fig_ref):
|
||
|
ax_ref = fig_ref.subplots()
|
||
|
ax_ref.plot(range(5))
|
||
|
|
||
|
ax_test = fig_test.subplots()
|
||
|
ax_test.plot(range(5), animated=True)
|
||
|
|
||
|
|
||
|
class TestSubplotMosaic:
|
||
|
@check_figures_equal(extensions=["png"])
|
||
|
@pytest.mark.parametrize(
|
||
|
"x", [
|
||
|
[["A", "A", "B"], ["C", "D", "B"]],
|
||
|
[[1, 1, 2], [3, 4, 2]],
|
||
|
(("A", "A", "B"), ("C", "D", "B")),
|
||
|
((1, 1, 2), (3, 4, 2))
|
||
|
]
|
||
|
)
|
||
|
def test_basic(self, fig_test, fig_ref, x):
|
||
|
grid_axes = fig_test.subplot_mosaic(x)
|
||
|
|
||
|
for k, ax in grid_axes.items():
|
||
|
ax.set_title(k)
|
||
|
|
||
|
labels = sorted(np.unique(x))
|
||
|
|
||
|
assert len(labels) == len(grid_axes)
|
||
|
|
||
|
gs = fig_ref.add_gridspec(2, 3)
|
||
|
axA = fig_ref.add_subplot(gs[:1, :2])
|
||
|
axA.set_title(labels[0])
|
||
|
|
||
|
axB = fig_ref.add_subplot(gs[:, 2])
|
||
|
axB.set_title(labels[1])
|
||
|
|
||
|
axC = fig_ref.add_subplot(gs[1, 0])
|
||
|
axC.set_title(labels[2])
|
||
|
|
||
|
axD = fig_ref.add_subplot(gs[1, 1])
|
||
|
axD.set_title(labels[3])
|
||
|
|
||
|
@check_figures_equal(extensions=["png"])
|
||
|
def test_all_nested(self, fig_test, fig_ref):
|
||
|
x = [["A", "B"], ["C", "D"]]
|
||
|
y = [["E", "F"], ["G", "H"]]
|
||
|
|
||
|
fig_ref.set_layout_engine("constrained")
|
||
|
fig_test.set_layout_engine("constrained")
|
||
|
|
||
|
grid_axes = fig_test.subplot_mosaic([[x, y]])
|
||
|
for ax in grid_axes.values():
|
||
|
ax.set_title(ax.get_label())
|
||
|
|
||
|
gs = fig_ref.add_gridspec(1, 2)
|
||
|
gs_left = gs[0, 0].subgridspec(2, 2)
|
||
|
for j, r in enumerate(x):
|
||
|
for k, label in enumerate(r):
|
||
|
fig_ref.add_subplot(gs_left[j, k]).set_title(label)
|
||
|
|
||
|
gs_right = gs[0, 1].subgridspec(2, 2)
|
||
|
for j, r in enumerate(y):
|
||
|
for k, label in enumerate(r):
|
||
|
fig_ref.add_subplot(gs_right[j, k]).set_title(label)
|
||
|
|
||
|
@check_figures_equal(extensions=["png"])
|
||
|
def test_nested(self, fig_test, fig_ref):
|
||
|
|
||
|
fig_ref.set_layout_engine("constrained")
|
||
|
fig_test.set_layout_engine("constrained")
|
||
|
|
||
|
x = [["A", "B"], ["C", "D"]]
|
||
|
|
||
|
y = [["F"], [x]]
|
||
|
|
||
|
grid_axes = fig_test.subplot_mosaic(y)
|
||
|
|
||
|
for k, ax in grid_axes.items():
|
||
|
ax.set_title(k)
|
||
|
|
||
|
gs = fig_ref.add_gridspec(2, 1)
|
||
|
|
||
|
gs_n = gs[1, 0].subgridspec(2, 2)
|
||
|
|
||
|
axA = fig_ref.add_subplot(gs_n[0, 0])
|
||
|
axA.set_title("A")
|
||
|
|
||
|
axB = fig_ref.add_subplot(gs_n[0, 1])
|
||
|
axB.set_title("B")
|
||
|
|
||
|
axC = fig_ref.add_subplot(gs_n[1, 0])
|
||
|
axC.set_title("C")
|
||
|
|
||
|
axD = fig_ref.add_subplot(gs_n[1, 1])
|
||
|
axD.set_title("D")
|
||
|
|
||
|
axF = fig_ref.add_subplot(gs[0, 0])
|
||
|
axF.set_title("F")
|
||
|
|
||
|
@check_figures_equal(extensions=["png"])
|
||
|
def test_nested_tuple(self, fig_test, fig_ref):
|
||
|
x = [["A", "B", "B"], ["C", "C", "D"]]
|
||
|
xt = (("A", "B", "B"), ("C", "C", "D"))
|
||
|
|
||
|
fig_ref.subplot_mosaic([["F"], [x]])
|
||
|
fig_test.subplot_mosaic([["F"], [xt]])
|
||
|
|
||
|
def test_nested_width_ratios(self):
|
||
|
x = [["A", [["B"],
|
||
|
["C"]]]]
|
||
|
width_ratios = [2, 1]
|
||
|
|
||
|
fig, axd = plt.subplot_mosaic(x, width_ratios=width_ratios)
|
||
|
|
||
|
assert axd["A"].get_gridspec().get_width_ratios() == width_ratios
|
||
|
assert axd["B"].get_gridspec().get_width_ratios() != width_ratios
|
||
|
|
||
|
def test_nested_height_ratios(self):
|
||
|
x = [["A", [["B"],
|
||
|
["C"]]], ["D", "D"]]
|
||
|
height_ratios = [1, 2]
|
||
|
|
||
|
fig, axd = plt.subplot_mosaic(x, height_ratios=height_ratios)
|
||
|
|
||
|
assert axd["D"].get_gridspec().get_height_ratios() == height_ratios
|
||
|
assert axd["B"].get_gridspec().get_height_ratios() != height_ratios
|
||
|
|
||
|
@check_figures_equal(extensions=["png"])
|
||
|
@pytest.mark.parametrize(
|
||
|
"x, empty_sentinel",
|
||
|
[
|
||
|
([["A", None], [None, "B"]], None),
|
||
|
([["A", "."], [".", "B"]], "SKIP"),
|
||
|
([["A", 0], [0, "B"]], 0),
|
||
|
([[1, None], [None, 2]], None),
|
||
|
([[1, "."], [".", 2]], "SKIP"),
|
||
|
([[1, 0], [0, 2]], 0),
|
||
|
],
|
||
|
)
|
||
|
def test_empty(self, fig_test, fig_ref, x, empty_sentinel):
|
||
|
if empty_sentinel != "SKIP":
|
||
|
kwargs = {"empty_sentinel": empty_sentinel}
|
||
|
else:
|
||
|
kwargs = {}
|
||
|
grid_axes = fig_test.subplot_mosaic(x, **kwargs)
|
||
|
|
||
|
for k, ax in grid_axes.items():
|
||
|
ax.set_title(k)
|
||
|
|
||
|
labels = sorted(
|
||
|
{name for row in x for name in row} - {empty_sentinel, "."}
|
||
|
)
|
||
|
|
||
|
assert len(labels) == len(grid_axes)
|
||
|
|
||
|
gs = fig_ref.add_gridspec(2, 2)
|
||
|
axA = fig_ref.add_subplot(gs[0, 0])
|
||
|
axA.set_title(labels[0])
|
||
|
|
||
|
axB = fig_ref.add_subplot(gs[1, 1])
|
||
|
axB.set_title(labels[1])
|
||
|
|
||
|
def test_fail_list_of_str(self):
|
||
|
with pytest.raises(ValueError, match='must be 2D'):
|
||
|
plt.subplot_mosaic(['foo', 'bar'])
|
||
|
with pytest.raises(ValueError, match='must be 2D'):
|
||
|
plt.subplot_mosaic(['foo'])
|
||
|
with pytest.raises(ValueError, match='must be 2D'):
|
||
|
plt.subplot_mosaic([['foo', ('bar',)]])
|
||
|
with pytest.raises(ValueError, match='must be 2D'):
|
||
|
plt.subplot_mosaic([['a', 'b'], [('a', 'b'), 'c']])
|
||
|
|
||
|
@check_figures_equal(extensions=["png"])
|
||
|
@pytest.mark.parametrize("subplot_kw", [{}, {"projection": "polar"}, None])
|
||
|
def test_subplot_kw(self, fig_test, fig_ref, subplot_kw):
|
||
|
x = [[1, 2]]
|
||
|
grid_axes = fig_test.subplot_mosaic(x, subplot_kw=subplot_kw)
|
||
|
subplot_kw = subplot_kw or {}
|
||
|
|
||
|
gs = fig_ref.add_gridspec(1, 2)
|
||
|
axA = fig_ref.add_subplot(gs[0, 0], **subplot_kw)
|
||
|
|
||
|
axB = fig_ref.add_subplot(gs[0, 1], **subplot_kw)
|
||
|
|
||
|
@check_figures_equal(extensions=["png"])
|
||
|
@pytest.mark.parametrize("multi_value", ['BC', tuple('BC')])
|
||
|
def test_per_subplot_kw(self, fig_test, fig_ref, multi_value):
|
||
|
x = 'AB;CD'
|
||
|
grid_axes = fig_test.subplot_mosaic(
|
||
|
x,
|
||
|
subplot_kw={'facecolor': 'red'},
|
||
|
per_subplot_kw={
|
||
|
'D': {'facecolor': 'blue'},
|
||
|
multi_value: {'facecolor': 'green'},
|
||
|
}
|
||
|
)
|
||
|
|
||
|
gs = fig_ref.add_gridspec(2, 2)
|
||
|
for color, spec in zip(['red', 'green', 'green', 'blue'], gs):
|
||
|
fig_ref.add_subplot(spec, facecolor=color)
|
||
|
|
||
|
def test_string_parser(self):
|
||
|
normalize = Figure._normalize_grid_string
|
||
|
|
||
|
assert normalize('ABC') == [['A', 'B', 'C']]
|
||
|
assert normalize('AB;CC') == [['A', 'B'], ['C', 'C']]
|
||
|
assert normalize('AB;CC;DE') == [['A', 'B'], ['C', 'C'], ['D', 'E']]
|
||
|
assert normalize("""
|
||
|
ABC
|
||
|
""") == [['A', 'B', 'C']]
|
||
|
assert normalize("""
|
||
|
AB
|
||
|
CC
|
||
|
""") == [['A', 'B'], ['C', 'C']]
|
||
|
assert normalize("""
|
||
|
AB
|
||
|
CC
|
||
|
DE
|
||
|
""") == [['A', 'B'], ['C', 'C'], ['D', 'E']]
|
||
|
|
||
|
def test_per_subplot_kw_expander(self):
|
||
|
normalize = Figure._norm_per_subplot_kw
|
||
|
assert normalize({"A": {}, "B": {}}) == {"A": {}, "B": {}}
|
||
|
assert normalize({("A", "B"): {}}) == {"A": {}, "B": {}}
|
||
|
with pytest.raises(
|
||
|
ValueError, match=f'The key {"B"!r} appears multiple times'
|
||
|
):
|
||
|
normalize({("A", "B"): {}, "B": {}})
|
||
|
with pytest.raises(
|
||
|
ValueError, match=f'The key {"B"!r} appears multiple times'
|
||
|
):
|
||
|
normalize({"B": {}, ("A", "B"): {}})
|
||
|
|
||
|
def test_extra_per_subplot_kw(self):
|
||
|
with pytest.raises(
|
||
|
ValueError, match=f'The keys {set("B")!r} are in'
|
||
|
):
|
||
|
Figure().subplot_mosaic("A", per_subplot_kw={"B": {}})
|
||
|
|
||
|
@check_figures_equal(extensions=["png"])
|
||
|
@pytest.mark.parametrize("str_pattern",
|
||
|
["AAA\nBBB", "\nAAA\nBBB\n", "ABC\nDEF"]
|
||
|
)
|
||
|
def test_single_str_input(self, fig_test, fig_ref, str_pattern):
|
||
|
grid_axes = fig_test.subplot_mosaic(str_pattern)
|
||
|
|
||
|
grid_axes = fig_ref.subplot_mosaic(
|
||
|
[list(ln) for ln in str_pattern.strip().split("\n")]
|
||
|
)
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"x,match",
|
||
|
[
|
||
|
(
|
||
|
[["A", "."], [".", "A"]],
|
||
|
(
|
||
|
"(?m)we found that the label .A. specifies a "
|
||
|
+ "non-rectangular or non-contiguous area."
|
||
|
),
|
||
|
),
|
||
|
(
|
||
|
[["A", "B"], [None, [["A", "B"], ["C", "D"]]]],
|
||
|
"There are duplicate keys .* between the outer layout",
|
||
|
),
|
||
|
("AAA\nc\nBBB", "All of the rows must be the same length"),
|
||
|
(
|
||
|
[["A", [["B", "C"], ["D"]]], ["E", "E"]],
|
||
|
"All of the rows must be the same length",
|
||
|
),
|
||
|
],
|
||
|
)
|
||
|
def test_fail(self, x, match):
|
||
|
fig = plt.figure()
|
||
|
with pytest.raises(ValueError, match=match):
|
||
|
fig.subplot_mosaic(x)
|
||
|
|
||
|
@check_figures_equal(extensions=["png"])
|
||
|
def test_hashable_keys(self, fig_test, fig_ref):
|
||
|
fig_test.subplot_mosaic([[object(), object()]])
|
||
|
fig_ref.subplot_mosaic([["A", "B"]])
|
||
|
|
||
|
@pytest.mark.parametrize('str_pattern',
|
||
|
['abc', 'cab', 'bca', 'cba', 'acb', 'bac'])
|
||
|
def test_user_order(self, str_pattern):
|
||
|
fig = plt.figure()
|
||
|
ax_dict = fig.subplot_mosaic(str_pattern)
|
||
|
assert list(str_pattern) == list(ax_dict)
|
||
|
assert list(fig.axes) == list(ax_dict.values())
|
||
|
|
||
|
def test_nested_user_order(self):
|
||
|
layout = [
|
||
|
["A", [["B", "C"],
|
||
|
["D", "E"]]],
|
||
|
["F", "G"],
|
||
|
[".", [["H", [["I"],
|
||
|
["."]]]]]
|
||
|
]
|
||
|
|
||
|
fig = plt.figure()
|
||
|
ax_dict = fig.subplot_mosaic(layout)
|
||
|
assert list(ax_dict) == list("ABCDEFGHI")
|
||
|
assert list(fig.axes) == list(ax_dict.values())
|
||
|
|
||
|
def test_share_all(self):
|
||
|
layout = [
|
||
|
["A", [["B", "C"],
|
||
|
["D", "E"]]],
|
||
|
["F", "G"],
|
||
|
[".", [["H", [["I"],
|
||
|
["."]]]]]
|
||
|
]
|
||
|
fig = plt.figure()
|
||
|
ax_dict = fig.subplot_mosaic(layout, sharex=True, sharey=True)
|
||
|
ax_dict["A"].set(xscale="log", yscale="logit")
|
||
|
assert all(ax.get_xscale() == "log" and ax.get_yscale() == "logit"
|
||
|
for ax in ax_dict.values())
|
||
|
|
||
|
|
||
|
def test_reused_gridspec():
|
||
|
"""Test that these all use the same gridspec"""
|
||
|
fig = plt.figure()
|
||
|
ax1 = fig.add_subplot(3, 2, (3, 5))
|
||
|
ax2 = fig.add_subplot(3, 2, 4)
|
||
|
ax3 = plt.subplot2grid((3, 2), (2, 1), colspan=2, fig=fig)
|
||
|
|
||
|
gs1 = ax1.get_subplotspec().get_gridspec()
|
||
|
gs2 = ax2.get_subplotspec().get_gridspec()
|
||
|
gs3 = ax3.get_subplotspec().get_gridspec()
|
||
|
|
||
|
assert gs1 == gs2
|
||
|
assert gs1 == gs3
|
||
|
|
||
|
|
||
|
@image_comparison(['test_subfigure.png'], style='mpl20',
|
||
|
savefig_kwarg={'facecolor': 'teal'})
|
||
|
def test_subfigure():
|
||
|
np.random.seed(19680801)
|
||
|
fig = plt.figure(layout='constrained')
|
||
|
sub = fig.subfigures(1, 2)
|
||
|
|
||
|
axs = sub[0].subplots(2, 2)
|
||
|
for ax in axs.flat:
|
||
|
pc = ax.pcolormesh(np.random.randn(30, 30), vmin=-2, vmax=2)
|
||
|
sub[0].colorbar(pc, ax=axs)
|
||
|
sub[0].suptitle('Left Side')
|
||
|
sub[0].set_facecolor('white')
|
||
|
|
||
|
axs = sub[1].subplots(1, 3)
|
||
|
for ax in axs.flat:
|
||
|
pc = ax.pcolormesh(np.random.randn(30, 30), vmin=-2, vmax=2)
|
||
|
sub[1].colorbar(pc, ax=axs, location='bottom')
|
||
|
sub[1].suptitle('Right Side')
|
||
|
sub[1].set_facecolor('white')
|
||
|
|
||
|
fig.suptitle('Figure suptitle', fontsize='xx-large')
|
||
|
|
||
|
# below tests for the draw zorder of subfigures.
|
||
|
leg = fig.legend(handles=[plt.Line2D([0], [0], label='Line{}'.format(i))
|
||
|
for i in range(5)], loc='center')
|
||
|
sub[0].set_zorder(leg.get_zorder() - 1)
|
||
|
sub[1].set_zorder(leg.get_zorder() + 1)
|
||
|
|
||
|
|
||
|
def test_subfigure_tightbbox():
|
||
|
# test that we can get the tightbbox with a subfigure...
|
||
|
fig = plt.figure(layout='constrained')
|
||
|
sub = fig.subfigures(1, 2)
|
||
|
|
||
|
np.testing.assert_allclose(
|
||
|
fig.get_tightbbox(fig.canvas.get_renderer()).width,
|
||
|
8.0)
|
||
|
|
||
|
|
||
|
def test_subfigure_dpi():
|
||
|
fig = plt.figure(dpi=100)
|
||
|
sub_fig = fig.subfigures()
|
||
|
assert sub_fig.get_dpi() == fig.get_dpi()
|
||
|
|
||
|
sub_fig.set_dpi(200)
|
||
|
assert sub_fig.get_dpi() == 200
|
||
|
assert fig.get_dpi() == 200
|
||
|
|
||
|
|
||
|
@image_comparison(['test_subfigure_ss.png'], style='mpl20',
|
||
|
savefig_kwarg={'facecolor': 'teal'}, tol=0.02)
|
||
|
def test_subfigure_ss():
|
||
|
# test assigning the subfigure via subplotspec
|
||
|
np.random.seed(19680801)
|
||
|
fig = plt.figure(layout='constrained')
|
||
|
gs = fig.add_gridspec(1, 2)
|
||
|
|
||
|
sub = fig.add_subfigure(gs[0], facecolor='pink')
|
||
|
|
||
|
axs = sub.subplots(2, 2)
|
||
|
for ax in axs.flat:
|
||
|
pc = ax.pcolormesh(np.random.randn(30, 30), vmin=-2, vmax=2)
|
||
|
sub.colorbar(pc, ax=axs)
|
||
|
sub.suptitle('Left Side')
|
||
|
|
||
|
ax = fig.add_subplot(gs[1])
|
||
|
ax.plot(np.arange(20))
|
||
|
ax.set_title('Axes')
|
||
|
|
||
|
fig.suptitle('Figure suptitle', fontsize='xx-large')
|
||
|
|
||
|
|
||
|
@image_comparison(['test_subfigure_double.png'], style='mpl20',
|
||
|
savefig_kwarg={'facecolor': 'teal'})
|
||
|
def test_subfigure_double():
|
||
|
# test assigning the subfigure via subplotspec
|
||
|
np.random.seed(19680801)
|
||
|
|
||
|
fig = plt.figure(layout='constrained', figsize=(10, 8))
|
||
|
|
||
|
fig.suptitle('fig')
|
||
|
|
||
|
subfigs = fig.subfigures(1, 2, wspace=0.07)
|
||
|
|
||
|
subfigs[0].set_facecolor('coral')
|
||
|
subfigs[0].suptitle('subfigs[0]')
|
||
|
|
||
|
subfigs[1].set_facecolor('coral')
|
||
|
subfigs[1].suptitle('subfigs[1]')
|
||
|
|
||
|
subfigsnest = subfigs[0].subfigures(2, 1, height_ratios=[1, 1.4])
|
||
|
subfigsnest[0].suptitle('subfigsnest[0]')
|
||
|
subfigsnest[0].set_facecolor('r')
|
||
|
axsnest0 = subfigsnest[0].subplots(1, 2, sharey=True)
|
||
|
for ax in axsnest0:
|
||
|
fontsize = 12
|
||
|
pc = ax.pcolormesh(np.random.randn(30, 30), vmin=-2.5, vmax=2.5)
|
||
|
ax.set_xlabel('x-label', fontsize=fontsize)
|
||
|
ax.set_ylabel('y-label', fontsize=fontsize)
|
||
|
ax.set_title('Title', fontsize=fontsize)
|
||
|
subfigsnest[0].colorbar(pc, ax=axsnest0)
|
||
|
|
||
|
subfigsnest[1].suptitle('subfigsnest[1]')
|
||
|
subfigsnest[1].set_facecolor('g')
|
||
|
axsnest1 = subfigsnest[1].subplots(3, 1, sharex=True)
|
||
|
for nn, ax in enumerate(axsnest1):
|
||
|
ax.set_ylabel(f'ylabel{nn}')
|
||
|
subfigsnest[1].supxlabel('supxlabel')
|
||
|
subfigsnest[1].supylabel('supylabel')
|
||
|
|
||
|
axsRight = subfigs[1].subplots(2, 2)
|
||
|
|
||
|
|
||
|
def test_subfigure_spanning():
|
||
|
# test that subfigures get laid out properly...
|
||
|
fig = plt.figure(constrained_layout=True)
|
||
|
gs = fig.add_gridspec(3, 3)
|
||
|
sub_figs = [
|
||
|
fig.add_subfigure(gs[0, 0]),
|
||
|
fig.add_subfigure(gs[0:2, 1]),
|
||
|
fig.add_subfigure(gs[2, 1:3]),
|
||
|
fig.add_subfigure(gs[0:, 1:])
|
||
|
]
|
||
|
|
||
|
w = 640
|
||
|
h = 480
|
||
|
np.testing.assert_allclose(sub_figs[0].bbox.min, [0., h * 2/3])
|
||
|
np.testing.assert_allclose(sub_figs[0].bbox.max, [w / 3, h])
|
||
|
|
||
|
np.testing.assert_allclose(sub_figs[1].bbox.min, [w / 3, h / 3])
|
||
|
np.testing.assert_allclose(sub_figs[1].bbox.max, [w * 2/3, h])
|
||
|
|
||
|
np.testing.assert_allclose(sub_figs[2].bbox.min, [w / 3, 0])
|
||
|
np.testing.assert_allclose(sub_figs[2].bbox.max, [w, h / 3])
|
||
|
|
||
|
# check here that slicing actually works. Last sub_fig
|
||
|
# with open slices failed, but only on draw...
|
||
|
for i in range(4):
|
||
|
sub_figs[i].add_subplot()
|
||
|
fig.draw_without_rendering()
|
||
|
|
||
|
|
||
|
@mpl.style.context('mpl20')
|
||
|
def test_subfigure_ticks():
|
||
|
# This tests a tick-spacing error that only seems applicable
|
||
|
# when the subfigures are saved to file. It is very hard to replicate
|
||
|
fig = plt.figure(constrained_layout=True, figsize=(10, 3))
|
||
|
# create left/right subfigs nested in bottom subfig
|
||
|
(subfig_bl, subfig_br) = fig.subfigures(1, 2, wspace=0.01,
|
||
|
width_ratios=[7, 2])
|
||
|
|
||
|
# put ax1-ax3 in gridspec of bottom-left subfig
|
||
|
gs = subfig_bl.add_gridspec(nrows=1, ncols=14)
|
||
|
|
||
|
ax1 = subfig_bl.add_subplot(gs[0, :1])
|
||
|
ax1.scatter(x=[-56.46881504821776, 24.179891162109396], y=[1500, 3600])
|
||
|
|
||
|
ax2 = subfig_bl.add_subplot(gs[0, 1:3], sharey=ax1)
|
||
|
ax2.scatter(x=[-126.5357270050049, 94.68456736755368], y=[1500, 3600])
|
||
|
ax3 = subfig_bl.add_subplot(gs[0, 3:14], sharey=ax1)
|
||
|
|
||
|
fig.dpi = 120
|
||
|
fig.draw_without_rendering()
|
||
|
ticks120 = ax2.get_xticks()
|
||
|
fig.dpi = 300
|
||
|
fig.draw_without_rendering()
|
||
|
ticks300 = ax2.get_xticks()
|
||
|
np.testing.assert_allclose(ticks120, ticks300)
|
||
|
|
||
|
|
||
|
@image_comparison(['test_subfigure_scatter_size.png'], style='mpl20',
|
||
|
remove_text=True)
|
||
|
def test_subfigure_scatter_size():
|
||
|
# markers in the left- and right-most subplots should be the same
|
||
|
fig = plt.figure()
|
||
|
gs = fig.add_gridspec(1, 2)
|
||
|
ax0 = fig.add_subplot(gs[1])
|
||
|
ax0.scatter([1, 2, 3], [1, 2, 3], s=30, marker='s')
|
||
|
ax0.scatter([3, 4, 5], [1, 2, 3], s=[20, 30, 40], marker='s')
|
||
|
|
||
|
sfig = fig.add_subfigure(gs[0])
|
||
|
axs = sfig.subplots(1, 2)
|
||
|
for ax in [ax0, axs[0]]:
|
||
|
ax.scatter([1, 2, 3], [1, 2, 3], s=30, marker='s', color='r')
|
||
|
ax.scatter([3, 4, 5], [1, 2, 3], s=[20, 30, 40], marker='s', color='g')
|
||
|
|
||
|
|
||
|
def test_subfigure_pdf():
|
||
|
fig = plt.figure(layout='constrained')
|
||
|
sub_fig = fig.subfigures()
|
||
|
ax = sub_fig.add_subplot(111)
|
||
|
b = ax.bar(1, 1)
|
||
|
ax.bar_label(b)
|
||
|
buffer = io.BytesIO()
|
||
|
fig.savefig(buffer, format='pdf')
|
||
|
|
||
|
|
||
|
def test_subfigures_wspace_hspace():
|
||
|
sub_figs = plt.figure().subfigures(2, 3, hspace=0.5, wspace=1/6.)
|
||
|
|
||
|
w = 640
|
||
|
h = 480
|
||
|
|
||
|
np.testing.assert_allclose(sub_figs[0, 0].bbox.min, [0., h * 0.6])
|
||
|
np.testing.assert_allclose(sub_figs[0, 0].bbox.max, [w * 0.3, h])
|
||
|
|
||
|
np.testing.assert_allclose(sub_figs[0, 1].bbox.min, [w * 0.35, h * 0.6])
|
||
|
np.testing.assert_allclose(sub_figs[0, 1].bbox.max, [w * 0.65, h])
|
||
|
|
||
|
np.testing.assert_allclose(sub_figs[0, 2].bbox.min, [w * 0.7, h * 0.6])
|
||
|
np.testing.assert_allclose(sub_figs[0, 2].bbox.max, [w, h])
|
||
|
|
||
|
np.testing.assert_allclose(sub_figs[1, 0].bbox.min, [0, 0])
|
||
|
np.testing.assert_allclose(sub_figs[1, 0].bbox.max, [w * 0.3, h * 0.4])
|
||
|
|
||
|
np.testing.assert_allclose(sub_figs[1, 1].bbox.min, [w * 0.35, 0])
|
||
|
np.testing.assert_allclose(sub_figs[1, 1].bbox.max, [w * 0.65, h * 0.4])
|
||
|
|
||
|
np.testing.assert_allclose(sub_figs[1, 2].bbox.min, [w * 0.7, 0])
|
||
|
np.testing.assert_allclose(sub_figs[1, 2].bbox.max, [w, h * 0.4])
|
||
|
|
||
|
|
||
|
def test_subfigure_remove():
|
||
|
fig = plt.figure()
|
||
|
sfs = fig.subfigures(2, 2)
|
||
|
sfs[1, 1].remove()
|
||
|
assert len(fig.subfigs) == 3
|
||
|
|
||
|
|
||
|
def test_add_subplot_kwargs():
|
||
|
# fig.add_subplot() always creates new axes, even if axes kwargs differ.
|
||
|
fig = plt.figure()
|
||
|
ax = fig.add_subplot(1, 1, 1)
|
||
|
ax1 = fig.add_subplot(1, 1, 1)
|
||
|
assert ax is not None
|
||
|
assert ax1 is not ax
|
||
|
plt.close()
|
||
|
|
||
|
fig = plt.figure()
|
||
|
ax = fig.add_subplot(1, 1, 1, projection='polar')
|
||
|
ax1 = fig.add_subplot(1, 1, 1, projection='polar')
|
||
|
assert ax is not None
|
||
|
assert ax1 is not ax
|
||
|
plt.close()
|
||
|
|
||
|
fig = plt.figure()
|
||
|
ax = fig.add_subplot(1, 1, 1, projection='polar')
|
||
|
ax1 = fig.add_subplot(1, 1, 1)
|
||
|
assert ax is not None
|
||
|
assert ax1.name == 'rectilinear'
|
||
|
assert ax1 is not ax
|
||
|
plt.close()
|
||
|
|
||
|
|
||
|
def test_add_axes_kwargs():
|
||
|
# fig.add_axes() always creates new axes, even if axes kwargs differ.
|
||
|
fig = plt.figure()
|
||
|
ax = fig.add_axes([0, 0, 1, 1])
|
||
|
ax1 = fig.add_axes([0, 0, 1, 1])
|
||
|
assert ax is not None
|
||
|
assert ax1 is not ax
|
||
|
plt.close()
|
||
|
|
||
|
fig = plt.figure()
|
||
|
ax = fig.add_axes([0, 0, 1, 1], projection='polar')
|
||
|
ax1 = fig.add_axes([0, 0, 1, 1], projection='polar')
|
||
|
assert ax is not None
|
||
|
assert ax1 is not ax
|
||
|
plt.close()
|
||
|
|
||
|
fig = plt.figure()
|
||
|
ax = fig.add_axes([0, 0, 1, 1], projection='polar')
|
||
|
ax1 = fig.add_axes([0, 0, 1, 1])
|
||
|
assert ax is not None
|
||
|
assert ax1.name == 'rectilinear'
|
||
|
assert ax1 is not ax
|
||
|
plt.close()
|
||
|
|
||
|
|
||
|
def test_ginput(recwarn): # recwarn undoes warn filters at exit.
|
||
|
warnings.filterwarnings("ignore", "cannot show the figure")
|
||
|
fig, ax = plt.subplots()
|
||
|
trans = ax.transData.transform
|
||
|
|
||
|
def single_press():
|
||
|
MouseEvent("button_press_event", fig.canvas, *trans((.1, .2)), 1)._process()
|
||
|
|
||
|
Timer(.1, single_press).start()
|
||
|
assert fig.ginput() == [(.1, .2)]
|
||
|
|
||
|
def multi_presses():
|
||
|
MouseEvent("button_press_event", fig.canvas, *trans((.1, .2)), 1)._process()
|
||
|
KeyEvent("key_press_event", fig.canvas, "backspace")._process()
|
||
|
MouseEvent("button_press_event", fig.canvas, *trans((.3, .4)), 1)._process()
|
||
|
MouseEvent("button_press_event", fig.canvas, *trans((.5, .6)), 1)._process()
|
||
|
MouseEvent("button_press_event", fig.canvas, *trans((0, 0)), 2)._process()
|
||
|
|
||
|
Timer(.1, multi_presses).start()
|
||
|
np.testing.assert_allclose(fig.ginput(3), [(.3, .4), (.5, .6)])
|
||
|
|
||
|
|
||
|
def test_waitforbuttonpress(recwarn): # recwarn undoes warn filters at exit.
|
||
|
warnings.filterwarnings("ignore", "cannot show the figure")
|
||
|
fig = plt.figure()
|
||
|
assert fig.waitforbuttonpress(timeout=.1) is None
|
||
|
Timer(.1, KeyEvent("key_press_event", fig.canvas, "z")._process).start()
|
||
|
assert fig.waitforbuttonpress() is True
|
||
|
Timer(.1, MouseEvent("button_press_event", fig.canvas, 0, 0, 1)._process).start()
|
||
|
assert fig.waitforbuttonpress() is False
|
||
|
|
||
|
|
||
|
def test_kwargs_pass():
|
||
|
fig = Figure(label='whole Figure')
|
||
|
sub_fig = fig.subfigures(1, 1, label='sub figure')
|
||
|
|
||
|
assert fig.get_label() == 'whole Figure'
|
||
|
assert sub_fig.get_label() == 'sub figure'
|
||
|
|
||
|
|
||
|
@check_figures_equal(extensions=["png"])
|
||
|
def test_rcparams(fig_test, fig_ref):
|
||
|
fig_ref.supxlabel("xlabel", weight='bold', size=15)
|
||
|
fig_ref.supylabel("ylabel", weight='bold', size=15)
|
||
|
fig_ref.suptitle("Title", weight='light', size=20)
|
||
|
with mpl.rc_context({'figure.labelweight': 'bold',
|
||
|
'figure.labelsize': 15,
|
||
|
'figure.titleweight': 'light',
|
||
|
'figure.titlesize': 20}):
|
||
|
fig_test.supxlabel("xlabel")
|
||
|
fig_test.supylabel("ylabel")
|
||
|
fig_test.suptitle("Title")
|
||
|
|
||
|
|
||
|
def test_deepcopy():
|
||
|
fig1, ax = plt.subplots()
|
||
|
ax.plot([0, 1], [2, 3])
|
||
|
ax.set_yscale('log')
|
||
|
|
||
|
fig2 = copy.deepcopy(fig1)
|
||
|
|
||
|
# Make sure it is a new object
|
||
|
assert fig2.axes[0] is not ax
|
||
|
# And that the axis scale got propagated
|
||
|
assert fig2.axes[0].get_yscale() == 'log'
|
||
|
# Update the deepcopy and check the original isn't modified
|
||
|
fig2.axes[0].set_yscale('linear')
|
||
|
assert ax.get_yscale() == 'log'
|
||
|
|
||
|
# And test the limits of the axes don't get propagated
|
||
|
ax.set_xlim(1e-1, 1e2)
|
||
|
# Draw these to make sure limits are updated
|
||
|
fig1.draw_without_rendering()
|
||
|
fig2.draw_without_rendering()
|
||
|
|
||
|
assert ax.get_xlim() == (1e-1, 1e2)
|
||
|
assert fig2.axes[0].get_xlim() == (0, 1)
|
||
|
|
||
|
|
||
|
def test_unpickle_with_device_pixel_ratio():
|
||
|
fig = Figure(dpi=42)
|
||
|
fig.canvas._set_device_pixel_ratio(7)
|
||
|
assert fig.dpi == 42*7
|
||
|
fig2 = pickle.loads(pickle.dumps(fig))
|
||
|
assert fig2.dpi == 42
|
||
|
|
||
|
|
||
|
def test_gridspec_no_mutate_input():
|
||
|
gs = {'left': .1}
|
||
|
gs_orig = dict(gs)
|
||
|
plt.subplots(1, 2, width_ratios=[1, 2], gridspec_kw=gs)
|
||
|
assert gs == gs_orig
|
||
|
plt.subplot_mosaic('AB', width_ratios=[1, 2], gridspec_kw=gs)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize('fmt', ['eps', 'pdf', 'png', 'ps', 'svg', 'svgz'])
|
||
|
def test_savefig_metadata(fmt):
|
||
|
Figure().savefig(io.BytesIO(), format=fmt, metadata={})
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize('fmt', ['jpeg', 'jpg', 'tif', 'tiff', 'webp', "raw", "rgba"])
|
||
|
def test_savefig_metadata_error(fmt):
|
||
|
with pytest.raises(ValueError, match="metadata not supported"):
|
||
|
Figure().savefig(io.BytesIO(), format=fmt, metadata={})
|
||
|
|
||
|
|
||
|
def test_get_constrained_layout_pads():
|
||
|
params = {'w_pad': 0.01, 'h_pad': 0.02, 'wspace': 0.03, 'hspace': 0.04}
|
||
|
expected = tuple([*params.values()])
|
||
|
fig = plt.figure(layout=mpl.layout_engine.ConstrainedLayoutEngine(**params))
|
||
|
with pytest.warns(PendingDeprecationWarning, match="will be deprecated"):
|
||
|
assert fig.get_constrained_layout_pads() == expected
|
||
|
|
||
|
|
||
|
def test_not_visible_figure():
|
||
|
fig = Figure()
|
||
|
|
||
|
buf = io.StringIO()
|
||
|
fig.savefig(buf, format='svg')
|
||
|
buf.seek(0)
|
||
|
assert '<g ' in buf.read()
|
||
|
|
||
|
fig.set_visible(False)
|
||
|
buf = io.StringIO()
|
||
|
fig.savefig(buf, format='svg')
|
||
|
buf.seek(0)
|
||
|
assert '<g ' not in buf.read()
|
||
|
|
||
|
|
||
|
def test_warn_colorbar_mismatch():
|
||
|
fig1, ax1 = plt.subplots()
|
||
|
fig2, (ax2_1, ax2_2) = plt.subplots(2)
|
||
|
im = ax1.imshow([[1, 2], [3, 4]])
|
||
|
|
||
|
fig1.colorbar(im) # should not warn
|
||
|
with pytest.warns(UserWarning, match="different Figure"):
|
||
|
fig2.colorbar(im)
|
||
|
# warn mismatch even when the host figure is not inferred
|
||
|
with pytest.warns(UserWarning, match="different Figure"):
|
||
|
fig2.colorbar(im, ax=ax1)
|
||
|
with pytest.warns(UserWarning, match="different Figure"):
|
||
|
fig2.colorbar(im, ax=ax2_1)
|
||
|
with pytest.warns(UserWarning, match="different Figure"):
|
||
|
fig2.colorbar(im, cax=ax2_2)
|
||
|
|
||
|
# edge case: only compare top level artist in case of subfigure
|
||
|
fig3 = plt.figure()
|
||
|
fig4 = plt.figure()
|
||
|
subfig3_1 = fig3.subfigures()
|
||
|
subfig3_2 = fig3.subfigures()
|
||
|
subfig4_1 = fig4.subfigures()
|
||
|
ax3_1 = subfig3_1.subplots()
|
||
|
ax3_2 = subfig3_1.subplots()
|
||
|
ax4_1 = subfig4_1.subplots()
|
||
|
im3_1 = ax3_1.imshow([[1, 2], [3, 4]])
|
||
|
im3_2 = ax3_2.imshow([[1, 2], [3, 4]])
|
||
|
im4_1 = ax4_1.imshow([[1, 2], [3, 4]])
|
||
|
|
||
|
fig3.colorbar(im3_1) # should not warn
|
||
|
subfig3_1.colorbar(im3_1) # should not warn
|
||
|
subfig3_1.colorbar(im3_2) # should not warn
|
||
|
with pytest.warns(UserWarning, match="different Figure"):
|
||
|
subfig3_1.colorbar(im4_1)
|