This commit is contained in:
cjw
2026-02-12 23:22:11 +08:00
parent 7b09eb3d89
commit 89660bba4e
5988 changed files with 2517516 additions and 0 deletions
@@ -0,0 +1,10 @@
from pathlib import Path
# Check that the test directories exist.
if not (Path(__file__).parent / 'baseline_images').exists():
raise OSError(
'The baseline image directory does not exist. '
'This is most likely because the test data is not installed. '
'You may need to install matplotlib from source to get the '
'test data.')
@@ -0,0 +1,2 @@
from matplotlib.testing.conftest import ( # noqa
mpl_test_settings, pytest_configure, pytest_unconfigure, pd, text_placeholders, xr)
@@ -0,0 +1,137 @@
from io import BytesIO
import pytest
import logging
from matplotlib import _afm
from matplotlib import font_manager as fm
# See note in afm.py re: use of comma as decimal separator in the
# UnderlineThickness field and re: use of non-ASCII characters in the Notice
# field.
AFM_TEST_DATA = b"""StartFontMetrics 2.0
Comment Comments are ignored.
Comment Creation Date:Mon Nov 13 12:34:11 GMT 2017
FontName MyFont-Bold
EncodingScheme FontSpecific
FullName My Font Bold
FamilyName Test Fonts
Weight Bold
ItalicAngle 0.0
IsFixedPitch false
UnderlinePosition -100
UnderlineThickness 56,789
Version 001.000
Notice Copyright \xa9 2017 No one.
FontBBox 0 -321 1234 369
StartCharMetrics 3
C 0 ; WX 250 ; N space ; B 0 0 0 0 ;
C 42 ; WX 1141 ; N foo ; B 40 60 800 360 ;
C 99 ; WX 583 ; N bar ; B 40 -10 543 210 ;
EndCharMetrics
EndFontMetrics
"""
def test_nonascii_str():
# This tests that we also decode bytes as utf-8 properly.
# Else, font files with non ascii characters fail to load.
inp_str = "привет"
byte_str = inp_str.encode("utf8")
ret = _afm._to_str(byte_str)
assert ret == inp_str
def test_parse_header():
fh = BytesIO(AFM_TEST_DATA)
header = _afm._parse_header(fh)
assert header == {
b'StartFontMetrics': 2.0,
b'FontName': 'MyFont-Bold',
b'EncodingScheme': 'FontSpecific',
b'FullName': 'My Font Bold',
b'FamilyName': 'Test Fonts',
b'Weight': 'Bold',
b'ItalicAngle': 0.0,
b'IsFixedPitch': False,
b'UnderlinePosition': -100,
b'UnderlineThickness': 56.789,
b'Version': '001.000',
b'Notice': b'Copyright \xa9 2017 No one.',
b'FontBBox': [0, -321, 1234, 369],
b'StartCharMetrics': 3,
}
def test_parse_char_metrics():
fh = BytesIO(AFM_TEST_DATA)
_afm._parse_header(fh) # position
metrics = _afm._parse_char_metrics(fh)
assert metrics == (
{0: (250.0, 'space', [0, 0, 0, 0]),
42: (1141.0, 'foo', [40, 60, 800, 360]),
99: (583.0, 'bar', [40, -10, 543, 210]),
},
{'space': (250.0, 'space', [0, 0, 0, 0]),
'foo': (1141.0, 'foo', [40, 60, 800, 360]),
'bar': (583.0, 'bar', [40, -10, 543, 210]),
})
def test_get_familyname_guessed():
fh = BytesIO(AFM_TEST_DATA)
font = _afm.AFM(fh)
del font._header[b'FamilyName'] # remove FamilyName, so we have to guess
assert font.get_familyname() == 'My Font'
def test_font_manager_weight_normalization():
font = _afm.AFM(BytesIO(
AFM_TEST_DATA.replace(b"Weight Bold\n", b"Weight Custom\n")))
assert fm.afmFontProperty("", font).weight == "normal"
@pytest.mark.parametrize(
"afm_data",
[
b"""nope
really nope""",
b"""StartFontMetrics 2.0
Comment Comments are ignored.
Comment Creation Date:Mon Nov 13 12:34:11 GMT 2017
FontName MyFont-Bold
EncodingScheme FontSpecific""",
],
)
def test_bad_afm(afm_data):
fh = BytesIO(afm_data)
with pytest.raises(RuntimeError):
_afm._parse_header(fh)
@pytest.mark.parametrize(
"afm_data",
[
b"""StartFontMetrics 2.0
Comment Comments are ignored.
Comment Creation Date:Mon Nov 13 12:34:11 GMT 2017
Aardvark bob
FontName MyFont-Bold
EncodingScheme FontSpecific
StartCharMetrics 3""",
b"""StartFontMetrics 2.0
Comment Comments are ignored.
Comment Creation Date:Mon Nov 13 12:34:11 GMT 2017
ItalicAngle zero degrees
FontName MyFont-Bold
EncodingScheme FontSpecific
StartCharMetrics 3""",
],
)
def test_malformed_header(afm_data, caplog):
fh = BytesIO(afm_data)
with caplog.at_level(logging.ERROR):
_afm._parse_header(fh)
assert len(caplog.records) == 1
@@ -0,0 +1,340 @@
import io
import numpy as np
from numpy.testing import assert_array_almost_equal
from PIL import features, Image, TiffTags
import pytest
from matplotlib import (
collections, patheffects, pyplot as plt, transforms as mtransforms,
rcParams, rc_context)
from matplotlib.backends.backend_agg import RendererAgg
from matplotlib.figure import Figure
from matplotlib.image import imread
from matplotlib.path import Path
from matplotlib.testing.decorators import image_comparison
from matplotlib.transforms import IdentityTransform
def test_repeated_save_with_alpha():
# We want an image which has a background color of bluish green, with an
# alpha of 0.25.
fig = Figure([1, 0.4])
fig.set_facecolor((0, 1, 0.4))
fig.patch.set_alpha(0.25)
# The target color is fig.patch.get_facecolor()
buf = io.BytesIO()
fig.savefig(buf,
facecolor=fig.get_facecolor(),
edgecolor='none')
# Save the figure again to check that the
# colors don't bleed from the previous renderer.
buf.seek(0)
fig.savefig(buf,
facecolor=fig.get_facecolor(),
edgecolor='none')
# Check the first pixel has the desired color & alpha
# (approx: 0, 1.0, 0.4, 0.25)
buf.seek(0)
assert_array_almost_equal(tuple(imread(buf)[0, 0]),
(0.0, 1.0, 0.4, 0.250),
decimal=3)
def test_large_single_path_collection():
buff = io.BytesIO()
# Generates a too-large single path in a path collection that
# would cause a segfault if the draw_markers optimization is
# applied.
f, ax = plt.subplots()
collection = collections.PathCollection(
[Path([[-10, 5], [10, 5], [10, -5], [-10, -5], [-10, 5]])])
ax.add_artist(collection)
ax.set_xlim(10**-3, 1)
plt.savefig(buff)
def test_marker_with_nan():
# This creates a marker with nans in it, which was segfaulting the
# Agg backend (see #3722)
fig, ax = plt.subplots(1)
steps = 1000
data = np.arange(steps)
ax.semilogx(data)
ax.fill_between(data, data*0.8, data*1.2)
buf = io.BytesIO()
fig.savefig(buf, format='png')
def test_long_path():
buff = io.BytesIO()
fig = Figure()
ax = fig.subplots()
points = np.ones(100_000)
points[::2] *= -1
ax.plot(points)
fig.savefig(buff, format='png')
@image_comparison(['agg_filter.png'], remove_text=True)
def test_agg_filter():
def smooth1d(x, window_len):
# copied from https://scipy-cookbook.readthedocs.io/
s = np.r_[
2*x[0] - x[window_len:1:-1], x, 2*x[-1] - x[-1:-window_len:-1]]
w = np.hanning(window_len)
y = np.convolve(w/w.sum(), s, mode='same')
return y[window_len-1:-window_len+1]
def smooth2d(A, sigma=3):
window_len = max(int(sigma), 3) * 2 + 1
A = np.apply_along_axis(smooth1d, 0, A, window_len)
A = np.apply_along_axis(smooth1d, 1, A, window_len)
return A
class BaseFilter:
def get_pad(self, dpi):
return 0
def process_image(self, padded_src, dpi):
raise NotImplementedError("Should be overridden by subclasses")
def __call__(self, im, dpi):
pad = self.get_pad(dpi)
padded_src = np.pad(im, [(pad, pad), (pad, pad), (0, 0)],
"constant")
tgt_image = self.process_image(padded_src, dpi)
return tgt_image, -pad, -pad
class OffsetFilter(BaseFilter):
def __init__(self, offsets=(0, 0)):
self.offsets = offsets
def get_pad(self, dpi):
return int(max(self.offsets) / 72 * dpi)
def process_image(self, padded_src, dpi):
ox, oy = self.offsets
a1 = np.roll(padded_src, int(ox / 72 * dpi), axis=1)
a2 = np.roll(a1, -int(oy / 72 * dpi), axis=0)
return a2
class GaussianFilter(BaseFilter):
"""Simple Gaussian filter."""
def __init__(self, sigma, alpha=0.5, color=(0, 0, 0)):
self.sigma = sigma
self.alpha = alpha
self.color = color
def get_pad(self, dpi):
return int(self.sigma*3 / 72 * dpi)
def process_image(self, padded_src, dpi):
tgt_image = np.empty_like(padded_src)
tgt_image[:, :, :3] = self.color
tgt_image[:, :, 3] = smooth2d(padded_src[:, :, 3] * self.alpha,
self.sigma / 72 * dpi)
return tgt_image
class DropShadowFilter(BaseFilter):
def __init__(self, sigma, alpha=0.3, color=(0, 0, 0), offsets=(0, 0)):
self.gauss_filter = GaussianFilter(sigma, alpha, color)
self.offset_filter = OffsetFilter(offsets)
def get_pad(self, dpi):
return max(self.gauss_filter.get_pad(dpi),
self.offset_filter.get_pad(dpi))
def process_image(self, padded_src, dpi):
t1 = self.gauss_filter.process_image(padded_src, dpi)
t2 = self.offset_filter.process_image(t1, dpi)
return t2
fig, ax = plt.subplots()
# draw lines
line1, = ax.plot([0.1, 0.5, 0.9], [0.1, 0.9, 0.5], "bo-",
mec="b", mfc="w", lw=5, mew=3, ms=10, label="Line 1")
line2, = ax.plot([0.1, 0.5, 0.9], [0.5, 0.2, 0.7], "ro-",
mec="r", mfc="w", lw=5, mew=3, ms=10, label="Line 1")
gauss = DropShadowFilter(4)
for line in [line1, line2]:
# draw shadows with same lines with slight offset.
xx = line.get_xdata()
yy = line.get_ydata()
shadow, = ax.plot(xx, yy)
shadow.update_from(line)
# offset transform
transform = mtransforms.offset_copy(
line.get_transform(), fig, x=4.0, y=-6.0, units='points')
shadow.set_transform(transform)
# adjust zorder of the shadow lines so that it is drawn below the
# original lines
shadow.set_zorder(line.get_zorder() - 0.5)
shadow.set_agg_filter(gauss)
shadow.set_rasterized(True) # to support mixed-mode renderers
ax.set_xlim(0., 1.)
ax.set_ylim(0., 1.)
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
def test_too_large_image():
fig = plt.figure(figsize=(300, 2**25))
buff = io.BytesIO()
with pytest.raises(ValueError):
fig.savefig(buff)
def test_chunksize():
x = range(200)
# Test without chunksize
fig, ax = plt.subplots()
ax.plot(x, np.sin(x))
fig.canvas.draw()
# Test with chunksize
fig, ax = plt.subplots()
rcParams['agg.path.chunksize'] = 105
ax.plot(x, np.sin(x))
fig.canvas.draw()
@pytest.mark.backend('Agg')
def test_jpeg_dpi():
# Check that dpi is set correctly in jpg files.
plt.plot([0, 1, 2], [0, 1, 0])
buf = io.BytesIO()
plt.savefig(buf, format="jpg", dpi=200)
im = Image.open(buf)
assert im.info['dpi'] == (200, 200)
def test_pil_kwargs_png():
from PIL.PngImagePlugin import PngInfo
buf = io.BytesIO()
pnginfo = PngInfo()
pnginfo.add_text("Software", "test")
plt.figure().savefig(buf, format="png", pil_kwargs={"pnginfo": pnginfo})
im = Image.open(buf)
assert im.info["Software"] == "test"
def test_pil_kwargs_tiff():
buf = io.BytesIO()
pil_kwargs = {"description": "test image"}
plt.figure().savefig(buf, format="tiff", pil_kwargs=pil_kwargs)
im = Image.open(buf)
tags = {TiffTags.TAGS_V2[k].name: v for k, v in im.tag_v2.items()}
assert tags["ImageDescription"] == "test image"
@pytest.mark.skipif(not features.check("webp"), reason="WebP support not available")
def test_pil_kwargs_webp():
plt.plot([0, 1, 2], [0, 1, 0])
buf_small = io.BytesIO()
pil_kwargs_low = {"quality": 1}
plt.savefig(buf_small, format="webp", pil_kwargs=pil_kwargs_low)
assert len(pil_kwargs_low) == 1
buf_large = io.BytesIO()
pil_kwargs_high = {"quality": 100}
plt.savefig(buf_large, format="webp", pil_kwargs=pil_kwargs_high)
assert len(pil_kwargs_high) == 1
assert buf_large.getbuffer().nbytes > buf_small.getbuffer().nbytes
@pytest.mark.skipif(not features.check("webp"), reason="WebP support not available")
def test_webp_alpha():
plt.plot([0, 1, 2], [0, 1, 0])
buf = io.BytesIO()
plt.savefig(buf, format="webp", transparent=True)
im = Image.open(buf)
assert im.mode == "RGBA"
def test_draw_path_collection_error_handling():
fig, ax = plt.subplots()
ax.scatter([1], [1]).set_paths(Path([(0, 1), (2, 3)]))
with pytest.raises(TypeError):
fig.canvas.draw()
def test_chunksize_fails():
# NOTE: This test covers multiple independent test scenarios in a single
# function, because each scenario uses ~2GB of memory and we don't
# want parallel test executors to accidentally run multiple of these
# at the same time.
N = 100_000
dpi = 500
w = 5*dpi
h = 6*dpi
# make a Path that spans the whole w-h rectangle
x = np.linspace(0, w, N)
y = np.ones(N) * h
y[::2] = 0
path = Path(np.vstack((x, y)).T)
# effectively disable path simplification (but leaving it "on")
path.simplify_threshold = 0
# setup the minimal GraphicsContext to draw a Path
ra = RendererAgg(w, h, dpi)
gc = ra.new_gc()
gc.set_linewidth(1)
gc.set_foreground('r')
gc.set_hatch('/')
with pytest.raises(OverflowError, match='cannot split hatched path'):
ra.draw_path(gc, path, IdentityTransform())
gc.set_hatch(None)
with pytest.raises(OverflowError, match='cannot split filled path'):
ra.draw_path(gc, path, IdentityTransform(), (1, 0, 0))
# Set to zero to disable, currently defaults to 0, but let's be sure.
with rc_context({'agg.path.chunksize': 0}):
with pytest.raises(OverflowError, match='Please set'):
ra.draw_path(gc, path, IdentityTransform())
# Set big enough that we do not try to chunk.
with rc_context({'agg.path.chunksize': 1_000_000}):
with pytest.raises(OverflowError, match='Please reduce'):
ra.draw_path(gc, path, IdentityTransform())
# Small enough we will try to chunk, but big enough we will fail to render.
with rc_context({'agg.path.chunksize': 90_000}):
with pytest.raises(OverflowError, match='Please reduce'):
ra.draw_path(gc, path, IdentityTransform())
path.should_simplify = False
with pytest.raises(OverflowError, match="should_simplify is False"):
ra.draw_path(gc, path, IdentityTransform())
def test_non_tuple_rgbaface():
# This passes rgbaFace as a ndarray to draw_path.
fig = plt.figure()
fig.add_subplot(projection="3d").scatter(
[0, 1, 2], [0, 1, 2], path_effects=[patheffects.Stroke(linewidth=4)])
fig.canvas.draw()
@@ -0,0 +1,33 @@
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import image_comparison
@image_comparison(baseline_images=['agg_filter_alpha'],
extensions=['png', 'pdf'])
def test_agg_filter_alpha():
# Remove this line when this test image is regenerated.
plt.rcParams['pcolormesh.snap'] = False
ax = plt.axes()
x, y = np.mgrid[0:7, 0:8]
data = x**2 - y**2
mesh = ax.pcolormesh(data, cmap='Reds', zorder=5)
def manual_alpha(im, dpi):
im[:, :, 3] *= 0.6
print('CALLED')
return im, 0, 0
# Note: Doing alpha like this is not the same as setting alpha on
# the mesh itself. Currently meshes are drawn as independent patches,
# and we see fine borders around the blocks of color. See the SO
# question for an example: https://stackoverflow.com/q/20678817/
mesh.set_agg_filter(manual_alpha)
# Currently we must enable rasterization for this to have an effect in
# the PDF backend.
mesh.set_rasterized(True)
ax.plot([0, 4, 7], [1, 3, 8])
@@ -0,0 +1,571 @@
import os
from pathlib import Path
import platform
import re
import shutil
import subprocess
import sys
import weakref
import numpy as np
import pytest
import matplotlib as mpl
from matplotlib import pyplot as plt
from matplotlib import animation
from matplotlib.animation import PillowWriter
from matplotlib.testing.decorators import check_figures_equal
@pytest.fixture()
def anim(request):
"""Create a simple animation (with options)."""
fig, ax = plt.subplots()
line, = ax.plot([], [])
ax.set_xlim(0, 10)
ax.set_ylim(-1, 1)
def init():
line.set_data([], [])
return line,
def animate(i):
x = np.linspace(0, 10, 100)
y = np.sin(x + i)
line.set_data(x, y)
return line,
# "klass" can be passed to determine the class returned by the fixture
kwargs = dict(getattr(request, 'param', {})) # make a copy
klass = kwargs.pop('klass', animation.FuncAnimation)
if 'frames' not in kwargs:
kwargs['frames'] = 5
return klass(fig=fig, func=animate, init_func=init, **kwargs)
class NullMovieWriter(animation.AbstractMovieWriter):
"""
A minimal MovieWriter. It doesn't actually write anything.
It just saves the arguments that were given to the setup() and
grab_frame() methods as attributes, and counts how many times
grab_frame() is called.
This class doesn't have an __init__ method with the appropriate
signature, and it doesn't define an isAvailable() method, so
it cannot be added to the 'writers' registry.
"""
def setup(self, fig, outfile, dpi, *args):
self.fig = fig
self.outfile = outfile
self.dpi = dpi
self.args = args
self._count = 0
def grab_frame(self, **savefig_kwargs):
from matplotlib.animation import _validate_grabframe_kwargs
_validate_grabframe_kwargs(savefig_kwargs)
self.savefig_kwargs = savefig_kwargs
self._count += 1
def finish(self):
pass
def test_null_movie_writer(anim):
# Test running an animation with NullMovieWriter.
plt.rcParams["savefig.facecolor"] = "auto"
filename = "unused.null"
dpi = 50
savefig_kwargs = dict(foo=0)
writer = NullMovieWriter()
anim.save(filename, dpi=dpi, writer=writer,
savefig_kwargs=savefig_kwargs)
assert writer.fig == plt.figure(1) # The figure used by anim fixture
assert writer.outfile == filename
assert writer.dpi == dpi
assert writer.args == ()
# we enrich the savefig kwargs to ensure we composite transparent
# output to an opaque background
for k, v in savefig_kwargs.items():
assert writer.savefig_kwargs[k] == v
assert writer._count == anim._save_count
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
def test_animation_delete(anim):
if platform.python_implementation() == 'PyPy':
# Something in the test setup fixture lingers around into the test and
# breaks pytest.warns on PyPy. This garbage collection fixes it.
# https://foss.heptapod.net/pypy/pypy/-/issues/3536
np.testing.break_cycles()
anim = animation.FuncAnimation(**anim)
with pytest.warns(Warning, match='Animation was deleted'):
del anim
np.testing.break_cycles()
def test_movie_writer_dpi_default():
class DummyMovieWriter(animation.MovieWriter):
def _run(self):
pass
# Test setting up movie writer with figure.dpi default.
fig = plt.figure()
filename = "unused.null"
fps = 5
codec = "unused"
bitrate = 1
extra_args = ["unused"]
writer = DummyMovieWriter(fps, codec, bitrate, extra_args)
writer.setup(fig, filename)
assert writer.dpi == fig.dpi
@animation.writers.register('null')
class RegisteredNullMovieWriter(NullMovieWriter):
# To be able to add NullMovieWriter to the 'writers' registry,
# we must define an __init__ method with a specific signature,
# and we must define the class method isAvailable().
# (These methods are not actually required to use an instance
# of this class as the 'writer' argument of Animation.save().)
def __init__(self, fps=None, codec=None, bitrate=None,
extra_args=None, metadata=None):
pass
@classmethod
def isAvailable(cls):
return True
WRITER_OUTPUT = [
('ffmpeg', 'movie.mp4'),
('ffmpeg_file', 'movie.mp4'),
('imagemagick', 'movie.gif'),
('imagemagick_file', 'movie.gif'),
('pillow', 'movie.gif'),
('html', 'movie.html'),
('null', 'movie.null')
]
def gen_writers():
for writer, output in WRITER_OUTPUT:
if not animation.writers.is_available(writer):
mark = pytest.mark.skip(
f"writer '{writer}' not available on this system")
yield pytest.param(writer, None, output, marks=[mark])
yield pytest.param(writer, None, Path(output), marks=[mark])
continue
writer_class = animation.writers[writer]
for frame_format in getattr(writer_class, 'supported_formats', [None]):
yield writer, frame_format, output
yield writer, frame_format, Path(output)
# Smoke test for saving animations. In the future, we should probably
# design more sophisticated tests which compare resulting frames a-la
# matplotlib.testing.image_comparison
@pytest.mark.parametrize('writer, frame_format, output', gen_writers())
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
def test_save_animation_smoketest(tmpdir, writer, frame_format, output, anim):
if frame_format is not None:
plt.rcParams["animation.frame_format"] = frame_format
anim = animation.FuncAnimation(**anim)
dpi = None
codec = None
if writer == 'ffmpeg':
# Issue #8253
anim._fig.set_size_inches((10.85, 9.21))
dpi = 100.
codec = 'h264'
# Use temporary directory for the file-based writers, which produce a file
# per frame with known names.
with tmpdir.as_cwd():
anim.save(output, fps=30, writer=writer, bitrate=500, dpi=dpi,
codec=codec)
del anim
@pytest.mark.parametrize('writer, frame_format, output', gen_writers())
def test_grabframe(tmpdir, writer, frame_format, output):
WriterClass = animation.writers[writer]
if frame_format is not None:
plt.rcParams["animation.frame_format"] = frame_format
fig, ax = plt.subplots()
dpi = None
codec = None
if writer == 'ffmpeg':
# Issue #8253
fig.set_size_inches((10.85, 9.21))
dpi = 100.
codec = 'h264'
test_writer = WriterClass()
# Use temporary directory for the file-based writers, which produce a file
# per frame with known names.
with tmpdir.as_cwd():
with test_writer.saving(fig, output, dpi):
# smoke test it works
test_writer.grab_frame()
for k in {'dpi', 'bbox_inches', 'format'}:
with pytest.raises(
TypeError,
match=f"grab_frame got an unexpected keyword argument {k!r}"
):
test_writer.grab_frame(**{k: object()})
@pytest.mark.parametrize('writer', [
pytest.param(
'ffmpeg', marks=pytest.mark.skipif(
not animation.FFMpegWriter.isAvailable(),
reason='Requires FFMpeg')),
pytest.param(
'imagemagick', marks=pytest.mark.skipif(
not animation.ImageMagickWriter.isAvailable(),
reason='Requires ImageMagick')),
])
@pytest.mark.parametrize('html, want', [
('none', None),
('html5', '<video width'),
('jshtml', '<script ')
])
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
def test_animation_repr_html(writer, html, want, anim):
if platform.python_implementation() == 'PyPy':
# Something in the test setup fixture lingers around into the test and
# breaks pytest.warns on PyPy. This garbage collection fixes it.
# https://foss.heptapod.net/pypy/pypy/-/issues/3536
np.testing.break_cycles()
if (writer == 'imagemagick' and html == 'html5'
# ImageMagick delegates to ffmpeg for this format.
and not animation.FFMpegWriter.isAvailable()):
pytest.skip('Requires FFMpeg')
# create here rather than in the fixture otherwise we get __del__ warnings
# about producing no output
anim = animation.FuncAnimation(**anim)
with plt.rc_context({'animation.writer': writer,
'animation.html': html}):
html = anim._repr_html_()
if want is None:
assert html is None
with pytest.warns(UserWarning):
del anim # Animation was never run, so will warn on cleanup.
np.testing.break_cycles()
else:
assert want in html
@pytest.mark.parametrize(
'anim',
[{'save_count': 10, 'frames': iter(range(5))}],
indirect=['anim']
)
def test_no_length_frames(anim):
anim.save('unused.null', writer=NullMovieWriter())
def test_movie_writer_registry():
assert len(animation.writers._registered) > 0
mpl.rcParams['animation.ffmpeg_path'] = "not_available_ever_xxxx"
assert not animation.writers.is_available("ffmpeg")
# something guaranteed to be available in path and exits immediately
bin = "true" if sys.platform != 'win32' else "where"
mpl.rcParams['animation.ffmpeg_path'] = bin
assert animation.writers.is_available("ffmpeg")
@pytest.mark.parametrize(
"method_name",
[pytest.param("to_html5_video", marks=pytest.mark.skipif(
not animation.writers.is_available(mpl.rcParams["animation.writer"]),
reason="animation writer not installed")),
"to_jshtml"])
@pytest.mark.parametrize('anim', [dict(frames=1)], indirect=['anim'])
def test_embed_limit(method_name, caplog, tmpdir, anim):
caplog.set_level("WARNING")
with tmpdir.as_cwd():
with mpl.rc_context({"animation.embed_limit": 1e-6}): # ~1 byte.
getattr(anim, method_name)()
assert len(caplog.records) == 1
record, = caplog.records
assert (record.name == "matplotlib.animation"
and record.levelname == "WARNING")
@pytest.mark.parametrize(
"method_name",
[pytest.param("to_html5_video", marks=pytest.mark.skipif(
not animation.writers.is_available(mpl.rcParams["animation.writer"]),
reason="animation writer not installed")),
"to_jshtml"])
@pytest.mark.parametrize('anim', [dict(frames=1)], indirect=['anim'])
def test_cleanup_temporaries(method_name, tmpdir, anim):
with tmpdir.as_cwd():
getattr(anim, method_name)()
assert list(Path(str(tmpdir)).iterdir()) == []
@pytest.mark.skipif(shutil.which("/bin/sh") is None, reason="requires a POSIX OS")
def test_failing_ffmpeg(tmpdir, monkeypatch, anim):
"""
Test that we correctly raise a CalledProcessError when ffmpeg fails.
To do so, mock ffmpeg using a simple executable shell script that
succeeds when called with no arguments (so that it gets registered by
`isAvailable`), but fails otherwise, and add it to the $PATH.
"""
with tmpdir.as_cwd():
monkeypatch.setenv("PATH", ".:" + os.environ["PATH"])
exe_path = Path(str(tmpdir), "ffmpeg")
exe_path.write_bytes(b"#!/bin/sh\n[[ $@ -eq 0 ]]\n")
os.chmod(exe_path, 0o755)
with pytest.raises(subprocess.CalledProcessError):
anim.save("test.mpeg")
@pytest.mark.parametrize("cache_frame_data", [False, True])
def test_funcanimation_cache_frame_data(cache_frame_data):
fig, ax = plt.subplots()
line, = ax.plot([], [])
class Frame(dict):
# this subclassing enables to use weakref.ref()
pass
def init():
line.set_data([], [])
return line,
def animate(frame):
line.set_data(frame['x'], frame['y'])
return line,
frames_generated = []
def frames_generator():
for _ in range(5):
x = np.linspace(0, 10, 100)
y = np.random.rand(100)
frame = Frame(x=x, y=y)
# collect weak references to frames
# to validate their references later
frames_generated.append(weakref.ref(frame))
yield frame
MAX_FRAMES = 100
anim = animation.FuncAnimation(fig, animate, init_func=init,
frames=frames_generator,
cache_frame_data=cache_frame_data,
save_count=MAX_FRAMES)
writer = NullMovieWriter()
anim.save('unused.null', writer=writer)
assert len(frames_generated) == 5
np.testing.break_cycles()
for f in frames_generated:
# If cache_frame_data is True, then the weakref should be alive;
# if cache_frame_data is False, then the weakref should be dead (None).
assert (f() is None) != cache_frame_data
@pytest.mark.parametrize('return_value', [
# User forgot to return (returns None).
None,
# User returned a string.
'string',
# User returned an int.
1,
# User returns a sequence of other objects, e.g., string instead of Artist.
('string', ),
# User forgot to return a sequence (handled in `animate` below.)
'artist',
])
def test_draw_frame(return_value):
# test _draw_frame method
fig, ax = plt.subplots()
line, = ax.plot([])
def animate(i):
# general update func
line.set_data([0, 1], [0, i])
if return_value == 'artist':
# *not* a sequence
return line
else:
return return_value
with pytest.raises(RuntimeError):
animation.FuncAnimation(
fig, animate, blit=True, cache_frame_data=False
)
def test_exhausted_animation(tmpdir):
fig, ax = plt.subplots()
def update(frame):
return []
anim = animation.FuncAnimation(
fig, update, frames=iter(range(10)), repeat=False,
cache_frame_data=False
)
with tmpdir.as_cwd():
anim.save("test.gif", writer='pillow')
with pytest.warns(UserWarning, match="exhausted"):
anim._start()
def test_no_frame_warning(tmpdir):
fig, ax = plt.subplots()
def update(frame):
return []
anim = animation.FuncAnimation(
fig, update, frames=[], repeat=False,
cache_frame_data=False
)
with pytest.warns(UserWarning, match="exhausted"):
anim._start()
@check_figures_equal(extensions=["png"])
def test_animation_frame(tmpdir, fig_test, fig_ref):
# Test the expected image after iterating through a few frames
# we save the animation to get the iteration because we are not
# in an interactive framework.
ax = fig_test.add_subplot()
ax.set_xlim(0, 2 * np.pi)
ax.set_ylim(-1, 1)
x = np.linspace(0, 2 * np.pi, 100)
line, = ax.plot([], [])
def init():
line.set_data([], [])
return line,
def animate(i):
line.set_data(x, np.sin(x + i / 100))
return line,
anim = animation.FuncAnimation(
fig_test, animate, init_func=init, frames=5,
blit=True, repeat=False)
with tmpdir.as_cwd():
anim.save("test.gif")
# Reference figure without animation
ax = fig_ref.add_subplot()
ax.set_xlim(0, 2 * np.pi)
ax.set_ylim(-1, 1)
# 5th frame's data
ax.plot(x, np.sin(x + 4 / 100))
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
def test_save_count_override_warnings_has_length(anim):
save_count = 5
frames = list(range(2))
match_target = (
f'You passed in an explicit {save_count=} '
"which is being ignored in favor of "
f"{len(frames)=}."
)
with pytest.warns(UserWarning, match=re.escape(match_target)):
anim = animation.FuncAnimation(
**{**anim, 'frames': frames, 'save_count': save_count}
)
assert anim._save_count == len(frames)
anim._init_draw()
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
def test_save_count_override_warnings_scaler(anim):
save_count = 5
frames = 7
match_target = (
f'You passed in an explicit {save_count=} ' +
"which is being ignored in favor of " +
f"{frames=}."
)
with pytest.warns(UserWarning, match=re.escape(match_target)):
anim = animation.FuncAnimation(
**{**anim, 'frames': frames, 'save_count': save_count}
)
assert anim._save_count == frames
anim._init_draw()
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
def test_disable_cache_warning(anim):
cache_frame_data = True
frames = iter(range(5))
match_target = (
f"{frames=!r} which we can infer the length of, "
"did not pass an explicit *save_count* "
f"and passed {cache_frame_data=}. To avoid a possibly "
"unbounded cache, frame data caching has been disabled. "
"To suppress this warning either pass "
"`cache_frame_data=False` or `save_count=MAX_FRAMES`."
)
with pytest.warns(UserWarning, match=re.escape(match_target)):
anim = animation.FuncAnimation(
**{**anim, 'cache_frame_data': cache_frame_data, 'frames': frames}
)
assert anim._cache_frame_data is False
anim._init_draw()
def test_movie_writer_invalid_path(anim):
if sys.platform == "win32":
match_str = r"\[WinError 3] .*'\\\\foo\\\\bar\\\\aardvark'"
else:
match_str = r"\[Errno 2] .*'/foo"
with pytest.raises(FileNotFoundError, match=match_str):
anim.save("/foo/bar/aardvark/thiscannotreallyexist.mp4",
writer=animation.FFMpegFileWriter())
def test_animation_with_transparency():
"""Test animation exhaustion with transparency using PillowWriter directly"""
fig, ax = plt.subplots()
rect = plt.Rectangle((0, 0), 1, 1, color='red', alpha=0.5)
ax.add_patch(rect)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
writer = PillowWriter(fps=30)
writer.setup(fig, 'unused.gif', dpi=100)
writer.grab_frame(transparent=True)
frame = writer._frames[-1]
# Check that the alpha channel is not 255, so frame has transparency
assert frame.getextrema()[3][0] < 255
plt.close(fig)
@@ -0,0 +1,152 @@
from __future__ import annotations
from collections.abc import Callable
import re
import typing
from typing import Any, TypeVar
import numpy as np
import pytest
import matplotlib as mpl
from matplotlib import _api
if typing.TYPE_CHECKING:
from typing_extensions import Self
T = TypeVar('T')
@pytest.mark.parametrize('target,shape_repr,test_shape',
[((None, ), "(N,)", (1, 3)),
((None, 3), "(N, 3)", (1,)),
((None, 3), "(N, 3)", (1, 2)),
((1, 5), "(1, 5)", (1, 9)),
((None, 2, None), "(M, 2, N)", (1, 3, 1))
])
def test_check_shape(target: tuple[int | None, ...],
shape_repr: str,
test_shape: tuple[int, ...]) -> None:
error_pattern = "^" + re.escape(
f"'aardvark' must be {len(target)}D with shape {shape_repr}, but your input "
f"has shape {test_shape}")
data = np.zeros(test_shape)
with pytest.raises(ValueError, match=error_pattern):
_api.check_shape(target, aardvark=data)
def test_classproperty_deprecation() -> None:
class A:
@_api.deprecated("0.0.0")
@_api.classproperty
def f(cls: Self) -> None:
pass
with pytest.warns(mpl.MatplotlibDeprecationWarning):
A.f
with pytest.warns(mpl.MatplotlibDeprecationWarning):
a = A()
a.f
def test_warn_deprecated():
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match=r'foo was deprecated in Matplotlib 3\.10 and will be '
r'removed in 3\.12\.'):
_api.warn_deprecated('3.10', name='foo')
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match=r'The foo class was deprecated in Matplotlib 3\.10 and '
r'will be removed in 3\.12\.'):
_api.warn_deprecated('3.10', name='foo', obj_type='class')
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match=r'foo was deprecated in Matplotlib 3\.10 and will be '
r'removed in 3\.12\. Use bar instead\.'):
_api.warn_deprecated('3.10', name='foo', alternative='bar')
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match=r'foo was deprecated in Matplotlib 3\.10 and will be '
r'removed in 3\.12\. More information\.'):
_api.warn_deprecated('3.10', name='foo', addendum='More information.')
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match=r'foo was deprecated in Matplotlib 3\.10 and will be '
r'removed in 4\.0\.'):
_api.warn_deprecated('3.10', name='foo', removal='4.0')
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match=r'foo was deprecated in Matplotlib 3\.10\.'):
_api.warn_deprecated('3.10', name='foo', removal=False)
with pytest.warns(PendingDeprecationWarning,
match=r'foo will be deprecated in a future version'):
_api.warn_deprecated('3.10', name='foo', pending=True)
with pytest.raises(ValueError, match=r'cannot have a scheduled removal'):
_api.warn_deprecated('3.10', name='foo', pending=True, removal='3.12')
with pytest.warns(mpl.MatplotlibDeprecationWarning, match=r'Complete replacement'):
_api.warn_deprecated('3.10', message='Complete replacement', name='foo',
alternative='bar', addendum='More information.',
obj_type='class', removal='4.0')
def test_deprecate_privatize_attribute() -> None:
class C:
def __init__(self) -> None: self._attr = 1
def _meth(self, arg: T) -> T: return arg
attr: int = _api.deprecate_privatize_attribute("0.0")
meth: Callable = _api.deprecate_privatize_attribute("0.0")
c = C()
with pytest.warns(mpl.MatplotlibDeprecationWarning):
assert c.attr == 1
with pytest.warns(mpl.MatplotlibDeprecationWarning):
c.attr = 2
with pytest.warns(mpl.MatplotlibDeprecationWarning):
assert c.attr == 2
with pytest.warns(mpl.MatplotlibDeprecationWarning):
assert c.meth(42) == 42
def test_delete_parameter() -> None:
@_api.delete_parameter("3.0", "foo")
def func1(foo: Any = None) -> None:
pass
@_api.delete_parameter("3.0", "foo")
def func2(**kwargs: Any) -> None:
pass
for func in [func1, func2]: # type: ignore[list-item]
func() # No warning.
with pytest.warns(mpl.MatplotlibDeprecationWarning):
func(foo="bar")
def pyplot_wrapper(foo: Any = _api.deprecation._deprecated_parameter) -> None:
func1(foo)
pyplot_wrapper() # No warning.
with pytest.warns(mpl.MatplotlibDeprecationWarning):
func(foo="bar")
def test_make_keyword_only() -> None:
@_api.make_keyword_only("3.0", "arg")
def func(pre: Any, arg: Any, post: Any = None) -> None:
pass
func(1, arg=2) # Check that no warning is emitted.
with pytest.warns(mpl.MatplotlibDeprecationWarning):
func(1, 2)
with pytest.warns(mpl.MatplotlibDeprecationWarning):
func(1, 2, 3)
def test_deprecation_alternative() -> None:
alternative = "`.f1`, `f2`, `f3(x) <.f3>` or `f4(x)<f4>`"
@_api.deprecated("1", alternative=alternative)
def f() -> None:
pass
if f.__doc__ is None:
pytest.skip('Documentation is disabled')
assert alternative in f.__doc__
def test_empty_check_in_list() -> None:
with pytest.raises(TypeError, match="No argument to check!"):
_api.check_in_list(["a"])
@@ -0,0 +1,179 @@
import pytest
import platform
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import image_comparison
import matplotlib.patches as mpatches
def draw_arrow(ax, t, r):
ax.annotate('', xy=(0.5, 0.5 + r), xytext=(0.5, 0.5), size=30,
arrowprops=dict(arrowstyle=t,
fc="b", ec='k'))
@image_comparison(['fancyarrow_test_image.png'],
tol=0 if platform.machine() == 'x86_64' else 0.012)
def test_fancyarrow():
# Added 0 to test division by zero error described in issue 3930
r = [0.4, 0.3, 0.2, 0.1, 0]
t = ["fancy", "simple", mpatches.ArrowStyle.Fancy()]
fig, axs = plt.subplots(len(t), len(r), squeeze=False,
figsize=(8, 4.5), subplot_kw=dict(aspect=1))
for i_r, r1 in enumerate(r):
for i_t, t1 in enumerate(t):
ax = axs[i_t, i_r]
draw_arrow(ax, t1, r1)
ax.tick_params(labelleft=False, labelbottom=False)
@image_comparison(['boxarrow_test_image.png'])
def test_boxarrow():
styles = mpatches.BoxStyle.get_styles()
n = len(styles)
spacing = 1.2
figheight = (n * spacing + .5)
fig = plt.figure(figsize=(4 / 1.5, figheight / 1.5))
fontsize = 0.3 * 72
for i, stylename in enumerate(sorted(styles)):
fig.text(0.5, ((n - i) * spacing - 0.5)/figheight, stylename,
ha="center",
size=fontsize,
transform=fig.transFigure,
bbox=dict(boxstyle=stylename, fc="w", ec="k"))
def __prepare_fancyarrow_dpi_cor_test():
"""
Convenience function that prepares and returns a FancyArrowPatch. It aims
at being used to test that the size of the arrow head does not depend on
the DPI value of the exported picture.
NB: this function *is not* a test in itself!
"""
fig2 = plt.figure("fancyarrow_dpi_cor_test", figsize=(4, 3), dpi=50)
ax = fig2.add_subplot()
ax.set_xlim([0, 1])
ax.set_ylim([0, 1])
ax.add_patch(mpatches.FancyArrowPatch(posA=(0.3, 0.4), posB=(0.8, 0.6),
lw=3, arrowstyle='->',
mutation_scale=100))
return fig2
@image_comparison(['fancyarrow_dpi_cor_100dpi.png'], remove_text=True,
tol=0 if platform.machine() == 'x86_64' else 0.02,
savefig_kwarg=dict(dpi=100))
def test_fancyarrow_dpi_cor_100dpi():
"""
Check the export of a FancyArrowPatch @ 100 DPI. FancyArrowPatch is
instantiated through a dedicated function because another similar test
checks a similar export but with a different DPI value.
Remark: test only a rasterized format.
"""
__prepare_fancyarrow_dpi_cor_test()
@image_comparison(['fancyarrow_dpi_cor_200dpi.png'], remove_text=True,
tol=0 if platform.machine() == 'x86_64' else 0.02,
savefig_kwarg=dict(dpi=200))
def test_fancyarrow_dpi_cor_200dpi():
"""
As test_fancyarrow_dpi_cor_100dpi, but exports @ 200 DPI. The relative size
of the arrow head should be the same.
"""
__prepare_fancyarrow_dpi_cor_test()
@image_comparison(['fancyarrow_dash.png'], remove_text=True, style='default')
def test_fancyarrow_dash():
fig, ax = plt.subplots()
e = mpatches.FancyArrowPatch((0, 0), (0.5, 0.5),
arrowstyle='-|>',
connectionstyle='angle3,angleA=0,angleB=90',
mutation_scale=10.0,
linewidth=2,
linestyle='dashed',
color='k')
e2 = mpatches.FancyArrowPatch((0, 0), (0.5, 0.5),
arrowstyle='-|>',
connectionstyle='angle3',
mutation_scale=10.0,
linewidth=2,
linestyle='dotted',
color='k')
ax.add_patch(e)
ax.add_patch(e2)
@image_comparison(['arrow_styles.png'], style='mpl20', remove_text=True,
tol=0 if platform.machine() == 'x86_64' else 0.02)
def test_arrow_styles():
styles = mpatches.ArrowStyle.get_styles()
n = len(styles)
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_xlim(0, 1)
ax.set_ylim(-1, n)
fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
for i, stylename in enumerate(sorted(styles)):
patch = mpatches.FancyArrowPatch((0.1 + (i % 2)*0.05, i),
(0.45 + (i % 2)*0.05, i),
arrowstyle=stylename,
mutation_scale=25)
ax.add_patch(patch)
for i, stylename in enumerate([']-[', ']-', '-[', '|-|']):
style = stylename
if stylename[0] != '-':
style += ',angleA=ANGLE'
if stylename[-1] != '-':
style += ',angleB=ANGLE'
for j, angle in enumerate([-30, 60]):
arrowstyle = style.replace('ANGLE', str(angle))
patch = mpatches.FancyArrowPatch((0.55, 2*i + j), (0.9, 2*i + j),
arrowstyle=arrowstyle,
mutation_scale=25)
ax.add_patch(patch)
@image_comparison(['connection_styles.png'], style='mpl20', remove_text=True,
tol=0 if platform.machine() == 'x86_64' else 0.013)
def test_connection_styles():
styles = mpatches.ConnectionStyle.get_styles()
n = len(styles)
fig, ax = plt.subplots(figsize=(6, 10))
ax.set_xlim(0, 1)
ax.set_ylim(-1, n)
for i, stylename in enumerate(sorted(styles)):
patch = mpatches.FancyArrowPatch((0.1, i), (0.8, i + 0.5),
arrowstyle="->",
connectionstyle=stylename,
mutation_scale=25)
ax.add_patch(patch)
def test_invalid_intersection():
conn_style_1 = mpatches.ConnectionStyle.Angle3(angleA=20, angleB=200)
p1 = mpatches.FancyArrowPatch((.2, .2), (.5, .5),
connectionstyle=conn_style_1)
with pytest.raises(ValueError):
plt.gca().add_patch(p1)
conn_style_2 = mpatches.ConnectionStyle.Angle3(angleA=20, angleB=199.9)
p2 = mpatches.FancyArrowPatch((.2, .2), (.5, .5),
connectionstyle=conn_style_2)
plt.gca().add_patch(p2)
@@ -0,0 +1,598 @@
import io
from itertools import chain
import numpy as np
import pytest
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.lines as mlines
import matplotlib.path as mpath
import matplotlib.transforms as mtransforms
import matplotlib.collections as mcollections
import matplotlib.artist as martist
import matplotlib.backend_bases as mbackend_bases
import matplotlib as mpl
from matplotlib.testing.decorators import check_figures_equal, image_comparison
def test_patch_transform_of_none():
# tests the behaviour of patches added to an Axes with various transform
# specifications
ax = plt.axes()
ax.set_xlim(1, 3)
ax.set_ylim(1, 3)
# Draw an ellipse over data coord (2, 2) by specifying device coords.
xy_data = (2, 2)
xy_pix = ax.transData.transform(xy_data)
# Not providing a transform of None puts the ellipse in data coordinates .
e = mpatches.Ellipse(xy_data, width=1, height=1, fc='yellow', alpha=0.5)
ax.add_patch(e)
assert e._transform == ax.transData
# Providing a transform of None puts the ellipse in device coordinates.
e = mpatches.Ellipse(xy_pix, width=120, height=120, fc='coral',
transform=None, alpha=0.5)
assert e.is_transform_set()
ax.add_patch(e)
assert isinstance(e._transform, mtransforms.IdentityTransform)
# Providing an IdentityTransform puts the ellipse in device coordinates.
e = mpatches.Ellipse(xy_pix, width=100, height=100,
transform=mtransforms.IdentityTransform(), alpha=0.5)
ax.add_patch(e)
assert isinstance(e._transform, mtransforms.IdentityTransform)
# Not providing a transform, and then subsequently "get_transform" should
# not mean that "is_transform_set".
e = mpatches.Ellipse(xy_pix, width=120, height=120, fc='coral',
alpha=0.5)
intermediate_transform = e.get_transform()
assert not e.is_transform_set()
ax.add_patch(e)
assert e.get_transform() != intermediate_transform
assert e.is_transform_set()
assert e._transform == ax.transData
def test_collection_transform_of_none():
# tests the behaviour of collections added to an Axes with various
# transform specifications
ax = plt.axes()
ax.set_xlim(1, 3)
ax.set_ylim(1, 3)
# draw an ellipse over data coord (2, 2) by specifying device coords
xy_data = (2, 2)
xy_pix = ax.transData.transform(xy_data)
# not providing a transform of None puts the ellipse in data coordinates
e = mpatches.Ellipse(xy_data, width=1, height=1)
c = mcollections.PatchCollection([e], facecolor='yellow', alpha=0.5)
ax.add_collection(c)
# the collection should be in data coordinates
assert c.get_offset_transform() + c.get_transform() == ax.transData
# providing a transform of None puts the ellipse in device coordinates
e = mpatches.Ellipse(xy_pix, width=120, height=120)
c = mcollections.PatchCollection([e], facecolor='coral',
alpha=0.5)
c.set_transform(None)
ax.add_collection(c)
assert isinstance(c.get_transform(), mtransforms.IdentityTransform)
# providing an IdentityTransform puts the ellipse in device coordinates
e = mpatches.Ellipse(xy_pix, width=100, height=100)
c = mcollections.PatchCollection([e],
transform=mtransforms.IdentityTransform(),
alpha=0.5)
ax.add_collection(c)
assert isinstance(c.get_offset_transform(), mtransforms.IdentityTransform)
@image_comparison(["clip_path_clipping"], remove_text=True)
def test_clipping():
exterior = mpath.Path.unit_rectangle().deepcopy()
exterior.vertices *= 4
exterior.vertices -= 2
interior = mpath.Path.unit_circle().deepcopy()
interior.vertices = interior.vertices[::-1]
clip_path = mpath.Path.make_compound_path(exterior, interior)
star = mpath.Path.unit_regular_star(6).deepcopy()
star.vertices *= 2.6
fig, (ax1, ax2) = plt.subplots(1, 2, sharex=True, sharey=True)
col = mcollections.PathCollection([star], lw=5, edgecolor='blue',
facecolor='red', alpha=0.7, hatch='*')
col.set_clip_path(clip_path, ax1.transData)
ax1.add_collection(col)
patch = mpatches.PathPatch(star, lw=5, edgecolor='blue', facecolor='red',
alpha=0.7, hatch='*')
patch.set_clip_path(clip_path, ax2.transData)
ax2.add_patch(patch)
ax1.set_xlim([-3, 3])
ax1.set_ylim([-3, 3])
@check_figures_equal(extensions=['png'])
def test_clipping_zoom(fig_test, fig_ref):
# This test places the Axes and sets its limits such that the clip path is
# outside the figure entirely. This should not break the clip path.
ax_test = fig_test.add_axes([0, 0, 1, 1])
l, = ax_test.plot([-3, 3], [-3, 3])
# Explicit Path instead of a Rectangle uses clip path processing, instead
# of a clip box optimization.
p = mpath.Path([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]])
p = mpatches.PathPatch(p, transform=ax_test.transData)
l.set_clip_path(p)
ax_ref = fig_ref.add_axes([0, 0, 1, 1])
ax_ref.plot([-3, 3], [-3, 3])
ax_ref.set(xlim=(0.5, 0.75), ylim=(0.5, 0.75))
ax_test.set(xlim=(0.5, 0.75), ylim=(0.5, 0.75))
def test_cull_markers():
x = np.random.random(20000)
y = np.random.random(20000)
fig, ax = plt.subplots()
ax.plot(x, y, 'k.')
ax.set_xlim(2, 3)
pdf = io.BytesIO()
fig.savefig(pdf, format="pdf")
assert len(pdf.getvalue()) < 8000
svg = io.BytesIO()
fig.savefig(svg, format="svg")
assert len(svg.getvalue()) < 20000
@image_comparison(['hatching'], remove_text=True, style='default')
def test_hatching():
fig, ax = plt.subplots(1, 1)
# Default hatch color.
rect1 = mpatches.Rectangle((0, 0), 3, 4, hatch='/')
ax.add_patch(rect1)
rect2 = mcollections.RegularPolyCollection(
4, sizes=[16000], offsets=[(1.5, 6.5)], offset_transform=ax.transData,
hatch='/')
ax.add_collection(rect2)
# Ensure edge color is not applied to hatching.
rect3 = mpatches.Rectangle((4, 0), 3, 4, hatch='/', edgecolor='C1')
ax.add_patch(rect3)
rect4 = mcollections.RegularPolyCollection(
4, sizes=[16000], offsets=[(5.5, 6.5)], offset_transform=ax.transData,
hatch='/', edgecolor='C1')
ax.add_collection(rect4)
ax.set_xlim(0, 7)
ax.set_ylim(0, 9)
def test_remove():
fig, ax = plt.subplots()
im = ax.imshow(np.arange(36).reshape(6, 6))
ln, = ax.plot(range(5))
assert fig.stale
assert ax.stale
fig.canvas.draw()
assert not fig.stale
assert not ax.stale
assert not ln.stale
assert im in ax._mouseover_set
assert ln not in ax._mouseover_set
assert im.axes is ax
im.remove()
ln.remove()
for art in [im, ln]:
assert art.axes is None
assert art.get_figure() is None
assert im not in ax._mouseover_set
assert fig.stale
assert ax.stale
@image_comparison(["default_edges.png"], remove_text=True, style='default')
def test_default_edges():
# Remove this line when this test image is regenerated.
plt.rcParams['text.kerning_factor'] = 6
fig, [[ax1, ax2], [ax3, ax4]] = plt.subplots(2, 2)
ax1.plot(np.arange(10), np.arange(10), 'x',
np.arange(10) + 1, np.arange(10), 'o')
ax2.bar(np.arange(10), np.arange(10), align='edge')
ax3.text(0, 0, "BOX", size=24, bbox=dict(boxstyle='sawtooth'))
ax3.set_xlim((-1, 1))
ax3.set_ylim((-1, 1))
pp1 = mpatches.PathPatch(
mpath.Path([(0, 0), (1, 0), (1, 1), (0, 0)],
[mpath.Path.MOVETO, mpath.Path.CURVE3,
mpath.Path.CURVE3, mpath.Path.CLOSEPOLY]),
fc="none", transform=ax4.transData)
ax4.add_patch(pp1)
def test_properties():
ln = mlines.Line2D([], [])
ln.properties() # Check that no warning is emitted.
def test_setp():
# Check empty list
plt.setp([])
plt.setp([[]])
# Check arbitrary iterables
fig, ax = plt.subplots()
lines1 = ax.plot(range(3))
lines2 = ax.plot(range(3))
martist.setp(chain(lines1, lines2), 'lw', 5)
plt.setp(ax.spines.values(), color='green')
# Check *file* argument
sio = io.StringIO()
plt.setp(lines1, 'zorder', file=sio)
assert sio.getvalue() == ' zorder: float\n'
def test_None_zorder():
fig, ax = plt.subplots()
ln, = ax.plot(range(5), zorder=None)
assert ln.get_zorder() == mlines.Line2D.zorder
ln.set_zorder(123456)
assert ln.get_zorder() == 123456
ln.set_zorder(None)
assert ln.get_zorder() == mlines.Line2D.zorder
@pytest.mark.parametrize('accept_clause, expected', [
('', 'unknown'),
("ACCEPTS: [ '-' | '--' | '-.' ]", "[ '-' | '--' | '-.' ]"),
('ACCEPTS: Some description.', 'Some description.'),
('.. ACCEPTS: Some description.', 'Some description.'),
('arg : int', 'int'),
('*arg : int', 'int'),
('arg : int\nACCEPTS: Something else.', 'Something else. '),
])
def test_artist_inspector_get_valid_values(accept_clause, expected):
class TestArtist(martist.Artist):
def set_f(self, arg):
pass
TestArtist.set_f.__doc__ = """
Some text.
%s
""" % accept_clause
valid_values = martist.ArtistInspector(TestArtist).get_valid_values('f')
assert valid_values == expected
def test_artist_inspector_get_aliases():
# test the correct format and type of get_aliases method
ai = martist.ArtistInspector(mlines.Line2D)
aliases = ai.get_aliases()
assert aliases["linewidth"] == {"lw"}
def test_set_alpha():
art = martist.Artist()
with pytest.raises(TypeError, match='^alpha must be numeric or None'):
art.set_alpha('string')
with pytest.raises(TypeError, match='^alpha must be numeric or None'):
art.set_alpha([1, 2, 3])
with pytest.raises(ValueError, match="outside 0-1 range"):
art.set_alpha(1.1)
with pytest.raises(ValueError, match="outside 0-1 range"):
art.set_alpha(np.nan)
def test_set_alpha_for_array():
art = martist.Artist()
with pytest.raises(TypeError, match='^alpha must be numeric or None'):
art._set_alpha_for_array('string')
with pytest.raises(ValueError, match="outside 0-1 range"):
art._set_alpha_for_array(1.1)
with pytest.raises(ValueError, match="outside 0-1 range"):
art._set_alpha_for_array(np.nan)
with pytest.raises(ValueError, match="alpha must be between 0 and 1"):
art._set_alpha_for_array([0.5, 1.1])
with pytest.raises(ValueError, match="alpha must be between 0 and 1"):
art._set_alpha_for_array([0.5, np.nan])
def test_callbacks():
def func(artist):
func.counter += 1
func.counter = 0
art = martist.Artist()
oid = art.add_callback(func)
assert func.counter == 0
art.pchanged() # must call the callback
assert func.counter == 1
art.set_zorder(10) # setting a property must also call the callback
assert func.counter == 2
art.remove_callback(oid)
art.pchanged() # must not call the callback anymore
assert func.counter == 2
def test_set_signature():
"""Test autogenerated ``set()`` for Artist subclasses."""
class MyArtist1(martist.Artist):
def set_myparam1(self, val):
pass
assert hasattr(MyArtist1.set, '_autogenerated_signature')
assert 'myparam1' in MyArtist1.set.__doc__
class MyArtist2(MyArtist1):
def set_myparam2(self, val):
pass
assert hasattr(MyArtist2.set, '_autogenerated_signature')
assert 'myparam1' in MyArtist2.set.__doc__
assert 'myparam2' in MyArtist2.set.__doc__
def test_set_is_overwritten():
"""set() defined in Artist subclasses should not be overwritten."""
class MyArtist3(martist.Artist):
def set(self, **kwargs):
"""Not overwritten."""
assert not hasattr(MyArtist3.set, '_autogenerated_signature')
assert MyArtist3.set.__doc__ == "Not overwritten."
class MyArtist4(MyArtist3):
pass
assert MyArtist4.set is MyArtist3.set
def test_format_cursor_data_BoundaryNorm():
"""Test if cursor data is correct when using BoundaryNorm."""
X = np.empty((3, 3))
X[0, 0] = 0.9
X[0, 1] = 0.99
X[0, 2] = 0.999
X[1, 0] = -1
X[1, 1] = 0
X[1, 2] = 1
X[2, 0] = 0.09
X[2, 1] = 0.009
X[2, 2] = 0.0009
# map range -1..1 to 0..256 in 0.1 steps
fig, ax = plt.subplots()
fig.suptitle("-1..1 to 0..256 in 0.1")
norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 20), 256)
img = ax.imshow(X, cmap='RdBu_r', norm=norm)
labels_list = [
"[0.9]",
"[1.]",
"[1.]",
"[-1.0]",
"[0.0]",
"[1.0]",
"[0.09]",
"[0.009]",
"[0.0009]",
]
for v, label in zip(X.flat, labels_list):
# label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.1))
assert img.format_cursor_data(v) == label
plt.close()
# map range -1..1 to 0..256 in 0.01 steps
fig, ax = plt.subplots()
fig.suptitle("-1..1 to 0..256 in 0.01")
cmap = mpl.colormaps['RdBu_r'].resampled(200)
norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 200), 200)
img = ax.imshow(X, cmap=cmap, norm=norm)
labels_list = [
"[0.90]",
"[0.99]",
"[1.0]",
"[-1.00]",
"[0.00]",
"[1.00]",
"[0.09]",
"[0.009]",
"[0.0009]",
]
for v, label in zip(X.flat, labels_list):
# label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.01))
assert img.format_cursor_data(v) == label
plt.close()
# map range -1..1 to 0..256 in 0.01 steps
fig, ax = plt.subplots()
fig.suptitle("-1..1 to 0..256 in 0.001")
cmap = mpl.colormaps['RdBu_r'].resampled(2000)
norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 2000), 2000)
img = ax.imshow(X, cmap=cmap, norm=norm)
labels_list = [
"[0.900]",
"[0.990]",
"[0.999]",
"[-1.000]",
"[0.000]",
"[1.000]",
"[0.090]",
"[0.009]",
"[0.0009]",
]
for v, label in zip(X.flat, labels_list):
# label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.001))
assert img.format_cursor_data(v) == label
plt.close()
# different testing data set with
# out of bounds values for 0..1 range
X = np.empty((7, 1))
X[0] = -1.0
X[1] = 0.0
X[2] = 0.1
X[3] = 0.5
X[4] = 0.9
X[5] = 1.0
X[6] = 2.0
labels_list = [
"[-1.0]",
"[0.0]",
"[0.1]",
"[0.5]",
"[0.9]",
"[1.0]",
"[2.0]",
]
fig, ax = plt.subplots()
fig.suptitle("noclip, neither")
norm = mcolors.BoundaryNorm(
np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='neither')
img = ax.imshow(X, cmap='RdBu_r', norm=norm)
for v, label in zip(X.flat, labels_list):
# label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33))
assert img.format_cursor_data(v) == label
plt.close()
fig, ax = plt.subplots()
fig.suptitle("noclip, min")
norm = mcolors.BoundaryNorm(
np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='min')
img = ax.imshow(X, cmap='RdBu_r', norm=norm)
for v, label in zip(X.flat, labels_list):
# label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33))
assert img.format_cursor_data(v) == label
plt.close()
fig, ax = plt.subplots()
fig.suptitle("noclip, max")
norm = mcolors.BoundaryNorm(
np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='max')
img = ax.imshow(X, cmap='RdBu_r', norm=norm)
for v, label in zip(X.flat, labels_list):
# label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33))
assert img.format_cursor_data(v) == label
plt.close()
fig, ax = plt.subplots()
fig.suptitle("noclip, both")
norm = mcolors.BoundaryNorm(
np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='both')
img = ax.imshow(X, cmap='RdBu_r', norm=norm)
for v, label in zip(X.flat, labels_list):
# label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33))
assert img.format_cursor_data(v) == label
plt.close()
fig, ax = plt.subplots()
fig.suptitle("clip, neither")
norm = mcolors.BoundaryNorm(
np.linspace(0, 1, 4, endpoint=True), 256, clip=True, extend='neither')
img = ax.imshow(X, cmap='RdBu_r', norm=norm)
for v, label in zip(X.flat, labels_list):
# label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33))
assert img.format_cursor_data(v) == label
plt.close()
def test_auto_no_rasterize():
class Gen1(martist.Artist):
...
assert 'draw' in Gen1.__dict__
assert Gen1.__dict__['draw'] is Gen1.draw
class Gen2(Gen1):
...
assert 'draw' not in Gen2.__dict__
assert Gen2.draw is Gen1.draw
def test_draw_wraper_forward_input():
class TestKlass(martist.Artist):
def draw(self, renderer, extra):
return extra
art = TestKlass()
renderer = mbackend_bases.RendererBase()
assert 'aardvark' == art.draw(renderer, 'aardvark')
assert 'aardvark' == art.draw(renderer, extra='aardvark')
def test_get_figure():
fig = plt.figure()
sfig1 = fig.subfigures()
sfig2 = sfig1.subfigures()
ax = sfig2.subplots()
assert fig.get_figure(root=True) is fig
assert fig.get_figure(root=False) is fig
assert ax.get_figure() is sfig2
assert ax.get_figure(root=False) is sfig2
assert ax.get_figure(root=True) is fig
# SubFigure.get_figure has separate implementation but should give consistent
# results to other artists.
assert sfig2.get_figure(root=False) is sfig1
assert sfig2.get_figure(root=True) is fig
# Currently different results by default.
with pytest.warns(mpl.MatplotlibDeprecationWarning):
assert sfig2.get_figure() is fig
# No deprecation warning if root and parent figure are the same.
assert sfig1.get_figure() is fig
# An artist not yet attached to anything has no figure.
ln = mlines.Line2D([], [])
assert ln.get_figure(root=True) is None
assert ln.get_figure(root=False) is None
# figure attribute is root for (Sub)Figures but parent for other artists.
assert ax.figure is sfig2
assert fig.figure is fig
assert sfig2.figure is fig
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.axis import XTick
def test_tick_labelcolor_array():
# Smoke test that we can instantiate a Tick with labelcolor as array.
ax = plt.axes()
XTick(ax, 0, labelcolor=np.array([1, 0, 0, 1]))
def test_axis_not_in_layout():
fig1, (ax1_left, ax1_right) = plt.subplots(ncols=2, layout='constrained')
fig2, (ax2_left, ax2_right) = plt.subplots(ncols=2, layout='constrained')
# 100 label overlapping the end of the axis
ax1_left.set_xlim([0, 100])
# 100 label not overlapping the end of the axis
ax2_left.set_xlim([0, 120])
for ax in ax1_left, ax2_left:
ax.set_xticks([0, 100])
ax.xaxis.set_in_layout(False)
for fig in fig1, fig2:
fig.draw_without_rendering()
# Positions should not be affected by overlapping 100 label
assert ax1_left.get_position().bounds == ax2_left.get_position().bounds
assert ax1_right.get_position().bounds == ax2_right.get_position().bounds
def test_translate_tick_params_reverse():
fig, ax = plt.subplots()
kw = {'label1On': 'a', 'label2On': 'b', 'tick1On': 'c', 'tick2On': 'd'}
assert (ax.xaxis._translate_tick_params(kw, reverse=True) ==
{'labelbottom': 'a', 'labeltop': 'b', 'bottom': 'c', 'top': 'd'})
assert (ax.yaxis._translate_tick_params(kw, reverse=True) ==
{'labelleft': 'a', 'labelright': 'b', 'left': 'c', 'right': 'd'})
@@ -0,0 +1,586 @@
import importlib
from matplotlib import path, transforms
from matplotlib.backend_bases import (
FigureCanvasBase, KeyEvent, LocationEvent, MouseButton, MouseEvent,
NavigationToolbar2, RendererBase)
from matplotlib.backend_tools import RubberbandBase
from matplotlib.figure import Figure
from matplotlib.testing._markers import needs_pgf_xelatex
import matplotlib.pyplot as plt
import numpy as np
import pytest
_EXPECTED_WARNING_TOOLMANAGER = (
r"Treat the new Tool classes introduced in "
r"v[0-9]*.[0-9]* as experimental for now; "
"the API and rcParam may change in future versions.")
def test_uses_per_path():
id = transforms.Affine2D()
paths = [path.Path.unit_regular_polygon(i) for i in range(3, 7)]
tforms_matrices = [id.rotate(i).get_matrix().copy() for i in range(1, 5)]
offsets = np.arange(20).reshape((10, 2))
facecolors = ['red', 'green']
edgecolors = ['red', 'green']
def check(master_transform, paths, all_transforms,
offsets, facecolors, edgecolors):
rb = RendererBase()
raw_paths = list(rb._iter_collection_raw_paths(
master_transform, paths, all_transforms))
gc = rb.new_gc()
ids = [path_id for xo, yo, path_id, gc0, rgbFace in
rb._iter_collection(
gc, range(len(raw_paths)), offsets,
transforms.AffineDeltaTransform(master_transform),
facecolors, edgecolors, [], [], [False],
[], 'screen')]
uses = rb._iter_collection_uses_per_path(
paths, all_transforms, offsets, facecolors, edgecolors)
if raw_paths:
seen = np.bincount(ids, minlength=len(raw_paths))
assert set(seen).issubset([uses - 1, uses])
check(id, paths, tforms_matrices, offsets, facecolors, edgecolors)
check(id, paths[0:1], tforms_matrices, offsets, facecolors, edgecolors)
check(id, [], tforms_matrices, offsets, facecolors, edgecolors)
check(id, paths, tforms_matrices[0:1], offsets, facecolors, edgecolors)
check(id, paths, [], offsets, facecolors, edgecolors)
for n in range(0, offsets.shape[0]):
check(id, paths, tforms_matrices, offsets[0:n, :],
facecolors, edgecolors)
check(id, paths, tforms_matrices, offsets, [], edgecolors)
check(id, paths, tforms_matrices, offsets, facecolors, [])
check(id, paths, tforms_matrices, offsets, [], [])
check(id, paths, tforms_matrices, offsets, facecolors[0:1], edgecolors)
def test_canvas_ctor():
assert isinstance(FigureCanvasBase().figure, Figure)
def test_get_default_filename():
fig = plt.figure()
assert fig.canvas.get_default_filename() == "Figure_1.png"
fig.canvas.manager.set_window_title("0:1/2<3")
assert fig.canvas.get_default_filename() == "0_1_2_3.png"
def test_canvas_change():
fig = plt.figure()
# Replaces fig.canvas
canvas = FigureCanvasBase(fig)
# Should still work.
plt.close(fig)
assert not plt.fignum_exists(fig.number)
@pytest.mark.backend('pdf')
def test_non_gui_warning(monkeypatch):
plt.subplots()
monkeypatch.setenv("DISPLAY", ":999")
with pytest.warns(UserWarning) as rec:
plt.show()
assert len(rec) == 1
assert ('FigureCanvasPdf is non-interactive, and thus cannot be shown'
in str(rec[0].message))
with pytest.warns(UserWarning) as rec:
plt.gcf().show()
assert len(rec) == 1
assert ('FigureCanvasPdf is non-interactive, and thus cannot be shown'
in str(rec[0].message))
def test_grab_clear():
fig, ax = plt.subplots()
fig.canvas.grab_mouse(ax)
assert fig.canvas.mouse_grabber == ax
fig.clear()
assert fig.canvas.mouse_grabber is None
@pytest.mark.parametrize(
"x, y", [(42, 24), (None, 42), (None, None), (200, 100.01), (205.75, 2.0)])
def test_location_event_position(x, y):
# LocationEvent should cast its x and y arguments to int unless it is None.
fig, ax = plt.subplots()
canvas = FigureCanvasBase(fig)
event = LocationEvent("test_event", canvas, x, y)
if x is None:
assert event.x is None
else:
assert event.x == int(x)
assert isinstance(event.x, int)
if y is None:
assert event.y is None
else:
assert event.y == int(y)
assert isinstance(event.y, int)
if x is not None and y is not None:
assert (ax.format_coord(x, y)
== f"(x, y) = ({ax.format_xdata(x)}, {ax.format_ydata(y)})")
ax.fmt_xdata = ax.fmt_ydata = lambda x: "foo"
assert ax.format_coord(x, y) == "(x, y) = (foo, foo)"
def test_location_event_position_twin():
fig, ax = plt.subplots()
ax.set(xlim=(0, 10), ylim=(0, 20))
assert ax.format_coord(5., 5.) == "(x, y) = (5.00, 5.00)"
ax.twinx().set(ylim=(0, 40))
assert ax.format_coord(5., 5.) == "(x, y) = (5.00, 5.00) | (5.00, 10.0)"
ax.twiny().set(xlim=(0, 5))
assert (ax.format_coord(5., 5.)
== "(x, y) = (5.00, 5.00) | (5.00, 10.0) | (2.50, 5.00)")
def test_pick():
fig = plt.figure()
fig.text(.5, .5, "hello", ha="center", va="center", picker=True)
fig.canvas.draw()
picks = []
def handle_pick(event):
assert event.mouseevent.key == "a"
picks.append(event)
fig.canvas.mpl_connect("pick_event", handle_pick)
KeyEvent("key_press_event", fig.canvas, "a")._process()
MouseEvent("button_press_event", fig.canvas,
*fig.transFigure.transform((.5, .5)),
MouseButton.LEFT)._process()
KeyEvent("key_release_event", fig.canvas, "a")._process()
assert len(picks) == 1
def test_interactive_zoom():
fig, ax = plt.subplots()
ax.set(xscale="logit")
assert ax.get_navigate_mode() is None
tb = NavigationToolbar2(fig.canvas)
tb.zoom()
assert ax.get_navigate_mode() == 'ZOOM'
xlim0 = ax.get_xlim()
ylim0 = ax.get_ylim()
# Zoom from x=1e-6, y=0.1 to x=1-1e-5, 0.8 (data coordinates, "d").
d0 = (1e-6, 0.1)
d1 = (1-1e-5, 0.8)
# Convert to screen coordinates ("s"). Events are defined only with pixel
# precision, so round the pixel values, and below, check against the
# corresponding xdata/ydata, which are close but not equal to d0/d1.
s0 = ax.transData.transform(d0).astype(int)
s1 = ax.transData.transform(d1).astype(int)
# Zoom in.
start_event = MouseEvent(
"button_press_event", fig.canvas, *s0, MouseButton.LEFT)
fig.canvas.callbacks.process(start_event.name, start_event)
stop_event = MouseEvent(
"button_release_event", fig.canvas, *s1, MouseButton.LEFT)
fig.canvas.callbacks.process(stop_event.name, stop_event)
assert ax.get_xlim() == (start_event.xdata, stop_event.xdata)
assert ax.get_ylim() == (start_event.ydata, stop_event.ydata)
# Zoom out.
start_event = MouseEvent(
"button_press_event", fig.canvas, *s1, MouseButton.RIGHT)
fig.canvas.callbacks.process(start_event.name, start_event)
stop_event = MouseEvent(
"button_release_event", fig.canvas, *s0, MouseButton.RIGHT)
fig.canvas.callbacks.process(stop_event.name, stop_event)
# Absolute tolerance much less than original xmin (1e-7).
assert ax.get_xlim() == pytest.approx(xlim0, rel=0, abs=1e-10)
assert ax.get_ylim() == pytest.approx(ylim0, rel=0, abs=1e-10)
tb.zoom()
assert ax.get_navigate_mode() is None
assert not ax.get_autoscalex_on() and not ax.get_autoscaley_on()
def test_widgetlock_zoompan():
fig, ax = plt.subplots()
ax.plot([0, 1], [0, 1])
fig.canvas.widgetlock(ax)
tb = NavigationToolbar2(fig.canvas)
tb.zoom()
assert ax.get_navigate_mode() is None
tb.pan()
assert ax.get_navigate_mode() is None
@pytest.mark.parametrize("plot_func", ["imshow", "contourf"])
@pytest.mark.parametrize("orientation", ["vertical", "horizontal"])
@pytest.mark.parametrize("tool,button,expected",
[("zoom", MouseButton.LEFT, (4, 6)), # zoom in
("zoom", MouseButton.RIGHT, (-20, 30)), # zoom out
("pan", MouseButton.LEFT, (-2, 8)),
("pan", MouseButton.RIGHT, (1.47, 7.78))]) # zoom
def test_interactive_colorbar(plot_func, orientation, tool, button, expected):
fig, ax = plt.subplots()
data = np.arange(12).reshape((4, 3))
vmin0, vmax0 = 0, 10
coll = getattr(ax, plot_func)(data, vmin=vmin0, vmax=vmax0)
cb = fig.colorbar(coll, ax=ax, orientation=orientation)
if plot_func == "contourf":
# Just determine we can't navigate and exit out of the test
assert not cb.ax.get_navigate()
return
assert cb.ax.get_navigate()
# Mouse from 4 to 6 (data coordinates, "d").
vmin, vmax = 4, 6
# The y coordinate doesn't matter, it just needs to be between 0 and 1
# However, we will set d0/d1 to the same y coordinate to test that small
# pixel changes in that coordinate doesn't cancel the zoom like a normal
# axes would.
d0 = (vmin, 0.5)
d1 = (vmax, 0.5)
# Swap them if the orientation is vertical
if orientation == "vertical":
d0 = d0[::-1]
d1 = d1[::-1]
# Convert to screen coordinates ("s"). Events are defined only with pixel
# precision, so round the pixel values, and below, check against the
# corresponding xdata/ydata, which are close but not equal to d0/d1.
s0 = cb.ax.transData.transform(d0).astype(int)
s1 = cb.ax.transData.transform(d1).astype(int)
# Set up the mouse movements
start_event = MouseEvent(
"button_press_event", fig.canvas, *s0, button)
stop_event = MouseEvent(
"button_release_event", fig.canvas, *s1, button)
tb = NavigationToolbar2(fig.canvas)
if tool == "zoom":
tb.zoom()
tb.press_zoom(start_event)
tb.drag_zoom(stop_event)
tb.release_zoom(stop_event)
else:
tb.pan()
tb.press_pan(start_event)
tb.drag_pan(stop_event)
tb.release_pan(stop_event)
# Should be close, but won't be exact due to screen integer resolution
assert (cb.vmin, cb.vmax) == pytest.approx(expected, abs=0.15)
def test_toolbar_zoompan():
with pytest.warns(UserWarning, match=_EXPECTED_WARNING_TOOLMANAGER):
plt.rcParams['toolbar'] = 'toolmanager'
ax = plt.gca()
fig = ax.get_figure()
assert ax.get_navigate_mode() is None
fig.canvas.manager.toolmanager.trigger_tool('zoom')
assert ax.get_navigate_mode() == "ZOOM"
fig.canvas.manager.toolmanager.trigger_tool('pan')
assert ax.get_navigate_mode() == "PAN"
def test_toolbar_home_restores_autoscale():
fig, ax = plt.subplots()
ax.plot(range(11), range(11))
tb = NavigationToolbar2(fig.canvas)
tb.zoom()
# Switch to log.
KeyEvent("key_press_event", fig.canvas, "k", 100, 100)._process()
KeyEvent("key_press_event", fig.canvas, "l", 100, 100)._process()
assert ax.get_xlim() == ax.get_ylim() == (1, 10) # Autolimits excluding 0.
# Switch back to linear.
KeyEvent("key_press_event", fig.canvas, "k", 100, 100)._process()
KeyEvent("key_press_event", fig.canvas, "l", 100, 100)._process()
assert ax.get_xlim() == ax.get_ylim() == (0, 10) # Autolimits.
# Zoom in from (x, y) = (2, 2) to (5, 5).
start, stop = ax.transData.transform([(2, 2), (5, 5)])
MouseEvent("button_press_event", fig.canvas, *start, MouseButton.LEFT)._process()
MouseEvent("button_release_event", fig.canvas, *stop, MouseButton.LEFT)._process()
# Go back to home.
KeyEvent("key_press_event", fig.canvas, "h")._process()
assert ax.get_xlim() == ax.get_ylim() == (0, 10)
# Switch to log.
KeyEvent("key_press_event", fig.canvas, "k", 100, 100)._process()
KeyEvent("key_press_event", fig.canvas, "l", 100, 100)._process()
assert ax.get_xlim() == ax.get_ylim() == (1, 10) # Autolimits excluding 0.
@pytest.mark.parametrize(
"backend", ['svg', 'ps', 'pdf',
pytest.param('pgf', marks=needs_pgf_xelatex)]
)
def test_draw(backend):
from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvas
test_backend = importlib.import_module(f'matplotlib.backends.backend_{backend}')
TestCanvas = test_backend.FigureCanvas
fig_test = Figure(constrained_layout=True)
TestCanvas(fig_test)
axes_test = fig_test.subplots(2, 2)
# defaults to FigureCanvasBase
fig_agg = Figure(constrained_layout=True)
# put a backends.backend_agg.FigureCanvas on it
FigureCanvas(fig_agg)
axes_agg = fig_agg.subplots(2, 2)
init_pos = [ax.get_position() for ax in axes_test.ravel()]
fig_test.canvas.draw()
fig_agg.canvas.draw()
layed_out_pos_test = [ax.get_position() for ax in axes_test.ravel()]
layed_out_pos_agg = [ax.get_position() for ax in axes_agg.ravel()]
for init, placed in zip(init_pos, layed_out_pos_test):
assert not np.allclose(init, placed, atol=0.005)
for ref, test in zip(layed_out_pos_agg, layed_out_pos_test):
np.testing.assert_allclose(ref, test, atol=0.005)
@pytest.mark.parametrize(
"key,mouseend,expectedxlim,expectedylim",
[(None, (0.2, 0.2), (3.49, 12.49), (2.7, 11.7)),
(None, (0.2, 0.5), (3.49, 12.49), (0, 9)),
(None, (0.5, 0.2), (0, 9), (2.7, 11.7)),
(None, (0.5, 0.5), (0, 9), (0, 9)), # No move
(None, (0.8, 0.25), (-3.47, 5.53), (2.25, 11.25)),
(None, (0.2, 0.25), (3.49, 12.49), (2.25, 11.25)),
(None, (0.8, 0.85), (-3.47, 5.53), (-3.14, 5.86)),
(None, (0.2, 0.85), (3.49, 12.49), (-3.14, 5.86)),
("shift", (0.2, 0.4), (3.49, 12.49), (0, 9)), # snap to x
("shift", (0.4, 0.2), (0, 9), (2.7, 11.7)), # snap to y
("shift", (0.2, 0.25), (3.49, 12.49), (3.49, 12.49)), # snap to diagonal
("shift", (0.8, 0.25), (-3.47, 5.53), (3.47, 12.47)), # snap to diagonal
("shift", (0.8, 0.9), (-3.58, 5.41), (-3.58, 5.41)), # snap to diagonal
("shift", (0.2, 0.85), (3.49, 12.49), (-3.49, 5.51)), # snap to diagonal
("x", (0.2, 0.1), (3.49, 12.49), (0, 9)), # only x
("y", (0.1, 0.2), (0, 9), (2.7, 11.7)), # only y
("control", (0.2, 0.2), (3.49, 12.49), (3.49, 12.49)), # diagonal
("control", (0.4, 0.2), (2.72, 11.72), (2.72, 11.72)), # diagonal
])
def test_interactive_pan(key, mouseend, expectedxlim, expectedylim):
fig, ax = plt.subplots()
ax.plot(np.arange(10))
assert ax.get_navigate()
# Set equal aspect ratio to easier see diagonal snap
ax.set_aspect('equal')
# Mouse move starts from 0.5, 0.5
mousestart = (0.5, 0.5)
# Convert to screen coordinates ("s"). Events are defined only with pixel
# precision, so round the pixel values, and below, check against the
# corresponding xdata/ydata, which are close but not equal to d0/d1.
sstart = ax.transData.transform(mousestart).astype(int)
send = ax.transData.transform(mouseend).astype(int)
# Set up the mouse movements
start_event = MouseEvent(
"button_press_event", fig.canvas, *sstart, button=MouseButton.LEFT,
key=key)
stop_event = MouseEvent(
"button_release_event", fig.canvas, *send, button=MouseButton.LEFT,
key=key)
tb = NavigationToolbar2(fig.canvas)
tb.pan()
tb.press_pan(start_event)
tb.drag_pan(stop_event)
tb.release_pan(stop_event)
# Should be close, but won't be exact due to screen integer resolution
assert tuple(ax.get_xlim()) == pytest.approx(expectedxlim, abs=0.02)
assert tuple(ax.get_ylim()) == pytest.approx(expectedylim, abs=0.02)
def test_toolmanager_remove():
with pytest.warns(UserWarning, match=_EXPECTED_WARNING_TOOLMANAGER):
plt.rcParams['toolbar'] = 'toolmanager'
fig = plt.gcf()
initial_len = len(fig.canvas.manager.toolmanager.tools)
assert 'forward' in fig.canvas.manager.toolmanager.tools
fig.canvas.manager.toolmanager.remove_tool('forward')
assert len(fig.canvas.manager.toolmanager.tools) == initial_len - 1
assert 'forward' not in fig.canvas.manager.toolmanager.tools
def test_toolmanager_get_tool():
with pytest.warns(UserWarning, match=_EXPECTED_WARNING_TOOLMANAGER):
plt.rcParams['toolbar'] = 'toolmanager'
fig = plt.gcf()
rubberband = fig.canvas.manager.toolmanager.get_tool('rubberband')
assert isinstance(rubberband, RubberbandBase)
assert fig.canvas.manager.toolmanager.get_tool(rubberband) is rubberband
with pytest.warns(UserWarning,
match="ToolManager does not control tool 'foo'"):
assert fig.canvas.manager.toolmanager.get_tool('foo') is None
assert fig.canvas.manager.toolmanager.get_tool('foo', warn=False) is None
with pytest.warns(UserWarning,
match="ToolManager does not control tool 'foo'"):
assert fig.canvas.manager.toolmanager.trigger_tool('foo') is None
def test_toolmanager_update_keymap():
with pytest.warns(UserWarning, match=_EXPECTED_WARNING_TOOLMANAGER):
plt.rcParams['toolbar'] = 'toolmanager'
fig = plt.gcf()
assert 'v' in fig.canvas.manager.toolmanager.get_tool_keymap('forward')
with pytest.warns(UserWarning,
match="Key c changed from back to forward"):
fig.canvas.manager.toolmanager.update_keymap('forward', 'c')
assert fig.canvas.manager.toolmanager.get_tool_keymap('forward') == ['c']
with pytest.raises(KeyError, match="'foo' not in Tools"):
fig.canvas.manager.toolmanager.update_keymap('foo', 'c')
@pytest.mark.parametrize("tool", ["zoom", "pan"])
@pytest.mark.parametrize("button", [MouseButton.LEFT, MouseButton.RIGHT])
@pytest.mark.parametrize("patch_vis", [True, False])
@pytest.mark.parametrize("forward_nav", [True, False, "auto"])
@pytest.mark.parametrize("t_s", ["twin", "share"])
def test_interactive_pan_zoom_events(tool, button, patch_vis, forward_nav, t_s):
# Bottom axes: ax_b Top axes: ax_t
fig, ax_b = plt.subplots()
ax_t = fig.add_subplot(221, zorder=99)
ax_t.set_forward_navigation_events(forward_nav)
ax_t.patch.set_visible(patch_vis)
# ----------------------------
if t_s == "share":
ax_t_twin = fig.add_subplot(222)
ax_t_twin.sharex(ax_t)
ax_t_twin.sharey(ax_t)
ax_b_twin = fig.add_subplot(223)
ax_b_twin.sharex(ax_b)
ax_b_twin.sharey(ax_b)
elif t_s == "twin":
ax_t_twin = ax_t.twinx()
ax_b_twin = ax_b.twinx()
# just some styling to simplify manual checks
ax_t.set_label("ax_t")
ax_t.patch.set_facecolor((1, 0, 0, 0.5))
ax_t_twin.set_label("ax_t_twin")
ax_t_twin.patch.set_facecolor("r")
ax_b.set_label("ax_b")
ax_b.patch.set_facecolor((0, 0, 1, 0.5))
ax_b_twin.set_label("ax_b_twin")
ax_b_twin.patch.set_facecolor("b")
# ----------------------------
# Set initial axis limits
init_xlim, init_ylim = (0, 10), (0, 10)
for ax in [ax_t, ax_b]:
ax.set_xlim(*init_xlim)
ax.set_ylim(*init_ylim)
# Mouse from 2 to 1 (in data-coordinates of ax_t).
xstart_t, xstop_t, ystart_t, ystop_t = 1, 2, 1, 2
# Convert to screen coordinates ("s"). Events are defined only with pixel
# precision, so round the pixel values, and below, check against the
# corresponding xdata/ydata, which are close but not equal to s0/s1.
s0 = ax_t.transData.transform((xstart_t, ystart_t)).astype(int)
s1 = ax_t.transData.transform((xstop_t, ystop_t)).astype(int)
# Calculate the mouse-distance in data-coordinates of the bottom-axes
xstart_b, ystart_b = ax_b.transData.inverted().transform(s0)
xstop_b, ystop_b = ax_b.transData.inverted().transform(s1)
# Set up the mouse movements
start_event = MouseEvent("button_press_event", fig.canvas, *s0, button)
stop_event = MouseEvent("button_release_event", fig.canvas, *s1, button)
tb = NavigationToolbar2(fig.canvas)
if tool == "zoom":
# Evaluate expected limits before executing the zoom-event
direction = ("in" if button == 1 else "out")
xlim_t, ylim_t = ax_t._prepare_view_from_bbox([*s0, *s1], direction)
if ax_t.get_forward_navigation_events() is True:
xlim_b, ylim_b = ax_b._prepare_view_from_bbox([*s0, *s1], direction)
elif ax_t.get_forward_navigation_events() is False:
xlim_b = init_xlim
ylim_b = init_ylim
else:
if not ax_t.patch.get_visible():
xlim_b, ylim_b = ax_b._prepare_view_from_bbox([*s0, *s1], direction)
else:
xlim_b = init_xlim
ylim_b = init_ylim
tb.zoom()
tb.press_zoom(start_event)
tb.drag_zoom(stop_event)
tb.release_zoom(stop_event)
assert ax_t.get_xlim() == pytest.approx(xlim_t, abs=0.15)
assert ax_t.get_ylim() == pytest.approx(ylim_t, abs=0.15)
assert ax_b.get_xlim() == pytest.approx(xlim_b, abs=0.15)
assert ax_b.get_ylim() == pytest.approx(ylim_b, abs=0.15)
# Check if twin-axes are properly triggered
assert ax_t.get_xlim() == pytest.approx(ax_t_twin.get_xlim(), abs=0.15)
assert ax_b.get_xlim() == pytest.approx(ax_b_twin.get_xlim(), abs=0.15)
else:
# Evaluate expected limits
# (call start_pan to make sure ax._pan_start is set)
ax_t.start_pan(*s0, button)
xlim_t, ylim_t = ax_t._get_pan_points(button, None, *s1).T.astype(float)
ax_t.end_pan()
if ax_t.get_forward_navigation_events() is True:
ax_b.start_pan(*s0, button)
xlim_b, ylim_b = ax_b._get_pan_points(button, None, *s1).T.astype(float)
ax_b.end_pan()
elif ax_t.get_forward_navigation_events() is False:
xlim_b = init_xlim
ylim_b = init_ylim
else:
if not ax_t.patch.get_visible():
ax_b.start_pan(*s0, button)
xlim_b, ylim_b = ax_b._get_pan_points(button, None, *s1).T.astype(float)
ax_b.end_pan()
else:
xlim_b = init_xlim
ylim_b = init_ylim
tb.pan()
tb.press_pan(start_event)
tb.drag_pan(stop_event)
tb.release_pan(stop_event)
assert ax_t.get_xlim() == pytest.approx(xlim_t, abs=0.15)
assert ax_t.get_ylim() == pytest.approx(ylim_t, abs=0.15)
assert ax_b.get_xlim() == pytest.approx(xlim_b, abs=0.15)
assert ax_b.get_ylim() == pytest.approx(ylim_b, abs=0.15)
# Check if twin-axes are properly triggered
assert ax_t.get_xlim() == pytest.approx(ax_t_twin.get_xlim(), abs=0.15)
assert ax_b.get_xlim() == pytest.approx(ax_b_twin.get_xlim(), abs=0.15)
@@ -0,0 +1,48 @@
import numpy as np
import pytest
from matplotlib.testing.decorators import check_figures_equal
from matplotlib import (
collections as mcollections, patches as mpatches, path as mpath)
@pytest.mark.backend('cairo')
@check_figures_equal(extensions=["png"])
def test_patch_alpha_coloring(fig_test, fig_ref):
"""
Test checks that the patch and collection are rendered with the specified
alpha values in their facecolor and edgecolor.
"""
star = mpath.Path.unit_regular_star(6)
circle = mpath.Path.unit_circle()
# concatenate the star with an internal cutout of the circle
verts = np.concatenate([circle.vertices, star.vertices[::-1]])
codes = np.concatenate([circle.codes, star.codes])
cut_star1 = mpath.Path(verts, codes)
cut_star2 = mpath.Path(verts + 1, codes)
# Reference: two separate patches
ax = fig_ref.subplots()
ax.set_xlim([-1, 2])
ax.set_ylim([-1, 2])
patch = mpatches.PathPatch(cut_star1,
linewidth=5, linestyle='dashdot',
facecolor=(1, 0, 0, 0.5),
edgecolor=(0, 0, 1, 0.75))
ax.add_patch(patch)
patch = mpatches.PathPatch(cut_star2,
linewidth=5, linestyle='dashdot',
facecolor=(1, 0, 0, 0.5),
edgecolor=(0, 0, 1, 0.75))
ax.add_patch(patch)
# Test: path collection
ax = fig_test.subplots()
ax.set_xlim([-1, 2])
ax.set_ylim([-1, 2])
col = mcollections.PathCollection([cut_star1, cut_star2],
linewidth=5, linestyles='dashdot',
facecolor=(1, 0, 0, 0.5),
edgecolor=(0, 0, 1, 0.75))
ax.add_collection(col)
@@ -0,0 +1,74 @@
import os
from matplotlib import pyplot as plt
import pytest
from unittest import mock
@pytest.mark.backend("gtk3agg", skip_on_importerror=True)
def test_correct_key():
pytest.xfail("test_widget_send_event is not triggering key_press_event")
from gi.repository import Gdk, Gtk # type: ignore[import]
fig = plt.figure()
buf = []
def send(event):
for key, mod in [
(Gdk.KEY_a, Gdk.ModifierType.SHIFT_MASK),
(Gdk.KEY_a, 0),
(Gdk.KEY_a, Gdk.ModifierType.CONTROL_MASK),
(Gdk.KEY_agrave, 0),
(Gdk.KEY_Control_L, Gdk.ModifierType.MOD1_MASK),
(Gdk.KEY_Alt_L, Gdk.ModifierType.CONTROL_MASK),
(Gdk.KEY_agrave,
Gdk.ModifierType.CONTROL_MASK
| Gdk.ModifierType.MOD1_MASK
| Gdk.ModifierType.MOD4_MASK),
(0xfd16, 0), # KEY_3270_Play.
(Gdk.KEY_BackSpace, 0),
(Gdk.KEY_BackSpace, Gdk.ModifierType.CONTROL_MASK),
]:
# This is not actually really the right API: it depends on the
# actual keymap (e.g. on Azerty, shift+agrave -> 0).
Gtk.test_widget_send_key(fig.canvas, key, mod)
def receive(event):
buf.append(event.key)
if buf == [
"A", "a", "ctrl+a",
"\N{LATIN SMALL LETTER A WITH GRAVE}",
"alt+control", "ctrl+alt",
"ctrl+alt+super+\N{LATIN SMALL LETTER A WITH GRAVE}",
# (No entry for KEY_3270_Play.)
"backspace", "ctrl+backspace",
]:
plt.close(fig)
fig.canvas.mpl_connect("draw_event", send)
fig.canvas.mpl_connect("key_press_event", receive)
plt.show()
@pytest.mark.backend("gtk3agg", skip_on_importerror=True)
def test_save_figure_return():
from gi.repository import Gtk
fig, ax = plt.subplots()
ax.imshow([[1]])
with mock.patch("gi.repository.Gtk.FileFilter") as fileFilter:
filt = fileFilter.return_value
filt.get_name.return_value = "Portable Network Graphics"
with mock.patch("gi.repository.Gtk.FileChooserDialog") as dialogChooser:
dialog = dialogChooser.return_value
dialog.get_filter.return_value = filt
dialog.get_filename.return_value = "foobar.png"
dialog.run.return_value = Gtk.ResponseType.OK
fname = fig.canvas.manager.toolbar.save_figure()
os.remove("foobar.png")
assert fname == "foobar.png"
with mock.patch("gi.repository.Gtk.MessageDialog"):
dialog.get_filename.return_value = None
dialog.run.return_value = Gtk.ResponseType.OK
fname = fig.canvas.manager.toolbar.save_figure()
assert fname is None
@@ -0,0 +1,46 @@
import os
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from matplotlib.testing import subprocess_run_for_testing
nbformat = pytest.importorskip('nbformat')
pytest.importorskip('nbconvert')
pytest.importorskip('ipykernel')
pytest.importorskip('matplotlib_inline')
def test_ipynb():
nb_path = Path(__file__).parent / 'test_inline_01.ipynb'
with TemporaryDirectory() as tmpdir:
out_path = Path(tmpdir, "out.ipynb")
subprocess_run_for_testing(
["jupyter", "nbconvert", "--to", "notebook",
"--execute", "--ExecutePreprocessor.timeout=500",
"--output", str(out_path), str(nb_path)],
env={**os.environ, "IPYTHONDIR": tmpdir},
check=True)
with out_path.open() as out:
nb = nbformat.read(out, nbformat.current_nbformat)
errors = [output for cell in nb.cells for output in cell.get("outputs", [])
if output.output_type == "error"]
assert not errors
import IPython
if IPython.version_info[:2] >= (8, 24):
expected_backend = "inline"
else:
# This code can be removed when Python 3.12, the latest version supported by
# IPython < 8.24, reaches end-of-life in late 2028.
expected_backend = "module://matplotlib_inline.backend_inline"
backend_outputs = nb.cells[2]["outputs"]
assert backend_outputs[0]["data"]["text/plain"] == f"'{expected_backend}'"
image = nb.cells[1]["outputs"][1]["data"]
assert image["text/plain"] == "<Figure size 300x200 with 1 Axes>"
assert "image/png" in image
@@ -0,0 +1,67 @@
import os
import pytest
from unittest import mock
import matplotlib as mpl
import matplotlib.pyplot as plt
try:
from matplotlib.backends import _macosx
except ImportError:
pytest.skip("These are mac only tests", allow_module_level=True)
@pytest.mark.backend('macosx')
def test_cached_renderer():
# Make sure that figures have an associated renderer after
# a fig.canvas.draw() call
fig = plt.figure(1)
fig.canvas.draw()
assert fig.canvas.get_renderer()._renderer is not None
fig = plt.figure(2)
fig.draw_without_rendering()
assert fig.canvas.get_renderer()._renderer is not None
@pytest.mark.backend('macosx')
def test_savefig_rcparam(monkeypatch, tmp_path):
def new_choose_save_file(title, directory, filename):
# Replacement function instead of opening a GUI window
# Make a new directory for testing the update of the rcParams
assert directory == str(tmp_path)
os.makedirs(f"{directory}/test")
return f"{directory}/test/{filename}"
monkeypatch.setattr(_macosx, "choose_save_file", new_choose_save_file)
fig = plt.figure()
with mpl.rc_context({"savefig.directory": tmp_path}):
fig.canvas.toolbar.save_figure()
# Check the saved location got created
save_file = f"{tmp_path}/test/{fig.canvas.get_default_filename()}"
assert os.path.exists(save_file)
# Check the savefig.directory rcParam got updated because
# we added a subdirectory "test"
assert mpl.rcParams["savefig.directory"] == f"{tmp_path}/test"
@pytest.mark.backend('macosx')
def test_ipython():
from matplotlib.testing import ipython_in_subprocess
ipython_in_subprocess("osx", {(8, 24): "macosx", (7, 0): "MacOSX"})
@pytest.mark.backend('macosx')
def test_save_figure_return():
fig, ax = plt.subplots()
ax.imshow([[1]])
prop = "matplotlib.backends._macosx.choose_save_file"
with mock.patch(prop, return_value="foobar.png"):
fname = fig.canvas.manager.toolbar.save_figure()
os.remove("foobar.png")
assert fname == "foobar.png"
with mock.patch(prop, return_value=None):
fname = fig.canvas.manager.toolbar.save_figure()
assert fname is None
@@ -0,0 +1,42 @@
import os
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from matplotlib.testing import subprocess_run_for_testing
nbformat = pytest.importorskip('nbformat')
pytest.importorskip('nbconvert')
pytest.importorskip('ipykernel')
# From https://blog.thedataincubator.com/2016/06/testing-jupyter-notebooks/
def test_ipynb():
nb_path = Path(__file__).parent / 'test_nbagg_01.ipynb'
with TemporaryDirectory() as tmpdir:
out_path = Path(tmpdir, "out.ipynb")
subprocess_run_for_testing(
["jupyter", "nbconvert", "--to", "notebook",
"--execute", "--ExecutePreprocessor.timeout=500",
"--output", str(out_path), str(nb_path)],
env={**os.environ, "IPYTHONDIR": tmpdir},
check=True)
with out_path.open() as out:
nb = nbformat.read(out, nbformat.current_nbformat)
errors = [output for cell in nb.cells for output in cell.get("outputs", [])
if output.output_type == "error"]
assert not errors
import IPython
if IPython.version_info[:2] >= (8, 24):
expected_backend = "notebook"
else:
# This code can be removed when Python 3.12, the latest version supported by
# IPython < 8.24, reaches end-of-life in late 2028.
expected_backend = "nbAgg"
backend_outputs = nb.cells[2]["outputs"]
assert backend_outputs[0]["data"]["text/plain"] == f"'{expected_backend}'"
@@ -0,0 +1,448 @@
import datetime
import decimal
import io
import os
from pathlib import Path
import numpy as np
import pytest
import matplotlib as mpl
from matplotlib import (
pyplot as plt, rcParams, font_manager as fm
)
from matplotlib.cbook import _get_data_path
from matplotlib.ft2font import FT2Font
from matplotlib.font_manager import findfont, FontProperties
from matplotlib.backends._backend_pdf_ps import get_glyphs_subset, font_as_file
from matplotlib.backends.backend_pdf import PdfPages
from matplotlib.patches import Rectangle
from matplotlib.testing.decorators import check_figures_equal, image_comparison
from matplotlib.testing._markers import needs_usetex
@image_comparison(['pdf_use14corefonts.pdf'])
def test_use14corefonts():
rcParams['pdf.use14corefonts'] = True
rcParams['font.family'] = 'sans-serif'
rcParams['font.size'] = 8
rcParams['font.sans-serif'] = ['Helvetica']
rcParams['pdf.compression'] = 0
text = '''A three-line text positioned just above a blue line
and containing some French characters and the euro symbol:
"Merci pépé pour les 10 €"'''
fig, ax = plt.subplots()
ax.set_title('Test PDF backend with option use14corefonts=True')
ax.text(0.5, 0.5, text, horizontalalignment='center',
verticalalignment='bottom',
fontsize=14)
ax.axhline(0.5, linewidth=0.5)
@pytest.mark.parametrize('fontname, fontfile', [
('DejaVu Sans', 'DejaVuSans.ttf'),
('WenQuanYi Zen Hei', 'wqy-zenhei.ttc'),
])
@pytest.mark.parametrize('fonttype', [3, 42])
def test_embed_fonts(fontname, fontfile, fonttype):
if Path(findfont(FontProperties(family=[fontname]))).name != fontfile:
pytest.skip(f'Font {fontname!r} may be missing')
rcParams['pdf.fonttype'] = fonttype
fig, ax = plt.subplots()
ax.plot([1, 2, 3])
ax.set_title('Axes Title', font=fontname)
fig.savefig(io.BytesIO(), format='pdf')
def test_multipage_pagecount():
with PdfPages(io.BytesIO()) as pdf:
assert pdf.get_pagecount() == 0
fig, ax = plt.subplots()
ax.plot([1, 2, 3])
fig.savefig(pdf, format="pdf")
assert pdf.get_pagecount() == 1
pdf.savefig()
assert pdf.get_pagecount() == 2
def test_multipage_properfinalize():
pdfio = io.BytesIO()
with PdfPages(pdfio) as pdf:
for i in range(10):
fig, ax = plt.subplots()
ax.set_title('This is a long title')
fig.savefig(pdf, format="pdf")
s = pdfio.getvalue()
assert s.count(b'startxref') == 1
assert len(s) < 40000
def test_multipage_keep_empty(tmp_path):
# An empty pdf deletes itself afterwards.
fn = tmp_path / "a.pdf"
with PdfPages(fn) as pdf:
pass
assert not fn.exists()
# Test pdf files with content, they should never be deleted.
fn = tmp_path / "b.pdf"
with PdfPages(fn) as pdf:
pdf.savefig(plt.figure())
assert fn.exists()
def test_composite_image():
# Test that figures can be saved with and without combining multiple images
# (on a single set of axes) into a single composite image.
X, Y = np.meshgrid(np.arange(-5, 5, 1), np.arange(-5, 5, 1))
Z = np.sin(Y ** 2)
fig, ax = plt.subplots()
ax.set_xlim(0, 3)
ax.imshow(Z, extent=[0, 1, 0, 1])
ax.imshow(Z[::-1], extent=[2, 3, 0, 1])
plt.rcParams['image.composite_image'] = True
with PdfPages(io.BytesIO()) as pdf:
fig.savefig(pdf, format="pdf")
assert len(pdf._file._images) == 1
plt.rcParams['image.composite_image'] = False
with PdfPages(io.BytesIO()) as pdf:
fig.savefig(pdf, format="pdf")
assert len(pdf._file._images) == 2
def test_indexed_image():
# An image with low color count should compress to a palette-indexed format.
pikepdf = pytest.importorskip('pikepdf')
data = np.zeros((256, 1, 3), dtype=np.uint8)
data[:, 0, 0] = np.arange(256) # Maximum unique colours for an indexed image.
rcParams['pdf.compression'] = True
fig = plt.figure()
fig.figimage(data, resize=True)
buf = io.BytesIO()
fig.savefig(buf, format='pdf', dpi='figure')
with pikepdf.Pdf.open(buf) as pdf:
page, = pdf.pages
image, = page.images.values()
pdf_image = pikepdf.PdfImage(image)
assert pdf_image.indexed
pil_image = pdf_image.as_pil_image()
rgb = np.asarray(pil_image.convert('RGB'))
np.testing.assert_array_equal(data, rgb)
def test_savefig_metadata(monkeypatch):
pikepdf = pytest.importorskip('pikepdf')
monkeypatch.setenv('SOURCE_DATE_EPOCH', '0')
fig, ax = plt.subplots()
ax.plot(range(5))
md = {
'Author': 'me',
'Title': 'Multipage PDF',
'Subject': 'Test page',
'Keywords': 'test,pdf,multipage',
'ModDate': datetime.datetime(
1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))),
'Trapped': 'True'
}
buf = io.BytesIO()
fig.savefig(buf, metadata=md, format='pdf')
with pikepdf.Pdf.open(buf) as pdf:
info = {k: str(v) for k, v in pdf.docinfo.items()}
assert info == {
'/Author': 'me',
'/CreationDate': 'D:19700101000000Z',
'/Creator': f'Matplotlib v{mpl.__version__}, https://matplotlib.org',
'/Keywords': 'test,pdf,multipage',
'/ModDate': 'D:19680801000000Z',
'/Producer': f'Matplotlib pdf backend v{mpl.__version__}',
'/Subject': 'Test page',
'/Title': 'Multipage PDF',
'/Trapped': '/True',
}
def test_invalid_metadata():
fig, ax = plt.subplots()
with pytest.warns(UserWarning,
match="Unknown infodict keyword: 'foobar'."):
fig.savefig(io.BytesIO(), format='pdf', metadata={'foobar': 'invalid'})
with pytest.warns(UserWarning,
match='not an instance of datetime.datetime.'):
fig.savefig(io.BytesIO(), format='pdf',
metadata={'ModDate': '1968-08-01'})
with pytest.warns(UserWarning,
match='not one of {"True", "False", "Unknown"}'):
fig.savefig(io.BytesIO(), format='pdf', metadata={'Trapped': 'foo'})
with pytest.warns(UserWarning, match='not an instance of str.'):
fig.savefig(io.BytesIO(), format='pdf', metadata={'Title': 1234})
def test_multipage_metadata(monkeypatch):
pikepdf = pytest.importorskip('pikepdf')
monkeypatch.setenv('SOURCE_DATE_EPOCH', '0')
fig, ax = plt.subplots()
ax.plot(range(5))
md = {
'Author': 'me',
'Title': 'Multipage PDF',
'Subject': 'Test page',
'Keywords': 'test,pdf,multipage',
'ModDate': datetime.datetime(
1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))),
'Trapped': 'True'
}
buf = io.BytesIO()
with PdfPages(buf, metadata=md) as pdf:
pdf.savefig(fig)
pdf.savefig(fig)
with pikepdf.Pdf.open(buf) as pdf:
info = {k: str(v) for k, v in pdf.docinfo.items()}
assert info == {
'/Author': 'me',
'/CreationDate': 'D:19700101000000Z',
'/Creator': f'Matplotlib v{mpl.__version__}, https://matplotlib.org',
'/Keywords': 'test,pdf,multipage',
'/ModDate': 'D:19680801000000Z',
'/Producer': f'Matplotlib pdf backend v{mpl.__version__}',
'/Subject': 'Test page',
'/Title': 'Multipage PDF',
'/Trapped': '/True',
}
def test_text_urls():
pikepdf = pytest.importorskip('pikepdf')
test_url = 'https://test_text_urls.matplotlib.org/'
fig = plt.figure(figsize=(2, 1))
fig.text(0.1, 0.1, 'test plain 123', url=f'{test_url}plain')
fig.text(0.1, 0.4, 'test mathtext $123$', url=f'{test_url}mathtext')
with io.BytesIO() as fd:
fig.savefig(fd, format='pdf')
with pikepdf.Pdf.open(fd) as pdf:
annots = pdf.pages[0].Annots
# Iteration over Annots must occur within the context manager,
# otherwise it may fail depending on the pdf structure.
for y, fragment in [('0.1', 'plain'), ('0.4', 'mathtext')]:
annot = next(
(a for a in annots if a.A.URI == f'{test_url}{fragment}'),
None)
assert annot is not None
assert getattr(annot, 'QuadPoints', None) is None
# Positions in points (72 per inch.)
assert annot.Rect[1] == decimal.Decimal(y) * 72
def test_text_rotated_urls():
pikepdf = pytest.importorskip('pikepdf')
test_url = 'https://test_text_urls.matplotlib.org/'
fig = plt.figure(figsize=(1, 1))
fig.text(0.1, 0.1, 'N', rotation=45, url=f'{test_url}')
with io.BytesIO() as fd:
fig.savefig(fd, format='pdf')
with pikepdf.Pdf.open(fd) as pdf:
annots = pdf.pages[0].Annots
# Iteration over Annots must occur within the context manager,
# otherwise it may fail depending on the pdf structure.
annot = next(
(a for a in annots if a.A.URI == f'{test_url}'),
None)
assert annot is not None
assert getattr(annot, 'QuadPoints', None) is not None
# Positions in points (72 per inch)
assert annot.Rect[0] == \
annot.QuadPoints[6] - decimal.Decimal('0.00001')
@needs_usetex
def test_text_urls_tex():
pikepdf = pytest.importorskip('pikepdf')
test_url = 'https://test_text_urls.matplotlib.org/'
fig = plt.figure(figsize=(2, 1))
fig.text(0.1, 0.7, 'test tex $123$', usetex=True, url=f'{test_url}tex')
with io.BytesIO() as fd:
fig.savefig(fd, format='pdf')
with pikepdf.Pdf.open(fd) as pdf:
annots = pdf.pages[0].Annots
# Iteration over Annots must occur within the context manager,
# otherwise it may fail depending on the pdf structure.
annot = next(
(a for a in annots if a.A.URI == f'{test_url}tex'),
None)
assert annot is not None
# Positions in points (72 per inch.)
assert annot.Rect[1] == decimal.Decimal('0.7') * 72
def test_pdfpages_fspath():
with PdfPages(Path(os.devnull)) as pdf:
pdf.savefig(plt.figure())
@image_comparison(['hatching_legend.pdf'])
def test_hatching_legend():
"""Test for correct hatching on patches in legend"""
fig = plt.figure(figsize=(1, 2))
a = Rectangle([0, 0], 0, 0, facecolor="green", hatch="XXXX")
b = Rectangle([0, 0], 0, 0, facecolor="blue", hatch="XXXX")
fig.legend([a, b, a, b], ["", "", "", ""])
@image_comparison(['grayscale_alpha.pdf'])
def test_grayscale_alpha():
"""Masking images with NaN did not work for grayscale images"""
x, y = np.ogrid[-2:2:.1, -2:2:.1]
dd = np.exp(-(x**2 + y**2))
dd[dd < .1] = np.nan
fig, ax = plt.subplots()
ax.imshow(dd, interpolation='none', cmap='gray_r')
ax.set_xticks([])
ax.set_yticks([])
@mpl.style.context('default')
@check_figures_equal(extensions=["pdf", "eps"])
def test_pdf_eps_savefig_when_color_is_none(fig_test, fig_ref):
ax_test = fig_test.add_subplot()
ax_test.set_axis_off()
ax_test.plot(np.sin(np.linspace(-5, 5, 100)), "v", c="none")
ax_ref = fig_ref.add_subplot()
ax_ref.set_axis_off()
@needs_usetex
def test_failing_latex():
"""Test failing latex subprocess call"""
plt.xlabel("$22_2_2$", usetex=True) # This fails with "Double subscript"
with pytest.raises(RuntimeError):
plt.savefig(io.BytesIO(), format="pdf")
def test_empty_rasterized():
# Check that empty figures that are rasterised save to pdf files fine
fig, ax = plt.subplots()
ax.plot([], [], rasterized=True)
fig.savefig(io.BytesIO(), format="pdf")
@image_comparison(['kerning.pdf'])
def test_kerning():
fig = plt.figure()
s = "AVAVAVAVAVAVAVAV€AAVV"
fig.text(0, .25, s, size=5)
fig.text(0, .75, s, size=20)
def test_glyphs_subset():
fpath = str(_get_data_path("fonts/ttf/DejaVuSerif.ttf"))
chars = "these should be subsetted! 1234567890"
# non-subsetted FT2Font
nosubfont = FT2Font(fpath)
nosubfont.set_text(chars)
# subsetted FT2Font
with get_glyphs_subset(fpath, chars) as subset:
subfont = FT2Font(font_as_file(subset))
subfont.set_text(chars)
nosubcmap = nosubfont.get_charmap()
subcmap = subfont.get_charmap()
# all unique chars must be available in subsetted font
assert {*chars} == {chr(key) for key in subcmap}
# subsetted font's charmap should have less entries
assert len(subcmap) < len(nosubcmap)
# since both objects are assigned same characters
assert subfont.get_num_glyphs() == nosubfont.get_num_glyphs()
@image_comparison(["multi_font_type3.pdf"], tol=4.6)
def test_multi_font_type3():
fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
pytest.skip("Font may be missing")
plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
plt.rc('pdf', fonttype=3)
fig = plt.figure()
fig.text(0.15, 0.475, "There are 几个汉字 in between!")
@image_comparison(["multi_font_type42.pdf"], tol=2.2)
def test_multi_font_type42():
fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
pytest.skip("Font may be missing")
plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
plt.rc('pdf', fonttype=42)
fig = plt.figure()
fig.text(0.15, 0.475, "There are 几个汉字 in between!")
@pytest.mark.parametrize('family_name, file_name',
[("Noto Sans", "NotoSans-Regular.otf"),
("FreeMono", "FreeMono.otf")])
def test_otf_font_smoke(family_name, file_name):
# checks that there's no segfault
fp = fm.FontProperties(family=[family_name])
if Path(fm.findfont(fp)).name != file_name:
pytest.skip(f"Font {family_name} may be missing")
plt.rc('font', family=[family_name], size=27)
fig = plt.figure()
fig.text(0.15, 0.475, "Привет мир!")
fig.savefig(io.BytesIO(), format="pdf")
@image_comparison(["truetype-conversion.pdf"])
# mpltest.ttf does not have "l"/"p" glyphs so we get a warning when trying to
# get the font extents.
def test_truetype_conversion(recwarn):
mpl.rcParams['pdf.fonttype'] = 3
fig, ax = plt.subplots()
ax.text(0, 0, "ABCDE",
font=Path(__file__).with_name("mpltest.ttf"), fontsize=80)
ax.set_xticks([])
ax.set_yticks([])
@@ -0,0 +1,402 @@
import datetime
from io import BytesIO
import os
import shutil
import numpy as np
from packaging.version import parse as parse_version
import pytest
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.testing import _has_tex_package, _check_for_pgf
from matplotlib.testing.exceptions import ImageComparisonFailure
from matplotlib.testing.compare import compare_images
from matplotlib.backends.backend_pgf import PdfPages
from matplotlib.testing.decorators import (
_image_directories, check_figures_equal, image_comparison)
from matplotlib.testing._markers import (
needs_ghostscript, needs_pgf_lualatex, needs_pgf_pdflatex,
needs_pgf_xelatex)
baseline_dir, result_dir = _image_directories(lambda: 'dummy func')
def compare_figure(fname, savefig_kwargs={}, tol=0):
actual = os.path.join(result_dir, fname)
plt.savefig(actual, **savefig_kwargs)
expected = os.path.join(result_dir, "expected_%s" % fname)
shutil.copyfile(os.path.join(baseline_dir, fname), expected)
err = compare_images(expected, actual, tol=tol)
if err:
raise ImageComparisonFailure(err)
@needs_pgf_xelatex
@needs_ghostscript
@pytest.mark.backend('pgf')
def test_tex_special_chars(tmp_path):
fig = plt.figure()
fig.text(.5, .5, "%_^ $a_b^c$")
buf = BytesIO()
fig.savefig(buf, format="png", backend="pgf")
buf.seek(0)
t = plt.imread(buf)
assert not (t == 1).all() # The leading "%" didn't eat up everything.
def create_figure():
plt.figure()
x = np.linspace(0, 1, 15)
# line plot
plt.plot(x, x ** 2, "b-")
# marker
plt.plot(x, 1 - x**2, "g>")
# filled paths and patterns
plt.fill_between([0., .4], [.4, 0.], hatch='//', facecolor="lightgray",
edgecolor="red")
plt.fill([3, 3, .8, .8, 3], [2, -2, -2, 0, 2], "b")
# text and typesetting
plt.plot([0.9], [0.5], "ro", markersize=3)
plt.text(0.9, 0.5, 'unicode (ü, °, \N{Section Sign}) and math ($\\mu_i = x_i^2$)',
ha='right', fontsize=20)
plt.ylabel('sans-serif, blue, $\\frac{\\sqrt{x}}{y^2}$..',
family='sans-serif', color='blue')
plt.text(1, 1, 'should be clipped as default clip_box is Axes bbox',
fontsize=20, clip_on=True)
plt.xlim(0, 1)
plt.ylim(0, 1)
# test compiling a figure to pdf with xelatex
@needs_pgf_xelatex
@pytest.mark.backend('pgf')
@image_comparison(['pgf_xelatex.pdf'], style='default')
def test_xelatex():
rc_xelatex = {'font.family': 'serif',
'pgf.rcfonts': False}
mpl.rcParams.update(rc_xelatex)
create_figure()
try:
_old_gs_version = \
mpl._get_executable_info('gs').version < parse_version('9.50')
except mpl.ExecutableNotFoundError:
_old_gs_version = True
# test compiling a figure to pdf with pdflatex
@needs_pgf_pdflatex
@pytest.mark.skipif(not _has_tex_package('type1ec'), reason='needs type1ec.sty')
@pytest.mark.skipif(not _has_tex_package('ucs'), reason='needs ucs.sty')
@pytest.mark.backend('pgf')
@image_comparison(['pgf_pdflatex.pdf'], style='default',
tol=11.71 if _old_gs_version else 0)
def test_pdflatex():
rc_pdflatex = {'font.family': 'serif',
'pgf.rcfonts': False,
'pgf.texsystem': 'pdflatex',
'pgf.preamble': ('\\usepackage[utf8x]{inputenc}'
'\\usepackage[T1]{fontenc}')}
mpl.rcParams.update(rc_pdflatex)
create_figure()
# test updating the rc parameters for each figure
@needs_pgf_xelatex
@needs_pgf_pdflatex
@mpl.style.context('default')
@pytest.mark.backend('pgf')
def test_rcupdate():
rc_sets = [{'font.family': 'sans-serif',
'font.size': 30,
'figure.subplot.left': .2,
'lines.markersize': 10,
'pgf.rcfonts': False,
'pgf.texsystem': 'xelatex'},
{'font.family': 'monospace',
'font.size': 10,
'figure.subplot.left': .1,
'lines.markersize': 20,
'pgf.rcfonts': False,
'pgf.texsystem': 'pdflatex',
'pgf.preamble': ('\\usepackage[utf8x]{inputenc}'
'\\usepackage[T1]{fontenc}'
'\\usepackage{sfmath}')}]
tol = [0, 13.2] if _old_gs_version else [0, 0]
for i, rc_set in enumerate(rc_sets):
with mpl.rc_context(rc_set):
for substring, pkg in [('sfmath', 'sfmath'), ('utf8x', 'ucs')]:
if (substring in mpl.rcParams['pgf.preamble']
and not _has_tex_package(pkg)):
pytest.skip(f'needs {pkg}.sty')
create_figure()
compare_figure(f'pgf_rcupdate{i + 1}.pdf', tol=tol[i])
# test backend-side clipping, since large numbers are not supported by TeX
@needs_pgf_xelatex
@mpl.style.context('default')
@pytest.mark.backend('pgf')
def test_pathclip():
np.random.seed(19680801)
mpl.rcParams.update({'font.family': 'serif', 'pgf.rcfonts': False})
fig, axs = plt.subplots(1, 2)
axs[0].plot([0., 1e100], [0., 1e100])
axs[0].set_xlim(0, 1)
axs[0].set_ylim(0, 1)
axs[1].scatter([0, 1], [1, 1])
axs[1].hist(np.random.normal(size=1000), bins=20, range=[-10, 10])
axs[1].set_xscale('log')
fig.savefig(BytesIO(), format="pdf") # No image comparison.
# test mixed mode rendering
@needs_pgf_xelatex
@pytest.mark.backend('pgf')
@image_comparison(['pgf_mixedmode.pdf'], style='default')
def test_mixedmode():
mpl.rcParams.update({'font.family': 'serif', 'pgf.rcfonts': False})
Y, X = np.ogrid[-1:1:40j, -1:1:40j]
plt.pcolor(X**2 + Y**2).set_rasterized(True)
# test bbox_inches clipping
@needs_pgf_xelatex
@mpl.style.context('default')
@pytest.mark.backend('pgf')
def test_bbox_inches():
mpl.rcParams.update({'font.family': 'serif', 'pgf.rcfonts': False})
fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.plot(range(5))
ax2.plot(range(5))
plt.tight_layout()
bbox = ax1.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
compare_figure('pgf_bbox_inches.pdf', savefig_kwargs={'bbox_inches': bbox},
tol=0)
@mpl.style.context('default')
@pytest.mark.backend('pgf')
@pytest.mark.parametrize('system', [
pytest.param('lualatex', marks=[needs_pgf_lualatex]),
pytest.param('pdflatex', marks=[needs_pgf_pdflatex]),
pytest.param('xelatex', marks=[needs_pgf_xelatex]),
])
def test_pdf_pages(system):
rc_pdflatex = {
'font.family': 'serif',
'pgf.rcfonts': False,
'pgf.texsystem': system,
}
mpl.rcParams.update(rc_pdflatex)
fig1, ax1 = plt.subplots()
ax1.plot(range(5))
fig1.tight_layout()
fig2, ax2 = plt.subplots(figsize=(3, 2))
ax2.plot(range(5))
fig2.tight_layout()
path = os.path.join(result_dir, f'pdfpages_{system}.pdf')
md = {
'Author': 'me',
'Title': 'Multipage PDF with pgf',
'Subject': 'Test page',
'Keywords': 'test,pdf,multipage',
'ModDate': datetime.datetime(
1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))),
'Trapped': 'Unknown'
}
with PdfPages(path, metadata=md) as pdf:
pdf.savefig(fig1)
pdf.savefig(fig2)
pdf.savefig(fig1)
assert pdf.get_pagecount() == 3
@mpl.style.context('default')
@pytest.mark.backend('pgf')
@pytest.mark.parametrize('system', [
pytest.param('lualatex', marks=[needs_pgf_lualatex]),
pytest.param('pdflatex', marks=[needs_pgf_pdflatex]),
pytest.param('xelatex', marks=[needs_pgf_xelatex]),
])
def test_pdf_pages_metadata_check(monkeypatch, system):
# Basically the same as test_pdf_pages, but we keep it separate to leave
# pikepdf as an optional dependency.
pikepdf = pytest.importorskip('pikepdf')
monkeypatch.setenv('SOURCE_DATE_EPOCH', '0')
mpl.rcParams.update({'pgf.texsystem': system})
fig, ax = plt.subplots()
ax.plot(range(5))
md = {
'Author': 'me',
'Title': 'Multipage PDF with pgf',
'Subject': 'Test page',
'Keywords': 'test,pdf,multipage',
'ModDate': datetime.datetime(
1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))),
'Trapped': 'True'
}
path = os.path.join(result_dir, f'pdfpages_meta_check_{system}.pdf')
with PdfPages(path, metadata=md) as pdf:
pdf.savefig(fig)
with pikepdf.Pdf.open(path) as pdf:
info = {k: str(v) for k, v in pdf.docinfo.items()}
# Not set by us, so don't bother checking.
if '/PTEX.FullBanner' in info:
del info['/PTEX.FullBanner']
if '/PTEX.Fullbanner' in info:
del info['/PTEX.Fullbanner']
# Some LaTeX engines ignore this setting, and state themselves as producer.
producer = info.pop('/Producer')
assert producer == f'Matplotlib pgf backend v{mpl.__version__}' or (
system == 'lualatex' and 'LuaTeX' in producer)
assert info == {
'/Author': 'me',
'/CreationDate': 'D:19700101000000Z',
'/Creator': f'Matplotlib v{mpl.__version__}, https://matplotlib.org',
'/Keywords': 'test,pdf,multipage',
'/ModDate': 'D:19680801000000Z',
'/Subject': 'Test page',
'/Title': 'Multipage PDF with pgf',
'/Trapped': '/True',
}
@needs_pgf_xelatex
def test_multipage_keep_empty(tmp_path):
# An empty pdf deletes itself afterwards.
fn = tmp_path / "a.pdf"
with PdfPages(fn) as pdf:
pass
assert not fn.exists()
# Test pdf files with content, they should never be deleted.
fn = tmp_path / "b.pdf"
with PdfPages(fn) as pdf:
pdf.savefig(plt.figure())
assert fn.exists()
@needs_pgf_xelatex
def test_tex_restart_after_error():
fig = plt.figure()
fig.suptitle(r"\oops")
with pytest.raises(ValueError):
fig.savefig(BytesIO(), format="pgf")
fig = plt.figure() # start from scratch
fig.suptitle(r"this is ok")
fig.savefig(BytesIO(), format="pgf")
@needs_pgf_xelatex
def test_bbox_inches_tight():
fig, ax = plt.subplots()
ax.imshow([[0, 1], [2, 3]])
fig.savefig(BytesIO(), format="pdf", backend="pgf", bbox_inches="tight")
@needs_pgf_xelatex
@needs_ghostscript
def test_png_transparency(): # Actually, also just testing that png works.
buf = BytesIO()
plt.figure().savefig(buf, format="png", backend="pgf", transparent=True)
buf.seek(0)
t = plt.imread(buf)
assert (t[..., 3] == 0).all() # fully transparent.
@needs_pgf_xelatex
def test_unknown_font(caplog):
with caplog.at_level("WARNING"):
mpl.rcParams["font.family"] = "this-font-does-not-exist"
plt.figtext(.5, .5, "hello, world")
plt.savefig(BytesIO(), format="pgf")
assert "Ignoring unknown font: this-font-does-not-exist" in [
r.getMessage() for r in caplog.records]
@check_figures_equal(extensions=["pdf"])
@pytest.mark.parametrize("texsystem", ("pdflatex", "xelatex", "lualatex"))
@pytest.mark.backend("pgf")
def test_minus_signs_with_tex(fig_test, fig_ref, texsystem):
if not _check_for_pgf(texsystem):
pytest.skip(texsystem + ' + pgf is required')
mpl.rcParams["pgf.texsystem"] = texsystem
fig_test.text(.5, .5, "$-1$")
fig_ref.text(.5, .5, "$\N{MINUS SIGN}1$")
@pytest.mark.backend("pgf")
def test_sketch_params():
fig, ax = plt.subplots(figsize=(3, 3))
ax.set_xticks([])
ax.set_yticks([])
ax.set_frame_on(False)
handle, = ax.plot([0, 1])
handle.set_sketch_params(scale=5, length=30, randomness=42)
with BytesIO() as fd:
fig.savefig(fd, format='pgf')
buf = fd.getvalue().decode()
baseline = r"""\pgfpathmoveto{\pgfqpoint{0.375000in}{0.300000in}}%
\pgfpathlineto{\pgfqpoint{2.700000in}{2.700000in}}%
\usepgfmodule{decorations}%
\usepgflibrary{decorations.pathmorphing}%
\pgfkeys{/pgf/decoration/.cd, """ \
r"""segment length = 0.150000in, amplitude = 0.100000in}%
\pgfmathsetseed{42}%
\pgfdecoratecurrentpath{random steps}%
\pgfusepath{stroke}%"""
# \pgfdecoratecurrentpath must be after the path definition and before the
# path is used (\pgfusepath)
assert baseline in buf
# test to make sure that the document font size is set consistently (see #26892)
@needs_pgf_xelatex
@pytest.mark.skipif(
not _has_tex_package('unicode-math'), reason='needs unicode-math.sty'
)
@pytest.mark.backend('pgf')
@image_comparison(['pgf_document_font_size.pdf'], style='default', remove_text=True)
def test_document_font_size():
mpl.rcParams.update({
'pgf.texsystem': 'xelatex',
'pgf.rcfonts': False,
'pgf.preamble': r'\usepackage{unicode-math}',
})
plt.figure()
plt.plot([],
label=r'$this is a very very very long math label a \times b + 10^{-3}$ '
r'and some text'
)
plt.plot([],
label=r'\normalsize the document font size is \the\fontdimen6\font'
)
plt.legend()
@@ -0,0 +1,380 @@
from collections import Counter
from pathlib import Path
import io
import re
import tempfile
import numpy as np
import pytest
from matplotlib import cbook, path, patheffects, font_manager as fm
from matplotlib.figure import Figure
from matplotlib.patches import Ellipse
from matplotlib.testing._markers import needs_ghostscript, needs_usetex
from matplotlib.testing.decorators import check_figures_equal, image_comparison
import matplotlib as mpl
import matplotlib.collections as mcollections
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
# This tests tends to hit a TeX cache lock on AppVeyor.
@pytest.mark.flaky(reruns=3)
@pytest.mark.parametrize('papersize', ['letter', 'figure'])
@pytest.mark.parametrize('orientation', ['portrait', 'landscape'])
@pytest.mark.parametrize('format, use_log, rcParams', [
('ps', False, {}),
('ps', False, {'ps.usedistiller': 'ghostscript'}),
('ps', False, {'ps.usedistiller': 'xpdf'}),
('ps', False, {'text.usetex': True}),
('eps', False, {}),
('eps', True, {'ps.useafm': True}),
('eps', False, {'text.usetex': True}),
], ids=[
'ps',
'ps with distiller=ghostscript',
'ps with distiller=xpdf',
'ps with usetex',
'eps',
'eps afm',
'eps with usetex'
])
def test_savefig_to_stringio(format, use_log, rcParams, orientation, papersize):
mpl.rcParams.update(rcParams)
if mpl.rcParams["ps.usedistiller"] == "ghostscript":
try:
mpl._get_executable_info("gs")
except mpl.ExecutableNotFoundError as exc:
pytest.skip(str(exc))
elif mpl.rcParams["ps.usedistiller"] == "xpdf":
try:
mpl._get_executable_info("gs") # Effectively checks for ps2pdf.
mpl._get_executable_info("pdftops")
except mpl.ExecutableNotFoundError as exc:
pytest.skip(str(exc))
fig, ax = plt.subplots()
with io.StringIO() as s_buf, io.BytesIO() as b_buf:
if use_log:
ax.set_yscale('log')
ax.plot([1, 2], [1, 2])
title = "Déjà vu"
if not mpl.rcParams["text.usetex"]:
title += " \N{MINUS SIGN}\N{EURO SIGN}"
ax.set_title(title)
allowable_exceptions = []
if mpl.rcParams["text.usetex"]:
allowable_exceptions.append(RuntimeError)
if mpl.rcParams["ps.useafm"]:
allowable_exceptions.append(mpl.MatplotlibDeprecationWarning)
try:
fig.savefig(s_buf, format=format, orientation=orientation,
papertype=papersize)
fig.savefig(b_buf, format=format, orientation=orientation,
papertype=papersize)
except tuple(allowable_exceptions) as exc:
pytest.skip(str(exc))
assert not s_buf.closed
assert not b_buf.closed
s_val = s_buf.getvalue().encode('ascii')
b_val = b_buf.getvalue()
if format == 'ps':
# Default figsize = (8, 6) inches = (576, 432) points = (203.2, 152.4) mm.
# Landscape orientation will swap dimensions.
if mpl.rcParams["ps.usedistiller"] == "xpdf":
# Some versions specifically show letter/203x152, but not all,
# so we can only use this simpler test.
if papersize == 'figure':
assert b'letter' not in s_val.lower()
else:
assert b'letter' in s_val.lower()
elif mpl.rcParams["ps.usedistiller"] or mpl.rcParams["text.usetex"]:
width = b'432.0' if orientation == 'landscape' else b'576.0'
wanted = (b'-dDEVICEWIDTHPOINTS=' + width if papersize == 'figure'
else b'-sPAPERSIZE')
assert wanted in s_val
else:
if papersize == 'figure':
assert b'%%DocumentPaperSizes' not in s_val
else:
assert b'%%DocumentPaperSizes' in s_val
# Strip out CreationDate: ghostscript and cairo don't obey
# SOURCE_DATE_EPOCH, and that environment variable is already tested in
# test_determinism.
s_val = re.sub(b"(?<=\n%%CreationDate: ).*", b"", s_val)
b_val = re.sub(b"(?<=\n%%CreationDate: ).*", b"", b_val)
assert s_val == b_val.replace(b'\r\n', b'\n')
def test_patheffects():
mpl.rcParams['path.effects'] = [
patheffects.withStroke(linewidth=4, foreground='w')]
fig, ax = plt.subplots()
ax.plot([1, 2, 3])
with io.BytesIO() as ps:
fig.savefig(ps, format='ps')
@needs_usetex
@needs_ghostscript
def test_tilde_in_tempfilename(tmp_path):
# Tilde ~ in the tempdir path (e.g. TMPDIR, TMP or TEMP on windows
# when the username is very long and windows uses a short name) breaks
# latex before https://github.com/matplotlib/matplotlib/pull/5928
base_tempdir = tmp_path / "short-1"
base_tempdir.mkdir()
# Change the path for new tempdirs, which is used internally by the ps
# backend to write a file.
with cbook._setattr_cm(tempfile, tempdir=str(base_tempdir)):
# usetex results in the latex call, which does not like the ~
mpl.rcParams['text.usetex'] = True
plt.plot([1, 2, 3, 4])
plt.xlabel(r'\textbf{time} (s)')
# use the PS backend to write the file...
plt.savefig(base_tempdir / 'tex_demo.eps', format="ps")
@image_comparison(["empty.eps"])
def test_transparency():
fig, ax = plt.subplots()
ax.set_axis_off()
ax.plot([0, 1], color="r", alpha=0)
ax.text(.5, .5, "foo", color="r", alpha=0)
@needs_usetex
@image_comparison(["empty.eps"])
def test_transparency_tex():
mpl.rcParams['text.usetex'] = True
fig, ax = plt.subplots()
ax.set_axis_off()
ax.plot([0, 1], color="r", alpha=0)
ax.text(.5, .5, "foo", color="r", alpha=0)
def test_bbox():
fig, ax = plt.subplots()
with io.BytesIO() as buf:
fig.savefig(buf, format='eps')
buf = buf.getvalue()
bb = re.search(b'^%%BoundingBox: (.+) (.+) (.+) (.+)$', buf, re.MULTILINE)
assert bb
hibb = re.search(b'^%%HiResBoundingBox: (.+) (.+) (.+) (.+)$', buf,
re.MULTILINE)
assert hibb
for i in range(1, 5):
# BoundingBox must use integers, and be ceil/floor of the hi res.
assert b'.' not in bb.group(i)
assert int(bb.group(i)) == pytest.approx(float(hibb.group(i)), 1)
@needs_usetex
def test_failing_latex():
"""Test failing latex subprocess call"""
mpl.rcParams['text.usetex'] = True
# This fails with "Double subscript"
plt.xlabel("$22_2_2$")
with pytest.raises(RuntimeError):
plt.savefig(io.BytesIO(), format="ps")
@needs_usetex
def test_partial_usetex(caplog):
caplog.set_level("WARNING")
plt.figtext(.1, .1, "foo", usetex=True)
plt.figtext(.2, .2, "bar", usetex=True)
plt.savefig(io.BytesIO(), format="ps")
record, = caplog.records # asserts there's a single record.
assert "as if usetex=False" in record.getMessage()
@needs_usetex
def test_usetex_preamble(caplog):
mpl.rcParams.update({
"text.usetex": True,
# Check that these don't conflict with the packages loaded by default.
"text.latex.preamble": r"\usepackage{color,graphicx,textcomp}",
})
plt.figtext(.5, .5, "foo")
plt.savefig(io.BytesIO(), format="ps")
@image_comparison(["useafm.eps"])
def test_useafm():
mpl.rcParams["ps.useafm"] = True
fig, ax = plt.subplots()
ax.set_axis_off()
ax.axhline(.5)
ax.text(.5, .5, "qk")
@image_comparison(["type3.eps"])
def test_type3_font():
plt.figtext(.5, .5, "I/J")
@image_comparison(["coloredhatcheszerolw.eps"])
def test_colored_hatch_zero_linewidth():
ax = plt.gca()
ax.add_patch(Ellipse((0, 0), 1, 1, hatch='/', facecolor='none',
edgecolor='r', linewidth=0))
ax.add_patch(Ellipse((0.5, 0.5), 0.5, 0.5, hatch='+', facecolor='none',
edgecolor='g', linewidth=0.2))
ax.add_patch(Ellipse((1, 1), 0.3, 0.8, hatch='\\', facecolor='none',
edgecolor='b', linewidth=0))
ax.set_axis_off()
@check_figures_equal(extensions=["eps"])
def test_text_clip(fig_test, fig_ref):
ax = fig_test.add_subplot()
# Fully clipped-out text should not appear.
ax.text(0, 0, "hello", transform=fig_test.transFigure, clip_on=True)
fig_ref.add_subplot()
@needs_ghostscript
def test_d_glyph(tmp_path):
# Ensure that we don't have a procedure defined as /d, which would be
# overwritten by the glyph definition for "d".
fig = plt.figure()
fig.text(.5, .5, "def")
out = tmp_path / "test.eps"
fig.savefig(out)
mpl.testing.compare.convert(out, cache=False) # Should not raise.
@image_comparison(["type42_without_prep.eps"], style='mpl20')
def test_type42_font_without_prep():
# Test whether Type 42 fonts without prep table are properly embedded
mpl.rcParams["ps.fonttype"] = 42
mpl.rcParams["mathtext.fontset"] = "stix"
plt.figtext(0.5, 0.5, "Mass $m$")
@pytest.mark.parametrize('fonttype', ["3", "42"])
def test_fonttype(fonttype):
mpl.rcParams["ps.fonttype"] = fonttype
fig, ax = plt.subplots()
ax.text(0.25, 0.5, "Forty-two is the answer to everything!")
buf = io.BytesIO()
fig.savefig(buf, format="ps")
test = b'/FontType ' + bytes(f"{fonttype}", encoding='utf-8') + b' def'
assert re.search(test, buf.getvalue(), re.MULTILINE)
def test_linedash():
"""Test that dashed lines do not break PS output"""
fig, ax = plt.subplots()
ax.plot([0, 1], linestyle="--")
buf = io.BytesIO()
fig.savefig(buf, format="ps")
assert buf.tell() > 0
def test_empty_line():
# Smoke-test for gh#23954
figure = Figure()
figure.text(0.5, 0.5, "\nfoo\n\n")
buf = io.BytesIO()
figure.savefig(buf, format='eps')
figure.savefig(buf, format='ps')
def test_no_duplicate_definition():
fig = Figure()
axs = fig.subplots(4, 4, subplot_kw=dict(projection="polar"))
for ax in axs.flat:
ax.set(xticks=[], yticks=[])
ax.plot([1, 2])
fig.suptitle("hello, world")
buf = io.StringIO()
fig.savefig(buf, format='eps')
buf.seek(0)
wds = [ln.partition(' ')[0] for
ln in buf.readlines()
if ln.startswith('/')]
assert max(Counter(wds).values()) == 1
@image_comparison(["multi_font_type3.eps"], tol=0.51)
def test_multi_font_type3():
fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
pytest.skip("Font may be missing")
plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
plt.rc('ps', fonttype=3)
fig = plt.figure()
fig.text(0.15, 0.475, "There are 几个汉字 in between!")
@image_comparison(["multi_font_type42.eps"], tol=1.6)
def test_multi_font_type42():
fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
pytest.skip("Font may be missing")
plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
plt.rc('ps', fonttype=42)
fig = plt.figure()
fig.text(0.15, 0.475, "There are 几个汉字 in between!")
@image_comparison(["scatter.eps"])
def test_path_collection():
rng = np.random.default_rng(19680801)
xvals = rng.uniform(0, 1, 10)
yvals = rng.uniform(0, 1, 10)
sizes = rng.uniform(30, 100, 10)
fig, ax = plt.subplots()
ax.scatter(xvals, yvals, sizes, edgecolor=[0.9, 0.2, 0.1], marker='<')
ax.set_axis_off()
paths = [path.Path.unit_regular_polygon(i) for i in range(3, 7)]
offsets = rng.uniform(0, 200, 20).reshape(10, 2)
sizes = [0.02, 0.04]
pc = mcollections.PathCollection(paths, sizes, zorder=-1,
facecolors='yellow', offsets=offsets)
ax.add_collection(pc)
ax.set_xlim(0, 1)
@image_comparison(["colorbar_shift.eps"], savefig_kwarg={"bbox_inches": "tight"},
style="mpl20")
def test_colorbar_shift(tmp_path):
cmap = mcolors.ListedColormap(["r", "g", "b"])
norm = mcolors.BoundaryNorm([-1, -0.5, 0.5, 1], cmap.N)
plt.scatter([0, 1], [1, 1], c=[0, 1], cmap=cmap, norm=norm)
plt.colorbar()
def test_auto_papersize_removal():
fig = plt.figure()
with pytest.raises(ValueError, match="'auto' is not a valid value"):
fig.savefig(io.BytesIO(), format='eps', papertype='auto')
with pytest.raises(ValueError, match="'auto' is not a valid value"):
mpl.rcParams['ps.papersize'] = 'auto'
@@ -0,0 +1,388 @@
import copy
import importlib
import os
import signal
import sys
from datetime import date, datetime
from unittest import mock
import pytest
import matplotlib
from matplotlib import pyplot as plt
from matplotlib._pylab_helpers import Gcf
from matplotlib import _c_internal_utils
try:
from matplotlib.backends.qt_compat import QtGui # type: ignore[attr-defined] # noqa: E501, F401
from matplotlib.backends.qt_compat import QtWidgets # type: ignore[attr-defined]
from matplotlib.backends.qt_editor import _formlayout
except ImportError:
pytestmark = pytest.mark.skip('No usable Qt bindings')
_test_timeout = 60 # A reasonably safe value for slower architectures.
@pytest.fixture
def qt_core(request):
from matplotlib.backends.qt_compat import QtCore
return QtCore
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
def test_fig_close():
# save the state of Gcf.figs
init_figs = copy.copy(Gcf.figs)
# make a figure using pyplot interface
fig = plt.figure()
# simulate user clicking the close button by reaching in
# and calling close on the underlying Qt object
fig.canvas.manager.window.close()
# assert that we have removed the reference to the FigureManager
# that got added by plt.figure()
assert init_figs == Gcf.figs
@pytest.mark.parametrize(
"qt_key, qt_mods, answer",
[
("Key_A", ["ShiftModifier"], "A"),
("Key_A", [], "a"),
("Key_A", ["ControlModifier"], ("ctrl+a")),
(
"Key_Aacute",
["ShiftModifier"],
"\N{LATIN CAPITAL LETTER A WITH ACUTE}",
),
("Key_Aacute", [], "\N{LATIN SMALL LETTER A WITH ACUTE}"),
("Key_Control", ["AltModifier"], ("alt+control")),
("Key_Alt", ["ControlModifier"], "ctrl+alt"),
(
"Key_Aacute",
["ControlModifier", "AltModifier", "MetaModifier"],
("ctrl+alt+meta+\N{LATIN SMALL LETTER A WITH ACUTE}"),
),
# We do not currently map the media keys, this may change in the
# future. This means the callback will never fire
("Key_Play", [], None),
("Key_Backspace", [], "backspace"),
(
"Key_Backspace",
["ControlModifier"],
"ctrl+backspace",
),
],
ids=[
'shift',
'lower',
'control',
'unicode_upper',
'unicode_lower',
'alt_control',
'control_alt',
'modifier_order',
'non_unicode_key',
'backspace',
'backspace_mod',
]
)
@pytest.mark.parametrize('backend', [
# Note: the value is irrelevant; the important part is the marker.
pytest.param(
'Qt5Agg',
marks=pytest.mark.backend('Qt5Agg', skip_on_importerror=True)),
pytest.param(
'QtAgg',
marks=pytest.mark.backend('QtAgg', skip_on_importerror=True)),
])
def test_correct_key(backend, qt_core, qt_key, qt_mods, answer, monkeypatch):
"""
Make a figure.
Send a key_press_event event (using non-public, qtX backend specific api).
Catch the event.
Assert sent and caught keys are the same.
"""
from matplotlib.backends.qt_compat import _to_int, QtCore
if sys.platform == "darwin" and answer is not None:
answer = answer.replace("ctrl", "cmd")
answer = answer.replace("control", "cmd")
answer = answer.replace("meta", "ctrl")
result = None
qt_mod = QtCore.Qt.KeyboardModifier.NoModifier
for mod in qt_mods:
qt_mod |= getattr(QtCore.Qt.KeyboardModifier, mod)
class _Event:
def isAutoRepeat(self): return False
def key(self): return _to_int(getattr(QtCore.Qt.Key, qt_key))
monkeypatch.setattr(QtWidgets.QApplication, "keyboardModifiers",
lambda self: qt_mod)
def on_key_press(event):
nonlocal result
result = event.key
qt_canvas = plt.figure().canvas
qt_canvas.mpl_connect('key_press_event', on_key_press)
qt_canvas.keyPressEvent(_Event())
assert result == answer
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
def test_device_pixel_ratio_change(qt_core):
"""
Make sure that if the pixel ratio changes, the figure dpi changes but the
widget remains the same logical size.
"""
prop = 'matplotlib.backends.backend_qt.FigureCanvasQT.devicePixelRatioF'
with mock.patch(prop) as p:
p.return_value = 3
fig = plt.figure(figsize=(5, 2), dpi=120)
qt_canvas = fig.canvas
qt_canvas.show()
def set_device_pixel_ratio(ratio):
p.return_value = ratio
window = qt_canvas.window().windowHandle()
current_version = tuple(
int(x) for x in qt_core.qVersion().split('.', 2)[:2])
if current_version >= (6, 6):
qt_core.QCoreApplication.sendEvent(
window,
qt_core.QEvent(qt_core.QEvent.Type.DevicePixelRatioChange))
else:
# The value here doesn't matter, as we can't mock the C++ QScreen
# object, but can override the functional wrapper around it.
# Emitting this event is simply to trigger the DPI change handler
# in Matplotlib in the same manner that it would occur normally.
window.screen().logicalDotsPerInchChanged.emit(96)
qt_canvas.draw()
qt_canvas.flush_events()
# Make sure the mocking worked
assert qt_canvas.device_pixel_ratio == ratio
qt_canvas.manager.show()
qt_canvas.draw()
qt_canvas.flush_events()
size = qt_canvas.size()
options = [
(None, 360, 1800, 720), # Use ratio at startup time.
(3, 360, 1800, 720), # Change to same ratio.
(2, 240, 1200, 480), # Change to different ratio.
(1.5, 180, 900, 360), # Fractional ratio.
]
for ratio, dpi, width, height in options:
if ratio is not None:
set_device_pixel_ratio(ratio)
# The DPI and the renderer width/height change
assert fig.dpi == dpi
assert qt_canvas.renderer.width == width
assert qt_canvas.renderer.height == height
# The actual widget size and figure logical size don't change.
assert size.width() == 600
assert size.height() == 240
assert qt_canvas.get_width_height() == (600, 240)
assert (fig.get_size_inches() == (5, 2)).all()
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
def test_subplottool():
fig, ax = plt.subplots()
with mock.patch("matplotlib.backends.qt_compat._exec", lambda obj: None):
fig.canvas.manager.toolbar.configure_subplots()
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
def test_figureoptions():
fig, ax = plt.subplots()
ax.plot([1, 2])
ax.imshow([[1]])
ax.scatter(range(3), range(3), c=range(3))
with mock.patch("matplotlib.backends.qt_compat._exec", lambda obj: None):
fig.canvas.manager.toolbar.edit_parameters()
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
def test_save_figure_return(tmp_path):
fig, ax = plt.subplots()
ax.imshow([[1]])
expected = tmp_path / "foobar.png"
prop = "matplotlib.backends.qt_compat.QtWidgets.QFileDialog.getSaveFileName"
with mock.patch(prop, return_value=(str(expected), None)):
fname = fig.canvas.manager.toolbar.save_figure()
assert fname == str(expected)
assert expected.exists()
with mock.patch(prop, return_value=(None, None)):
fname = fig.canvas.manager.toolbar.save_figure()
assert fname is None
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
def test_figureoptions_with_datetime_axes():
fig, ax = plt.subplots()
xydata = [
datetime(year=2021, month=1, day=1),
datetime(year=2021, month=2, day=1)
]
ax.plot(xydata, xydata)
with mock.patch("matplotlib.backends.qt_compat._exec", lambda obj: None):
fig.canvas.manager.toolbar.edit_parameters()
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
def test_double_resize():
# Check that resizing a figure twice keeps the same window size
fig, ax = plt.subplots()
fig.canvas.draw()
window = fig.canvas.manager.window
w, h = 3, 2
fig.set_size_inches(w, h)
assert fig.canvas.width() == w * matplotlib.rcParams['figure.dpi']
assert fig.canvas.height() == h * matplotlib.rcParams['figure.dpi']
old_width = window.width()
old_height = window.height()
fig.set_size_inches(w, h)
assert window.width() == old_width
assert window.height() == old_height
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
def test_canvas_reinit():
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
called = False
def crashing_callback(fig, stale):
nonlocal called
fig.canvas.draw_idle()
called = True
fig, ax = plt.subplots()
fig.stale_callback = crashing_callback
# this should not raise
canvas = FigureCanvasQTAgg(fig)
fig.stale = True
assert called
@pytest.mark.backend('Qt5Agg', skip_on_importerror=True)
def test_form_widget_get_with_datetime_and_date_fields():
from matplotlib.backends.backend_qt import _create_qApp
_create_qApp()
form = [
("Datetime field", datetime(year=2021, month=3, day=11)),
("Date field", date(year=2021, month=3, day=11))
]
widget = _formlayout.FormWidget(form)
widget.setup()
values = widget.get()
assert values == [
datetime(year=2021, month=3, day=11),
date(year=2021, month=3, day=11)
]
def _get_testable_qt_backends():
envs = []
for deps, env in [
([qt_api], {"MPLBACKEND": "qtagg", "QT_API": qt_api})
for qt_api in ["PyQt6", "PySide6", "PyQt5", "PySide2"]
]:
reason = None
missing = [dep for dep in deps if not importlib.util.find_spec(dep)]
if (sys.platform == "linux" and
not _c_internal_utils.display_is_valid()):
reason = "$DISPLAY and $WAYLAND_DISPLAY are unset"
elif missing:
reason = "{} cannot be imported".format(", ".join(missing))
elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'):
reason = "macosx backend fails on Azure"
marks = []
if reason:
marks.append(pytest.mark.skip(
reason=f"Skipping {env} because {reason}"))
envs.append(pytest.param(env, marks=marks, id=str(env)))
return envs
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
def test_fig_sigint_override(qt_core):
from matplotlib.backends.backend_qt5 import _BackendQT5
# Create a figure
plt.figure()
# Variable to access the handler from the inside of the event loop
event_loop_handler = None
# Callback to fire during event loop: save SIGINT handler, then exit
def fire_signal_and_quit():
# Save event loop signal
nonlocal event_loop_handler
event_loop_handler = signal.getsignal(signal.SIGINT)
# Request event loop exit
qt_core.QCoreApplication.exit()
# Timer to exit event loop
qt_core.QTimer.singleShot(0, fire_signal_and_quit)
# Save original SIGINT handler
original_handler = signal.getsignal(signal.SIGINT)
# Use our own SIGINT handler to be 100% sure this is working
def custom_handler(signum, frame):
pass
signal.signal(signal.SIGINT, custom_handler)
try:
# mainloop() sets SIGINT, starts Qt event loop (which triggers timer
# and exits) and then mainloop() resets SIGINT
matplotlib.backends.backend_qt._BackendQT.mainloop()
# Assert: signal handler during loop execution is changed
# (can't test equality with func)
assert event_loop_handler != custom_handler
# Assert: current signal handler is the same as the one we set before
assert signal.getsignal(signal.SIGINT) == custom_handler
# Repeat again to test that SIG_DFL and SIG_IGN will not be overridden
for custom_handler in (signal.SIG_DFL, signal.SIG_IGN):
qt_core.QTimer.singleShot(0, fire_signal_and_quit)
signal.signal(signal.SIGINT, custom_handler)
_BackendQT5.mainloop()
assert event_loop_handler == custom_handler
assert signal.getsignal(signal.SIGINT) == custom_handler
finally:
# Reset SIGINT handler to what it was before the test
signal.signal(signal.SIGINT, original_handler)
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
def test_ipython():
from matplotlib.testing import ipython_in_subprocess
ipython_in_subprocess("qt", {(8, 24): "qtagg", (8, 15): "QtAgg", (7, 0): "Qt5Agg"})
@@ -0,0 +1,180 @@
from collections.abc import Sequence
from typing import Any
import pytest
import matplotlib as mpl
from matplotlib.backends import BackendFilter, backend_registry
@pytest.fixture
def clear_backend_registry():
# Fixture that clears the singleton backend_registry before and after use
# so that the test state remains isolated.
backend_registry._clear()
yield
backend_registry._clear()
def has_duplicates(seq: Sequence[Any]) -> bool:
return len(seq) > len(set(seq))
@pytest.mark.parametrize(
'framework,expected',
[
('qt', 'qtagg'),
('gtk3', 'gtk3agg'),
('gtk4', 'gtk4agg'),
('wx', 'wxagg'),
('tk', 'tkagg'),
('macosx', 'macosx'),
('headless', 'agg'),
('does not exist', None),
]
)
def test_backend_for_gui_framework(framework, expected):
assert backend_registry.backend_for_gui_framework(framework) == expected
def test_list_builtin():
backends = backend_registry.list_builtin()
assert not has_duplicates(backends)
# Compare using sets as order is not important
assert {*backends} == {
'gtk3agg', 'gtk3cairo', 'gtk4agg', 'gtk4cairo', 'macosx', 'nbagg', 'notebook',
'qtagg', 'qtcairo', 'qt5agg', 'qt5cairo', 'tkagg',
'tkcairo', 'webagg', 'wx', 'wxagg', 'wxcairo', 'agg', 'cairo', 'pdf', 'pgf',
'ps', 'svg', 'template',
}
@pytest.mark.parametrize(
'filter,expected',
[
(BackendFilter.INTERACTIVE,
['gtk3agg', 'gtk3cairo', 'gtk4agg', 'gtk4cairo', 'macosx', 'nbagg', 'notebook',
'qtagg', 'qtcairo', 'qt5agg', 'qt5cairo', 'tkagg',
'tkcairo', 'webagg', 'wx', 'wxagg', 'wxcairo']),
(BackendFilter.NON_INTERACTIVE,
['agg', 'cairo', 'pdf', 'pgf', 'ps', 'svg', 'template']),
]
)
def test_list_builtin_with_filter(filter, expected):
backends = backend_registry.list_builtin(filter)
assert not has_duplicates(backends)
# Compare using sets as order is not important
assert {*backends} == {*expected}
def test_list_gui_frameworks():
frameworks = backend_registry.list_gui_frameworks()
assert not has_duplicates(frameworks)
# Compare using sets as order is not important
assert {*frameworks} == {
"gtk3", "gtk4", "macosx", "qt", "qt5", "qt6", "tk", "wx",
}
@pytest.mark.parametrize("backend, is_valid", [
("agg", True),
("QtAgg", True),
("module://anything", True),
("made-up-name", False),
])
def test_is_valid_backend(backend, is_valid):
assert backend_registry.is_valid_backend(backend) == is_valid
@pytest.mark.parametrize("backend, normalized", [
("agg", "matplotlib.backends.backend_agg"),
("QtAgg", "matplotlib.backends.backend_qtagg"),
("module://Anything", "Anything"),
])
def test_backend_normalization(backend, normalized):
assert backend_registry._backend_module_name(backend) == normalized
def test_deprecated_rcsetup_attributes():
match = "was deprecated in Matplotlib 3.9"
with pytest.warns(mpl.MatplotlibDeprecationWarning, match=match):
mpl.rcsetup.interactive_bk
with pytest.warns(mpl.MatplotlibDeprecationWarning, match=match):
mpl.rcsetup.non_interactive_bk
with pytest.warns(mpl.MatplotlibDeprecationWarning, match=match):
mpl.rcsetup.all_backends
def test_entry_points_inline():
pytest.importorskip('matplotlib_inline')
backends = backend_registry.list_all()
assert 'inline' in backends
def test_entry_points_ipympl():
pytest.importorskip('ipympl')
backends = backend_registry.list_all()
assert 'ipympl' in backends
assert 'widget' in backends
def test_entry_point_name_shadows_builtin(clear_backend_registry):
with pytest.raises(RuntimeError):
backend_registry._validate_and_store_entry_points(
[('qtagg', 'module1')])
def test_entry_point_name_duplicate(clear_backend_registry):
with pytest.raises(RuntimeError):
backend_registry._validate_and_store_entry_points(
[('some_name', 'module1'), ('some_name', 'module2')])
def test_entry_point_identical(clear_backend_registry):
# Issue https://github.com/matplotlib/matplotlib/issues/28367
# Multiple entry points with the same name and value (value is the module)
# are acceptable.
n = len(backend_registry._name_to_module)
backend_registry._validate_and_store_entry_points(
[('some_name', 'some.module'), ('some_name', 'some.module')])
assert len(backend_registry._name_to_module) == n+1
assert backend_registry._name_to_module['some_name'] == 'module://some.module'
def test_entry_point_name_is_module(clear_backend_registry):
with pytest.raises(RuntimeError):
backend_registry._validate_and_store_entry_points(
[('module://backend.something', 'module1')])
@pytest.mark.parametrize('backend', [
'agg',
'module://matplotlib.backends.backend_agg',
])
def test_load_entry_points_only_if_needed(clear_backend_registry, backend):
assert not backend_registry._loaded_entry_points
check = backend_registry.resolve_backend(backend)
assert check == (backend, None)
assert not backend_registry._loaded_entry_points
backend_registry.list_all() # Force load of entry points
assert backend_registry._loaded_entry_points
@pytest.mark.parametrize(
'gui_or_backend, expected_backend, expected_gui',
[
('agg', 'agg', None),
('qt', 'qtagg', 'qt'),
('TkCairo', 'tkcairo', 'tk'),
]
)
def test_resolve_gui_or_backend(gui_or_backend, expected_backend, expected_gui):
backend, gui = backend_registry.resolve_gui_or_backend(gui_or_backend)
assert backend == expected_backend
assert gui == expected_gui
def test_resolve_gui_or_backend_invalid():
match = "is not a recognised GUI loop or backend name"
with pytest.raises(RuntimeError, match=match):
backend_registry.resolve_gui_or_backend('no-such-name')
@@ -0,0 +1,707 @@
import datetime
from io import BytesIO
from pathlib import Path
import xml.etree.ElementTree
import xml.parsers.expat
import pytest
import numpy as np
import matplotlib as mpl
from matplotlib.figure import Figure
from matplotlib.patches import Circle
from matplotlib.text import Text
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import check_figures_equal, image_comparison
from matplotlib.testing._markers import needs_usetex
from matplotlib import font_manager as fm
from matplotlib.offsetbox import (OffsetImage, AnnotationBbox)
def test_visibility():
fig, ax = plt.subplots()
x = np.linspace(0, 4 * np.pi, 50)
y = np.sin(x)
yerr = np.ones_like(y)
a, b, c = ax.errorbar(x, y, yerr=yerr, fmt='ko')
for artist in b:
artist.set_visible(False)
with BytesIO() as fd:
fig.savefig(fd, format='svg')
buf = fd.getvalue()
parser = xml.parsers.expat.ParserCreate()
parser.Parse(buf) # this will raise ExpatError if the svg is invalid
@image_comparison(['fill_black_with_alpha.svg'], remove_text=True)
def test_fill_black_with_alpha():
fig, ax = plt.subplots()
ax.scatter(x=[0, 0.1, 1], y=[0, 0, 0], c='k', alpha=0.1, s=10000)
@image_comparison(['noscale'], remove_text=True)
def test_noscale():
X, Y = np.meshgrid(np.arange(-5, 5, 1), np.arange(-5, 5, 1))
Z = np.sin(Y ** 2)
fig, ax = plt.subplots()
ax.imshow(Z, cmap='gray', interpolation='none')
def test_text_urls():
fig = plt.figure()
test_url = "http://test_text_urls.matplotlib.org"
fig.suptitle("test_text_urls", url=test_url)
with BytesIO() as fd:
fig.savefig(fd, format='svg')
buf = fd.getvalue().decode()
expected = f'<a xlink:href="{test_url}">'
assert expected in buf
@image_comparison(['bold_font_output.svg'])
def test_bold_font_output():
fig, ax = plt.subplots()
ax.plot(np.arange(10), np.arange(10))
ax.set_xlabel('nonbold-xlabel')
ax.set_ylabel('bold-ylabel', fontweight='bold')
ax.set_title('bold-title', fontweight='bold')
@image_comparison(['bold_font_output_with_none_fonttype.svg'])
def test_bold_font_output_with_none_fonttype():
plt.rcParams['svg.fonttype'] = 'none'
fig, ax = plt.subplots()
ax.plot(np.arange(10), np.arange(10))
ax.set_xlabel('nonbold-xlabel')
ax.set_ylabel('bold-ylabel', fontweight='bold')
ax.set_title('bold-title', fontweight='bold')
@check_figures_equal(tol=20)
def test_rasterized(fig_test, fig_ref):
t = np.arange(0, 100) * (2.3)
x = np.cos(t)
y = np.sin(t)
ax_ref = fig_ref.subplots()
ax_ref.plot(x, y, "-", c="r", lw=10)
ax_ref.plot(x+1, y, "-", c="b", lw=10)
ax_test = fig_test.subplots()
ax_test.plot(x, y, "-", c="r", lw=10, rasterized=True)
ax_test.plot(x+1, y, "-", c="b", lw=10, rasterized=True)
@check_figures_equal(extensions=['svg'])
def test_rasterized_ordering(fig_test, fig_ref):
t = np.arange(0, 100) * (2.3)
x = np.cos(t)
y = np.sin(t)
ax_ref = fig_ref.subplots()
ax_ref.set_xlim(0, 3)
ax_ref.set_ylim(-1.1, 1.1)
ax_ref.plot(x, y, "-", c="r", lw=10, rasterized=True)
ax_ref.plot(x+1, y, "-", c="b", lw=10, rasterized=False)
ax_ref.plot(x+2, y, "-", c="g", lw=10, rasterized=True)
ax_ref.plot(x+3, y, "-", c="m", lw=10, rasterized=True)
ax_test = fig_test.subplots()
ax_test.set_xlim(0, 3)
ax_test.set_ylim(-1.1, 1.1)
ax_test.plot(x, y, "-", c="r", lw=10, rasterized=True, zorder=1.1)
ax_test.plot(x+2, y, "-", c="g", lw=10, rasterized=True, zorder=1.3)
ax_test.plot(x+3, y, "-", c="m", lw=10, rasterized=True, zorder=1.4)
ax_test.plot(x+1, y, "-", c="b", lw=10, rasterized=False, zorder=1.2)
@check_figures_equal(tol=5, extensions=['svg', 'pdf'])
def test_prevent_rasterization(fig_test, fig_ref):
loc = [0.05, 0.05]
ax_ref = fig_ref.subplots()
ax_ref.plot([loc[0]], [loc[1]], marker="x", c="black", zorder=2)
b = mpl.offsetbox.TextArea("X")
abox = mpl.offsetbox.AnnotationBbox(b, loc, zorder=2.1)
ax_ref.add_artist(abox)
ax_test = fig_test.subplots()
ax_test.plot([loc[0]], [loc[1]], marker="x", c="black", zorder=2,
rasterized=True)
b = mpl.offsetbox.TextArea("X")
abox = mpl.offsetbox.AnnotationBbox(b, loc, zorder=2.1)
ax_test.add_artist(abox)
def test_count_bitmaps():
def count_tag(fig, tag):
with BytesIO() as fd:
fig.savefig(fd, format='svg')
buf = fd.getvalue().decode()
return buf.count(f"<{tag}")
# No rasterized elements
fig1 = plt.figure()
ax1 = fig1.add_subplot(1, 1, 1)
ax1.set_axis_off()
for n in range(5):
ax1.plot([0, 20], [0, n], "b-", rasterized=False)
assert count_tag(fig1, "image") == 0
assert count_tag(fig1, "path") == 6 # axis patch plus lines
# rasterized can be merged
fig2 = plt.figure()
ax2 = fig2.add_subplot(1, 1, 1)
ax2.set_axis_off()
for n in range(5):
ax2.plot([0, 20], [0, n], "b-", rasterized=True)
assert count_tag(fig2, "image") == 1
assert count_tag(fig2, "path") == 1 # axis patch
# rasterized can't be merged without affecting draw order
fig3 = plt.figure()
ax3 = fig3.add_subplot(1, 1, 1)
ax3.set_axis_off()
for n in range(5):
ax3.plot([0, 20], [n, 0], "b-", rasterized=False)
ax3.plot([0, 20], [0, n], "b-", rasterized=True)
assert count_tag(fig3, "image") == 5
assert count_tag(fig3, "path") == 6
# rasterized whole axes
fig4 = plt.figure()
ax4 = fig4.add_subplot(1, 1, 1)
ax4.set_axis_off()
ax4.set_rasterized(True)
for n in range(5):
ax4.plot([0, 20], [n, 0], "b-", rasterized=False)
ax4.plot([0, 20], [0, n], "b-", rasterized=True)
assert count_tag(fig4, "image") == 1
assert count_tag(fig4, "path") == 1
# rasterized can be merged, but inhibited by suppressComposite
fig5 = plt.figure()
fig5.suppressComposite = True
ax5 = fig5.add_subplot(1, 1, 1)
ax5.set_axis_off()
for n in range(5):
ax5.plot([0, 20], [0, n], "b-", rasterized=True)
assert count_tag(fig5, "image") == 5
assert count_tag(fig5, "path") == 1 # axis patch
# Use Computer Modern Sans Serif, not Helvetica (which has no \textwon).
@mpl.style.context('default')
@needs_usetex
def test_unicode_won():
fig = Figure()
fig.text(.5, .5, r'\textwon', usetex=True)
with BytesIO() as fd:
fig.savefig(fd, format='svg')
buf = fd.getvalue()
tree = xml.etree.ElementTree.fromstring(buf)
ns = 'http://www.w3.org/2000/svg'
won_id = 'SFSS3583-8e'
assert len(tree.findall(f'.//{{{ns}}}path[@d][@id="{won_id}"]')) == 1
assert f'#{won_id}' in tree.find(f'.//{{{ns}}}use').attrib.values()
def test_svgnone_with_data_coordinates():
plt.rcParams.update({'svg.fonttype': 'none', 'font.stretch': 'condensed'})
expected = 'Unlikely to appear by chance'
fig, ax = plt.subplots()
ax.text(np.datetime64('2019-06-30'), 1, expected)
ax.set_xlim(np.datetime64('2019-01-01'), np.datetime64('2019-12-31'))
ax.set_ylim(0, 2)
with BytesIO() as fd:
fig.savefig(fd, format='svg')
fd.seek(0)
buf = fd.read().decode()
assert expected in buf and "condensed" in buf
def test_gid():
"""Test that object gid appears in output svg."""
from matplotlib.offsetbox import OffsetBox
from matplotlib.axis import Tick
fig = plt.figure()
ax1 = fig.add_subplot(131)
ax1.imshow([[1., 2.], [2., 3.]], aspect="auto")
ax1.scatter([1, 2, 3], [1, 2, 3], label="myscatter")
ax1.plot([2, 3, 1], label="myplot")
ax1.legend()
ax1a = ax1.twinx()
ax1a.bar([1, 2, 3], [1, 2, 3])
ax2 = fig.add_subplot(132, projection="polar")
ax2.plot([0, 1.5, 3], [1, 2, 3])
ax3 = fig.add_subplot(133, projection="3d")
ax3.plot([1, 2], [1, 2], [1, 2])
fig.canvas.draw()
gdic = {}
for idx, obj in enumerate(fig.findobj(include_self=True)):
if obj.get_visible():
gid = f"test123{obj.__class__.__name__}_{idx}"
gdic[gid] = obj
obj.set_gid(gid)
with BytesIO() as fd:
fig.savefig(fd, format='svg')
buf = fd.getvalue().decode()
def include(gid, obj):
# we need to exclude certain objects which will not appear in the svg
if isinstance(obj, OffsetBox):
return False
if isinstance(obj, Text):
if obj.get_text() == "":
return False
elif obj.axes is None:
return False
if isinstance(obj, plt.Line2D):
xdata, ydata = obj.get_data()
if len(xdata) == len(ydata) == 1:
return False
elif not hasattr(obj, "axes") or obj.axes is None:
return False
if isinstance(obj, Tick):
loc = obj.get_loc()
if loc == 0:
return False
vi = obj.get_view_interval()
if loc < min(vi) or loc > max(vi):
return False
return True
for gid, obj in gdic.items():
if include(gid, obj):
assert gid in buf
def test_clip_path_ids_reuse():
fig, circle = Figure(), Circle((0, 0), radius=10)
for i in range(5):
ax = fig.add_subplot()
aimg = ax.imshow([[i]])
aimg.set_clip_path(circle)
inner_circle = Circle((0, 0), radius=1)
ax = fig.add_subplot()
aimg = ax.imshow([[0]])
aimg.set_clip_path(inner_circle)
with BytesIO() as fd:
fig.savefig(fd, format='svg')
buf = fd.getvalue()
tree = xml.etree.ElementTree.fromstring(buf)
ns = 'http://www.w3.org/2000/svg'
clip_path_ids = set()
for node in tree.findall(f'.//{{{ns}}}clipPath[@id]'):
node_id = node.attrib['id']
assert node_id not in clip_path_ids # assert ID uniqueness
clip_path_ids.add(node_id)
assert len(clip_path_ids) == 2 # only two clipPaths despite reuse in multiple axes
def test_savefig_tight():
# Check that the draw-disabled renderer correctly disables open/close_group
# as well.
plt.savefig(BytesIO(), format="svgz", bbox_inches="tight")
def test_url():
# Test that object url appears in output svg.
fig, ax = plt.subplots()
# collections
s = ax.scatter([1, 2, 3], [4, 5, 6])
s.set_urls(['https://example.com/foo', 'https://example.com/bar', None])
# Line2D
p, = plt.plot([2, 3, 4], [4, 5, 6])
p.set_url('https://example.com/baz')
# Line2D markers-only
p, = plt.plot([3, 4, 5], [4, 5, 6], linestyle='none', marker='x')
p.set_url('https://example.com/quux')
b = BytesIO()
fig.savefig(b, format='svg')
b = b.getvalue()
for v in [b'foo', b'bar', b'baz', b'quux']:
assert b'https://example.com/' + v in b
def test_url_tick(monkeypatch):
monkeypatch.setenv('SOURCE_DATE_EPOCH', '19680801')
fig1, ax = plt.subplots()
ax.scatter([1, 2, 3], [4, 5, 6])
for i, tick in enumerate(ax.yaxis.get_major_ticks()):
tick.set_url(f'https://example.com/{i}')
fig2, ax = plt.subplots()
ax.scatter([1, 2, 3], [4, 5, 6])
for i, tick in enumerate(ax.yaxis.get_major_ticks()):
tick.label1.set_url(f'https://example.com/{i}')
tick.label2.set_url(f'https://example.com/{i}')
b1 = BytesIO()
fig1.savefig(b1, format='svg')
b1 = b1.getvalue()
b2 = BytesIO()
fig2.savefig(b2, format='svg')
b2 = b2.getvalue()
for i in range(len(ax.yaxis.get_major_ticks())):
assert f'https://example.com/{i}'.encode('ascii') in b1
assert b1 == b2
def test_svg_default_metadata(monkeypatch):
# Values have been predefined for 'Creator', 'Date', 'Format', and 'Type'.
monkeypatch.setenv('SOURCE_DATE_EPOCH', '19680801')
fig, ax = plt.subplots()
with BytesIO() as fd:
fig.savefig(fd, format='svg')
buf = fd.getvalue().decode()
# Creator
assert mpl.__version__ in buf
# Date
assert '1970-08-16' in buf
# Format
assert 'image/svg+xml' in buf
# Type
assert 'StillImage' in buf
# Now make sure all the default metadata can be cleared.
with BytesIO() as fd:
fig.savefig(fd, format='svg', metadata={'Date': None, 'Creator': None,
'Format': None, 'Type': None})
buf = fd.getvalue().decode()
# Creator
assert mpl.__version__ not in buf
# Date
assert '1970-08-16' not in buf
# Format
assert 'image/svg+xml' not in buf
# Type
assert 'StillImage' not in buf
def test_svg_clear_default_metadata(monkeypatch):
# Makes sure that setting a default metadata to `None`
# removes the corresponding tag from the metadata.
monkeypatch.setenv('SOURCE_DATE_EPOCH', '19680801')
metadata_contains = {'creator': mpl.__version__, 'date': '1970-08-16',
'format': 'image/svg+xml', 'type': 'StillImage'}
SVGNS = '{http://www.w3.org/2000/svg}'
RDFNS = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}'
CCNS = '{http://creativecommons.org/ns#}'
DCNS = '{http://purl.org/dc/elements/1.1/}'
fig, ax = plt.subplots()
for name in metadata_contains:
with BytesIO() as fd:
fig.savefig(fd, format='svg', metadata={name.title(): None})
buf = fd.getvalue().decode()
root = xml.etree.ElementTree.fromstring(buf)
work, = root.findall(f'./{SVGNS}metadata/{RDFNS}RDF/{CCNS}Work')
for key in metadata_contains:
data = work.findall(f'./{DCNS}{key}')
if key == name:
# The one we cleared is not there
assert not data
continue
# Everything else should be there
data, = data
xmlstr = xml.etree.ElementTree.tostring(data, encoding="unicode")
assert metadata_contains[key] in xmlstr
def test_svg_clear_all_metadata():
# Makes sure that setting all default metadata to `None`
# removes the metadata tag from the output.
fig, ax = plt.subplots()
with BytesIO() as fd:
fig.savefig(fd, format='svg', metadata={'Date': None, 'Creator': None,
'Format': None, 'Type': None})
buf = fd.getvalue().decode()
SVGNS = '{http://www.w3.org/2000/svg}'
root = xml.etree.ElementTree.fromstring(buf)
assert not root.findall(f'./{SVGNS}metadata')
def test_svg_metadata():
single_value = ['Coverage', 'Identifier', 'Language', 'Relation', 'Source',
'Title', 'Type']
multi_value = ['Contributor', 'Creator', 'Keywords', 'Publisher', 'Rights']
metadata = {
'Date': [datetime.date(1968, 8, 1),
datetime.datetime(1968, 8, 2, 1, 2, 3)],
'Description': 'description\ntext',
**{k: f'{k} foo' for k in single_value},
**{k: [f'{k} bar', f'{k} baz'] for k in multi_value},
}
fig = plt.figure()
with BytesIO() as fd:
fig.savefig(fd, format='svg', metadata=metadata)
buf = fd.getvalue().decode()
SVGNS = '{http://www.w3.org/2000/svg}'
RDFNS = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}'
CCNS = '{http://creativecommons.org/ns#}'
DCNS = '{http://purl.org/dc/elements/1.1/}'
root = xml.etree.ElementTree.fromstring(buf)
rdf, = root.findall(f'./{SVGNS}metadata/{RDFNS}RDF')
# Check things that are single entries.
titles = [node.text for node in root.findall(f'./{SVGNS}title')]
assert titles == [metadata['Title']]
types = [node.attrib[f'{RDFNS}resource']
for node in rdf.findall(f'./{CCNS}Work/{DCNS}type')]
assert types == [metadata['Type']]
for k in ['Description', *single_value]:
if k == 'Type':
continue
values = [node.text
for node in rdf.findall(f'./{CCNS}Work/{DCNS}{k.lower()}')]
assert values == [metadata[k]]
# Check things that are multi-value entries.
for k in multi_value:
if k == 'Keywords':
continue
values = [
node.text
for node in rdf.findall(
f'./{CCNS}Work/{DCNS}{k.lower()}/{CCNS}Agent/{DCNS}title')]
assert values == metadata[k]
# Check special things.
dates = [node.text for node in rdf.findall(f'./{CCNS}Work/{DCNS}date')]
assert dates == ['1968-08-01/1968-08-02T01:02:03']
values = [node.text for node in
rdf.findall(f'./{CCNS}Work/{DCNS}subject/{RDFNS}Bag/{RDFNS}li')]
assert values == metadata['Keywords']
@image_comparison(["multi_font_aspath.svg"], tol=1.8)
def test_multi_font_type3():
fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
pytest.skip("Font may be missing")
plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
plt.rc('svg', fonttype='path')
fig = plt.figure()
fig.text(0.15, 0.475, "There are 几个汉字 in between!")
@image_comparison(["multi_font_astext.svg"])
def test_multi_font_type42():
fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
pytest.skip("Font may be missing")
fig = plt.figure()
plt.rc('svg', fonttype='none')
plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
fig.text(0.15, 0.475, "There are 几个汉字 in between!")
@pytest.mark.parametrize('metadata,error,message', [
({'Date': 1}, TypeError, "Invalid type for Date metadata. Expected str"),
({'Date': [1]}, TypeError,
"Invalid type for Date metadata. Expected iterable"),
({'Keywords': 1}, TypeError,
"Invalid type for Keywords metadata. Expected str"),
({'Keywords': [1]}, TypeError,
"Invalid type for Keywords metadata. Expected iterable"),
({'Creator': 1}, TypeError,
"Invalid type for Creator metadata. Expected str"),
({'Creator': [1]}, TypeError,
"Invalid type for Creator metadata. Expected iterable"),
({'Title': 1}, TypeError,
"Invalid type for Title metadata. Expected str"),
({'Format': 1}, TypeError,
"Invalid type for Format metadata. Expected str"),
({'Foo': 'Bar'}, ValueError, "Unknown metadata key"),
])
def test_svg_incorrect_metadata(metadata, error, message):
with pytest.raises(error, match=message), BytesIO() as fd:
fig = plt.figure()
fig.savefig(fd, format='svg', metadata=metadata)
def test_svg_escape():
fig = plt.figure()
fig.text(0.5, 0.5, "<\'\"&>", gid="<\'\"&>")
with BytesIO() as fd:
fig.savefig(fd, format='svg')
buf = fd.getvalue().decode()
assert '&lt;&apos;&quot;&amp;&gt;"' in buf
@pytest.mark.parametrize("font_str", [
"'DejaVu Sans', 'WenQuanYi Zen Hei', 'Arial', sans-serif",
"'DejaVu Serif', 'WenQuanYi Zen Hei', 'Times New Roman', serif",
"'Arial', 'WenQuanYi Zen Hei', cursive",
"'Impact', 'WenQuanYi Zen Hei', fantasy",
"'DejaVu Sans Mono', 'WenQuanYi Zen Hei', 'Courier New', monospace",
# These do not work because the logic to get the font metrics will not find
# WenQuanYi as the fallback logic stops with the first fallback font:
# "'DejaVu Sans Mono', 'Courier New', 'WenQuanYi Zen Hei', monospace",
# "'DejaVu Sans', 'Arial', 'WenQuanYi Zen Hei', sans-serif",
# "'DejaVu Serif', 'Times New Roman', 'WenQuanYi Zen Hei', serif",
])
@pytest.mark.parametrize("include_generic", [True, False])
def test_svg_font_string(font_str, include_generic):
fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
pytest.skip("Font may be missing")
explicit, *rest, generic = map(
lambda x: x.strip("'"), font_str.split(", ")
)
size = len(generic)
if include_generic:
rest = rest + [generic]
plt.rcParams[f"font.{generic}"] = rest
plt.rcParams["font.size"] = size
plt.rcParams["svg.fonttype"] = "none"
fig, ax = plt.subplots()
if generic == "sans-serif":
generic_options = ["sans", "sans-serif", "sans serif"]
else:
generic_options = [generic]
for generic_name in generic_options:
# test that fallback works
ax.text(0.5, 0.5, "There are 几个汉字 in between!",
family=[explicit, generic_name], ha="center")
# test deduplication works
ax.text(0.5, 0.1, "There are 几个汉字 in between!",
family=[explicit, *rest, generic_name], ha="center")
ax.axis("off")
with BytesIO() as fd:
fig.savefig(fd, format="svg")
buf = fd.getvalue()
tree = xml.etree.ElementTree.fromstring(buf)
ns = "http://www.w3.org/2000/svg"
text_count = 0
for text_element in tree.findall(f".//{{{ns}}}text"):
text_count += 1
font_style = dict(
map(lambda x: x.strip(), _.strip().split(":"))
for _ in dict(text_element.items())["style"].split(";")
)
assert font_style["font-size"] == f"{size}px"
assert font_style["font-family"] == font_str
assert text_count == len(ax.texts)
def test_annotationbbox_gid():
# Test that object gid appears in the AnnotationBbox
# in output svg.
fig = plt.figure()
ax = fig.add_subplot()
arr_img = np.ones((32, 32))
xy = (0.3, 0.55)
imagebox = OffsetImage(arr_img, zoom=0.1)
imagebox.image.axes = ax
ab = AnnotationBbox(imagebox, xy,
xybox=(120., -80.),
xycoords='data',
boxcoords="offset points",
pad=0.5,
arrowprops=dict(
arrowstyle="->",
connectionstyle="angle,angleA=0,angleB=90,rad=3")
)
ab.set_gid("a test for issue 20044")
ax.add_artist(ab)
with BytesIO() as fd:
fig.savefig(fd, format='svg')
buf = fd.getvalue().decode('utf-8')
expected = '<g id="a test for issue 20044">'
assert expected in buf
def test_svgid():
"""Test that `svg.id` rcparam appears in output svg if not None."""
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [3, 2, 1])
fig.canvas.draw()
# Default: svg.id = None
with BytesIO() as fd:
fig.savefig(fd, format='svg')
buf = fd.getvalue().decode()
tree = xml.etree.ElementTree.fromstring(buf)
assert plt.rcParams['svg.id'] is None
assert not tree.findall('.[@id]')
# String: svg.id = str
svg_id = 'a test for issue 28535'
plt.rc('svg', id=svg_id)
with BytesIO() as fd:
fig.savefig(fd, format='svg')
buf = fd.getvalue().decode()
tree = xml.etree.ElementTree.fromstring(buf)
assert plt.rcParams['svg.id'] == svg_id
assert tree.findall(f'.[@id="{svg_id}"]')
@@ -0,0 +1,62 @@
"""
Backend-loading machinery tests, using variations on the template backend.
"""
import sys
from types import SimpleNamespace
from unittest.mock import MagicMock
import matplotlib as mpl
from matplotlib import pyplot as plt
from matplotlib.backends import backend_template
from matplotlib.backends.backend_template import (
FigureCanvasTemplate, FigureManagerTemplate)
def test_load_template():
mpl.use("template")
assert type(plt.figure().canvas) == FigureCanvasTemplate
def test_load_old_api(monkeypatch):
mpl_test_backend = SimpleNamespace(**vars(backend_template))
mpl_test_backend.new_figure_manager = (
lambda num, *args, FigureClass=mpl.figure.Figure, **kwargs:
FigureManagerTemplate(
FigureCanvasTemplate(FigureClass(*args, **kwargs)), num))
monkeypatch.setitem(sys.modules, "mpl_test_backend", mpl_test_backend)
mpl.use("module://mpl_test_backend")
assert type(plt.figure().canvas) == FigureCanvasTemplate
plt.draw_if_interactive()
def test_show(monkeypatch):
mpl_test_backend = SimpleNamespace(**vars(backend_template))
mock_show = MagicMock()
monkeypatch.setattr(
mpl_test_backend.FigureManagerTemplate, "pyplot_show", mock_show)
monkeypatch.setitem(sys.modules, "mpl_test_backend", mpl_test_backend)
mpl.use("module://mpl_test_backend")
plt.show()
mock_show.assert_called_with()
def test_show_old_global_api(monkeypatch):
mpl_test_backend = SimpleNamespace(**vars(backend_template))
mock_show = MagicMock()
monkeypatch.setattr(mpl_test_backend, "show", mock_show, raising=False)
monkeypatch.setitem(sys.modules, "mpl_test_backend", mpl_test_backend)
mpl.use("module://mpl_test_backend")
plt.show()
mock_show.assert_called_with()
def test_load_case_sensitive(monkeypatch):
mpl_test_backend = SimpleNamespace(**vars(backend_template))
mock_show = MagicMock()
monkeypatch.setattr(
mpl_test_backend.FigureManagerTemplate, "pyplot_show", mock_show)
monkeypatch.setitem(sys.modules, "mpl_Test_Backend", mpl_test_backend)
mpl.use("module://mpl_Test_Backend")
plt.show()
mock_show.assert_called_with()
@@ -0,0 +1,280 @@
import functools
import importlib
import os
import platform
import subprocess
import sys
import pytest
from matplotlib import _c_internal_utils
from matplotlib.testing import subprocess_run_helper
_test_timeout = 60 # A reasonably safe value for slower architectures.
def _isolated_tk_test(success_count, func=None):
"""
A decorator to run *func* in a subprocess and assert that it prints
"success" *success_count* times and nothing on stderr.
TkAgg tests seem to have interactions between tests, so isolate each test
in a subprocess. See GH#18261
"""
if func is None:
return functools.partial(_isolated_tk_test, success_count)
if "MPL_TEST_ESCAPE_HATCH" in os.environ:
# set in subprocess_run_helper() below
return func
@pytest.mark.skipif(
not importlib.util.find_spec('tkinter'),
reason="missing tkinter"
)
@pytest.mark.skipif(
sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(),
reason="$DISPLAY is unset"
)
@pytest.mark.xfail( # https://github.com/actions/setup-python/issues/649
('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
sys.platform == 'darwin' and sys.version_info[:2] < (3, 11),
reason='Tk version mismatch on Azure macOS CI'
)
@functools.wraps(func)
def test_func():
# even if the package exists, may not actually be importable this can
# be the case on some CI systems.
pytest.importorskip('tkinter')
try:
proc = subprocess_run_helper(
func, timeout=_test_timeout, extra_env=dict(
MPLBACKEND="TkAgg", MPL_TEST_ESCAPE_HATCH="1"))
except subprocess.TimeoutExpired:
pytest.fail("Subprocess timed out")
except subprocess.CalledProcessError as e:
pytest.fail("Subprocess failed to test intended behavior\n"
+ str(e.stderr))
else:
# macOS may actually emit irrelevant errors about Accelerated
# OpenGL vs. software OpenGL, or some permission error on Azure, so
# suppress them.
# Asserting stderr first (and printing it on failure) should be
# more helpful for debugging that printing a failed success count.
ignored_lines = ["OpenGL", "CFMessagePort: bootstrap_register",
"/usr/include/servers/bootstrap_defs.h"]
assert not [line for line in proc.stderr.splitlines()
if all(msg not in line for msg in ignored_lines)]
assert proc.stdout.count("success") == success_count
return test_func
@_isolated_tk_test(success_count=6) # len(bad_boxes)
def test_blit():
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.backends.backend_tkagg # noqa
from matplotlib.backends import _backend_tk, _tkagg
fig, ax = plt.subplots()
photoimage = fig.canvas._tkphoto
data = np.ones((4, 4, 4), dtype=np.uint8)
# Test out of bounds blitting.
bad_boxes = ((-1, 2, 0, 2),
(2, 0, 0, 2),
(1, 6, 0, 2),
(0, 2, -1, 2),
(0, 2, 2, 0),
(0, 2, 1, 6))
for bad_box in bad_boxes:
try:
_tkagg.blit(
photoimage.tk.interpaddr(), str(photoimage), data,
_tkagg.TK_PHOTO_COMPOSITE_OVERLAY, (0, 1, 2, 3), bad_box)
except ValueError:
print("success")
# Test blitting to a destroyed canvas.
plt.close(fig)
_backend_tk.blit(photoimage, data, (0, 1, 2, 3))
@_isolated_tk_test(success_count=1)
def test_figuremanager_preserves_host_mainloop():
import tkinter
import matplotlib.pyplot as plt
success = []
def do_plot():
plt.figure()
plt.plot([1, 2], [3, 5])
plt.close()
root.after(0, legitimate_quit)
def legitimate_quit():
root.quit()
success.append(True)
root = tkinter.Tk()
root.after(0, do_plot)
root.mainloop()
if success:
print("success")
@pytest.mark.skipif(platform.python_implementation() != 'CPython',
reason='PyPy does not support Tkinter threading: '
'https://foss.heptapod.net/pypy/pypy/-/issues/1929')
@pytest.mark.flaky(reruns=3)
@_isolated_tk_test(success_count=1)
def test_figuremanager_cleans_own_mainloop():
import tkinter
import time
import matplotlib.pyplot as plt
import threading
from matplotlib.cbook import _get_running_interactive_framework
root = tkinter.Tk()
plt.plot([1, 2, 3], [1, 2, 5])
def target():
while not 'tk' == _get_running_interactive_framework():
time.sleep(.01)
plt.close()
if show_finished_event.wait():
print('success')
show_finished_event = threading.Event()
thread = threading.Thread(target=target, daemon=True)
thread.start()
plt.show(block=True) # Testing if this function hangs.
show_finished_event.set()
thread.join()
@pytest.mark.flaky(reruns=3)
@_isolated_tk_test(success_count=0)
def test_never_update():
import tkinter
del tkinter.Misc.update
del tkinter.Misc.update_idletasks
import matplotlib.pyplot as plt
fig = plt.figure()
plt.show(block=False)
plt.draw() # Test FigureCanvasTkAgg.
fig.canvas.toolbar.configure_subplots() # Test NavigationToolbar2Tk.
# Test FigureCanvasTk filter_destroy callback
fig.canvas.get_tk_widget().after(100, plt.close, fig)
# Check for update() or update_idletasks() in the event queue, functionally
# equivalent to tkinter.Misc.update.
plt.show(block=True)
# Note that exceptions would be printed to stderr; _isolated_tk_test
# checks them.
@_isolated_tk_test(success_count=2)
def test_missing_back_button():
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
class Toolbar(NavigationToolbar2Tk):
# Only display the buttons we need.
toolitems = [t for t in NavigationToolbar2Tk.toolitems if
t[0] in ('Home', 'Pan', 'Zoom')]
fig = plt.figure()
print("success")
Toolbar(fig.canvas, fig.canvas.manager.window) # This should not raise.
print("success")
@_isolated_tk_test(success_count=2)
def test_save_figure_return():
import matplotlib.pyplot as plt
from unittest import mock
fig = plt.figure()
prop = "tkinter.filedialog.asksaveasfilename"
with mock.patch(prop, return_value="foobar.png"):
fname = fig.canvas.manager.toolbar.save_figure()
os.remove("foobar.png")
assert fname == "foobar.png"
print("success")
with mock.patch(prop, return_value=""):
fname = fig.canvas.manager.toolbar.save_figure()
assert fname is None
print("success")
@_isolated_tk_test(success_count=1)
def test_canvas_focus():
import tkinter as tk
import matplotlib.pyplot as plt
success = []
def check_focus():
tkcanvas = fig.canvas.get_tk_widget()
# Give the plot window time to appear
if not tkcanvas.winfo_viewable():
tkcanvas.wait_visibility()
# Make sure the canvas has the focus, so that it's able to receive
# keyboard events.
if tkcanvas.focus_lastfor() == tkcanvas:
success.append(True)
plt.close()
root.destroy()
root = tk.Tk()
fig = plt.figure()
plt.plot([1, 2, 3])
root.after(0, plt.show)
root.after(100, check_focus)
root.mainloop()
if success:
print("success")
@_isolated_tk_test(success_count=2)
def test_embedding():
import tkinter as tk
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.backend_bases import key_press_handler
from matplotlib.figure import Figure
root = tk.Tk()
def test_figure(master):
fig = Figure()
ax = fig.add_subplot()
ax.plot([1, 2, 3])
canvas = FigureCanvasTkAgg(fig, master=master)
canvas.draw()
canvas.mpl_connect("key_press_event", key_press_handler)
canvas.get_tk_widget().pack(expand=True, fill="both")
toolbar = NavigationToolbar2Tk(canvas, master, pack_toolbar=False)
toolbar.pack(expand=True, fill="x")
canvas.get_tk_widget().forget()
toolbar.forget()
test_figure(root)
print("success")
# Test with a dark button color. Doesn't actually check whether the icon
# color becomes lighter, just that the code doesn't break.
root.tk_setPalette(background="sky blue", selectColor="midnight blue",
foreground="white")
test_figure(root)
print("success")
@@ -0,0 +1,20 @@
import pytest
from matplotlib.backend_tools import ToolHelpBase
@pytest.mark.parametrize('rc_shortcut,expected', [
('home', 'Home'),
('backspace', 'Backspace'),
('f1', 'F1'),
('ctrl+a', 'Ctrl+A'),
('ctrl+A', 'Ctrl+Shift+A'),
('a', 'a'),
('A', 'A'),
('ctrl+shift+f1', 'Ctrl+Shift+F1'),
('1', '1'),
('cmd+p', 'Cmd+P'),
('cmd+1', 'Cmd+1'),
])
def test_format_shortcut(rc_shortcut, expected):
assert ToolHelpBase.format_shortcut(rc_shortcut) == expected
@@ -0,0 +1,32 @@
import os
import sys
import pytest
import matplotlib.backends.backend_webagg_core
from matplotlib.testing import subprocess_run_for_testing
@pytest.mark.parametrize("backend", ["webagg", "nbagg"])
def test_webagg_fallback(backend):
pytest.importorskip("tornado")
if backend == "nbagg":
pytest.importorskip("IPython")
env = dict(os.environ)
if sys.platform != "win32":
env["DISPLAY"] = ""
env["MPLBACKEND"] = backend
test_code = (
"import os;"
+ f"assert os.environ['MPLBACKEND'] == '{backend}';"
+ "import matplotlib.pyplot as plt; "
+ "print(plt.get_backend());"
f"assert '{backend}' == plt.get_backend().lower();"
)
subprocess_run_for_testing([sys.executable, "-c", test_code], env=env, check=True)
def test_webagg_core_no_toolbar():
fm = matplotlib.backends.backend_webagg_core.FigureManagerWebAgg
assert fm._toolbar2_class is None
@@ -0,0 +1,791 @@
import functools
import importlib
import importlib.util
import inspect
import json
import os
import platform
import signal
import subprocess
import sys
import tempfile
import time
import urllib.request
from PIL import Image
import pytest
import matplotlib as mpl
from matplotlib import _c_internal_utils
from matplotlib.backend_tools import ToolToggleBase
from matplotlib.testing import subprocess_run_helper as _run_helper, is_ci_environment
class _WaitForStringPopen(subprocess.Popen):
"""
A Popen that passes flags that allow triggering KeyboardInterrupt.
"""
def __init__(self, *args, **kwargs):
if sys.platform == 'win32':
kwargs['creationflags'] = subprocess.CREATE_NEW_CONSOLE
super().__init__(
*args, **kwargs,
# Force Agg so that each test can switch to its desired backend.
env={**os.environ, "MPLBACKEND": "Agg", "SOURCE_DATE_EPOCH": "0"},
stdout=subprocess.PIPE, universal_newlines=True)
def wait_for(self, terminator):
"""Read until the terminator is reached."""
buf = ''
while True:
c = self.stdout.read(1)
if not c:
raise RuntimeError(
f'Subprocess died before emitting expected {terminator!r}')
buf += c
if buf.endswith(terminator):
return
# Minimal smoke-testing of the backends for which the dependencies are
# PyPI-installable on CI. They are not available for all tested Python
# versions so we don't fail on missing backends.
@functools.lru_cache
def _get_available_interactive_backends():
_is_linux_and_display_invalid = (sys.platform == "linux" and
not _c_internal_utils.display_is_valid())
_is_linux_and_xdisplay_invalid = (sys.platform == "linux" and
not _c_internal_utils.xdisplay_is_valid())
envs = []
for deps, env in [
*[([qt_api],
{"MPLBACKEND": "qtagg", "QT_API": qt_api})
for qt_api in ["PyQt6", "PySide6", "PyQt5", "PySide2"]],
*[([qt_api, "cairocffi"],
{"MPLBACKEND": "qtcairo", "QT_API": qt_api})
for qt_api in ["PyQt6", "PySide6", "PyQt5", "PySide2"]],
*[(["cairo", "gi"], {"MPLBACKEND": f"gtk{version}{renderer}"})
for version in [3, 4] for renderer in ["agg", "cairo"]],
(["tkinter"], {"MPLBACKEND": "tkagg"}),
(["wx"], {"MPLBACKEND": "wx"}),
(["wx"], {"MPLBACKEND": "wxagg"}),
(["matplotlib.backends._macosx"], {"MPLBACKEND": "macosx"}),
]:
reason = None
missing = [dep for dep in deps if not importlib.util.find_spec(dep)]
if missing:
reason = "{} cannot be imported".format(", ".join(missing))
elif _is_linux_and_xdisplay_invalid and (
env["MPLBACKEND"] == "tkagg"
# Remove when https://github.com/wxWidgets/Phoenix/pull/2638 is out.
or env["MPLBACKEND"].startswith("wx")):
reason = "$DISPLAY is unset"
elif _is_linux_and_display_invalid:
reason = "$DISPLAY and $WAYLAND_DISPLAY are unset"
elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'):
reason = "macosx backend fails on Azure"
elif env["MPLBACKEND"].startswith('gtk'):
try:
import gi # type: ignore[import]
except ImportError:
# Though we check that `gi` exists above, it is possible that its
# C-level dependencies are not available, and then it still raises an
# `ImportError`, so guard against that.
available_gtk_versions = []
else:
gi_repo = gi.Repository.get_default()
available_gtk_versions = gi_repo.enumerate_versions('Gtk')
version = env["MPLBACKEND"][3]
if f'{version}.0' not in available_gtk_versions:
reason = "no usable GTK bindings"
marks = []
if reason:
marks.append(pytest.mark.skip(reason=f"Skipping {env} because {reason}"))
elif env["MPLBACKEND"].startswith('wx') and sys.platform == 'darwin':
# ignore on macosx because that's currently broken (github #16849)
marks.append(pytest.mark.xfail(reason='github #16849'))
elif (env['MPLBACKEND'] == 'tkagg' and
('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
sys.platform == 'darwin' and
sys.version_info[:2] < (3, 11)
):
marks.append( # https://github.com/actions/setup-python/issues/649
pytest.mark.xfail(reason='Tk version mismatch on Azure macOS CI'))
envs.append(({**env, 'BACKEND_DEPS': ','.join(deps)}, marks))
return envs
def _get_testable_interactive_backends():
# We re-create this because some of the callers below might modify the markers.
return [pytest.param({**env}, marks=[*marks],
id='-'.join(f'{k}={v}' for k, v in env.items()))
for env, marks in _get_available_interactive_backends()]
# Reasonable safe values for slower CI/Remote and local architectures.
_test_timeout = 120 if is_ci_environment() else 20
def _test_toolbar_button_la_mode_icon(fig):
# test a toolbar button icon using an image in LA mode (GH issue 25174)
# create an icon in LA mode
with tempfile.TemporaryDirectory() as tempdir:
img = Image.new("LA", (26, 26))
tmp_img_path = os.path.join(tempdir, "test_la_icon.png")
img.save(tmp_img_path)
class CustomTool(ToolToggleBase):
image = tmp_img_path
description = "" # gtk3 backend does not allow None
toolmanager = fig.canvas.manager.toolmanager
toolbar = fig.canvas.manager.toolbar
toolmanager.add_tool("test", CustomTool)
toolbar.add_tool("test", "group")
# The source of this function gets extracted and run in another process, so it
# must be fully self-contained.
# Using a timer not only allows testing of timers (on other backends), but is
# also necessary on gtk3 and wx, where directly processing a KeyEvent() for "q"
# from draw_event causes breakage as the canvas widget gets deleted too early.
def _test_interactive_impl():
import importlib.util
import io
import json
import sys
import pytest
import matplotlib as mpl
from matplotlib import pyplot as plt
from matplotlib.backend_bases import KeyEvent
mpl.rcParams.update({
"webagg.open_in_browser": False,
"webagg.port_retries": 1,
})
mpl.rcParams.update(json.loads(sys.argv[1]))
backend = plt.rcParams["backend"].lower()
if backend.endswith("agg") and not backend.startswith(("gtk", "web")):
# Force interactive framework setup.
fig = plt.figure()
plt.close(fig)
# Check that we cannot switch to a backend using another interactive
# framework, but can switch to a backend using cairo instead of agg,
# or a non-interactive backend. In the first case, we use tkagg as
# the "other" interactive backend as it is (essentially) guaranteed
# to be present. Moreover, don't test switching away from gtk3 (as
# Gtk.main_level() is not set up at this point yet) and webagg (which
# uses no interactive framework).
if backend != "tkagg":
with pytest.raises(ImportError):
mpl.use("tkagg", force=True)
def check_alt_backend(alt_backend):
mpl.use(alt_backend, force=True)
fig = plt.figure()
assert (type(fig.canvas).__module__ ==
f"matplotlib.backends.backend_{alt_backend}")
plt.close("all")
if importlib.util.find_spec("cairocffi"):
check_alt_backend(backend[:-3] + "cairo")
check_alt_backend("svg")
mpl.use(backend, force=True)
fig, ax = plt.subplots()
assert type(fig.canvas).__module__ == f"matplotlib.backends.backend_{backend}"
assert fig.canvas.manager.get_window_title() == "Figure 1"
if mpl.rcParams["toolbar"] == "toolmanager":
# test toolbar button icon LA mode see GH issue 25174
_test_toolbar_button_la_mode_icon(fig)
ax.plot([0, 1], [2, 3])
if fig.canvas.toolbar: # i.e toolbar2.
fig.canvas.toolbar.draw_rubberband(None, 1., 1, 2., 2)
timer = fig.canvas.new_timer(1.) # Test that floats are cast to int.
timer.add_callback(KeyEvent("key_press_event", fig.canvas, "q")._process)
# Trigger quitting upon draw.
fig.canvas.mpl_connect("draw_event", lambda event: timer.start())
fig.canvas.mpl_connect("close_event", print)
result = io.BytesIO()
fig.savefig(result, format='png')
plt.show()
# Ensure that the window is really closed.
plt.pause(0.5)
# Test that saving works after interactive window is closed, but the figure
# is not deleted.
result_after = io.BytesIO()
fig.savefig(result_after, format='png')
assert result.getvalue() == result_after.getvalue()
@pytest.mark.parametrize("env", _get_testable_interactive_backends())
@pytest.mark.parametrize("toolbar", ["toolbar2", "toolmanager"])
@pytest.mark.flaky(reruns=3)
def test_interactive_backend(env, toolbar):
if env["MPLBACKEND"] == "macosx":
if toolbar == "toolmanager":
pytest.skip("toolmanager is not implemented for macosx.")
if env["MPLBACKEND"] == "wx":
pytest.skip("wx backend is deprecated; tests failed on appveyor")
if env["MPLBACKEND"] == "wxagg" and toolbar == "toolmanager":
pytest.skip("Temporarily deactivated: show() changes figure height "
"and thus fails the test")
try:
proc = _run_helper(
_test_interactive_impl,
json.dumps({"toolbar": toolbar}),
timeout=_test_timeout,
extra_env=env,
)
except subprocess.CalledProcessError as err:
pytest.fail(
"Subprocess failed to test intended behavior\n"
+ str(err.stderr))
assert proc.stdout.count("CloseEvent") == 1
def _test_thread_impl():
from concurrent.futures import ThreadPoolExecutor
import matplotlib as mpl
from matplotlib import pyplot as plt
mpl.rcParams.update({
"webagg.open_in_browser": False,
"webagg.port_retries": 1,
})
# Test artist creation and drawing does not crash from thread
# No other guarantees!
fig, ax = plt.subplots()
# plt.pause needed vs plt.show(block=False) at least on toolbar2-tkagg
plt.pause(0.5)
future = ThreadPoolExecutor().submit(ax.plot, [1, 3, 6])
future.result() # Joins the thread; rethrows any exception.
fig.canvas.mpl_connect("close_event", print)
future = ThreadPoolExecutor().submit(fig.canvas.draw)
plt.pause(0.5) # flush_events fails here on at least Tkagg (bpo-41176)
future.result() # Joins the thread; rethrows any exception.
plt.close() # backend is responsible for flushing any events here
if plt.rcParams["backend"].lower().startswith("wx"):
# TODO: debug why WX needs this only on py >= 3.8
fig.canvas.flush_events()
_thread_safe_backends = _get_testable_interactive_backends()
# Known unsafe backends. Remove the xfails if they start to pass!
for param in _thread_safe_backends:
backend = param.values[0]["MPLBACKEND"]
if "cairo" in backend:
# Cairo backends save a cairo_t on the graphics context, and sharing
# these is not threadsafe.
param.marks.append(
pytest.mark.xfail(raises=subprocess.CalledProcessError))
elif backend == "wx":
param.marks.append(
pytest.mark.xfail(raises=subprocess.CalledProcessError))
elif backend == "macosx":
from packaging.version import parse
mac_ver = platform.mac_ver()[0]
# Note, macOS Big Sur is both 11 and 10.16, depending on SDK that
# Python was compiled against.
if mac_ver and parse(mac_ver) < parse('10.16'):
param.marks.append(
pytest.mark.xfail(raises=subprocess.TimeoutExpired,
strict=True))
elif param.values[0].get("QT_API") == "PySide2":
param.marks.append(
pytest.mark.xfail(raises=subprocess.CalledProcessError))
elif backend == "tkagg" and platform.python_implementation() != 'CPython':
param.marks.append(
pytest.mark.xfail(
reason='PyPy does not support Tkinter threading: '
'https://foss.heptapod.net/pypy/pypy/-/issues/1929',
strict=True))
elif (backend == 'tkagg' and
('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
sys.platform == 'darwin' and sys.version_info[:2] < (3, 11)):
param.marks.append( # https://github.com/actions/setup-python/issues/649
pytest.mark.xfail('Tk version mismatch on Azure macOS CI'))
@pytest.mark.parametrize("env", _thread_safe_backends)
@pytest.mark.flaky(reruns=3)
def test_interactive_thread_safety(env):
proc = _run_helper(_test_thread_impl, timeout=_test_timeout, extra_env=env)
assert proc.stdout.count("CloseEvent") == 1
def _impl_test_lazy_auto_backend_selection():
import matplotlib
import matplotlib.pyplot as plt
# just importing pyplot should not be enough to trigger resolution
bk = matplotlib.rcParams._get('backend')
assert not isinstance(bk, str)
assert plt._backend_mod is None
# but actually plotting should
plt.plot(5)
assert plt._backend_mod is not None
bk = matplotlib.rcParams._get('backend')
assert isinstance(bk, str)
def test_lazy_auto_backend_selection():
_run_helper(_impl_test_lazy_auto_backend_selection,
timeout=_test_timeout)
def _implqt5agg():
import matplotlib.backends.backend_qt5agg # noqa
import sys
assert 'PyQt6' not in sys.modules
assert 'pyside6' not in sys.modules
assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
def _implcairo():
import matplotlib.backends.backend_qt5cairo # noqa
import sys
assert 'PyQt6' not in sys.modules
assert 'pyside6' not in sys.modules
assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
def _implcore():
import matplotlib.backends.backend_qt5 # noqa
import sys
assert 'PyQt6' not in sys.modules
assert 'pyside6' not in sys.modules
assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
def test_qt5backends_uses_qt5():
qt5_bindings = [
dep for dep in ['PyQt5', 'pyside2']
if importlib.util.find_spec(dep) is not None
]
qt6_bindings = [
dep for dep in ['PyQt6', 'pyside6']
if importlib.util.find_spec(dep) is not None
]
if len(qt5_bindings) == 0 or len(qt6_bindings) == 0:
pytest.skip('need both QT6 and QT5 bindings')
_run_helper(_implqt5agg, timeout=_test_timeout)
if importlib.util.find_spec('pycairo') is not None:
_run_helper(_implcairo, timeout=_test_timeout)
_run_helper(_implcore, timeout=_test_timeout)
def _impl_missing():
import sys
# Simulate uninstalled
sys.modules["PyQt6"] = None
sys.modules["PyQt5"] = None
sys.modules["PySide2"] = None
sys.modules["PySide6"] = None
import matplotlib.pyplot as plt
with pytest.raises(ImportError, match="Failed to import any of the following Qt"):
plt.switch_backend("qtagg")
# Specifically ensure that Pyside6/Pyqt6 are not in the error message for qt5agg
with pytest.raises(ImportError, match="^(?:(?!(PySide6|PyQt6)).)*$"):
plt.switch_backend("qt5agg")
def test_qt_missing():
_run_helper(_impl_missing, timeout=_test_timeout)
def _impl_test_cross_Qt_imports():
import importlib
import sys
import warnings
_, host_binding, mpl_binding = sys.argv
# import the mpl binding. This will force us to use that binding
importlib.import_module(f'{mpl_binding}.QtCore')
mpl_binding_qwidgets = importlib.import_module(f'{mpl_binding}.QtWidgets')
import matplotlib.backends.backend_qt
host_qwidgets = importlib.import_module(f'{host_binding}.QtWidgets')
host_app = host_qwidgets.QApplication(["mpl testing"])
warnings.filterwarnings("error", message=r".*Mixing Qt major.*",
category=UserWarning)
matplotlib.backends.backend_qt._create_qApp()
def qt5_and_qt6_pairs():
qt5_bindings = [
dep for dep in ['PyQt5', 'PySide2']
if importlib.util.find_spec(dep) is not None
]
qt6_bindings = [
dep for dep in ['PyQt6', 'PySide6']
if importlib.util.find_spec(dep) is not None
]
if len(qt5_bindings) == 0 or len(qt6_bindings) == 0:
yield pytest.param(None, None,
marks=[pytest.mark.skip('need both QT6 and QT5 bindings')])
return
for qt5 in qt5_bindings:
for qt6 in qt6_bindings:
yield from ([qt5, qt6], [qt6, qt5])
@pytest.mark.skipif(
sys.platform == "linux" and not _c_internal_utils.display_is_valid(),
reason="$DISPLAY and $WAYLAND_DISPLAY are unset")
@pytest.mark.parametrize('host, mpl', [*qt5_and_qt6_pairs()])
def test_cross_Qt_imports(host, mpl):
try:
proc = _run_helper(_impl_test_cross_Qt_imports, host, mpl,
timeout=_test_timeout)
except subprocess.CalledProcessError as ex:
# We do try to warn the user they are doing something that we do not
# expect to work, so we're going to ignore if the subprocess crashes or
# is killed, and just check that the warning is printed.
stderr = ex.stderr
else:
stderr = proc.stderr
assert "Mixing Qt major versions may not work as expected." in stderr
@pytest.mark.skipif('TF_BUILD' in os.environ,
reason="this test fails an azure for unknown reasons")
@pytest.mark.skipif(sys.platform == "win32", reason="Cannot send SIGINT on Windows.")
def test_webagg():
pytest.importorskip("tornado")
proc = subprocess.Popen(
[sys.executable, "-c",
inspect.getsource(_test_interactive_impl)
+ "\n_test_interactive_impl()", "{}"],
env={**os.environ, "MPLBACKEND": "webagg", "SOURCE_DATE_EPOCH": "0"})
url = f'http://{mpl.rcParams["webagg.address"]}:{mpl.rcParams["webagg.port"]}'
timeout = time.perf_counter() + _test_timeout
try:
while True:
try:
retcode = proc.poll()
# check that the subprocess for the server is not dead
assert retcode is None
conn = urllib.request.urlopen(url)
break
except urllib.error.URLError:
if time.perf_counter() > timeout:
pytest.fail("Failed to connect to the webagg server.")
else:
continue
conn.close()
proc.send_signal(signal.SIGINT)
assert proc.wait(timeout=_test_timeout) == 0
finally:
if proc.poll() is None:
proc.kill()
def _lazy_headless():
import os
import sys
backend, deps = sys.argv[1:]
deps = deps.split(',')
# make it look headless
os.environ.pop('DISPLAY', None)
os.environ.pop('WAYLAND_DISPLAY', None)
for dep in deps:
assert dep not in sys.modules
# we should fast-track to Agg
import matplotlib.pyplot as plt
assert plt.get_backend() == 'agg'
for dep in deps:
assert dep not in sys.modules
# make sure we really have dependencies installed
for dep in deps:
importlib.import_module(dep)
assert dep in sys.modules
# try to switch and make sure we fail with ImportError
try:
plt.switch_backend(backend)
except ImportError:
pass
else:
sys.exit(1)
@pytest.mark.skipif(sys.platform != "linux", reason="this a linux-only test")
@pytest.mark.parametrize("env", _get_testable_interactive_backends())
def test_lazy_linux_headless(env):
proc = _run_helper(
_lazy_headless,
env.pop('MPLBACKEND'), env.pop("BACKEND_DEPS"),
timeout=_test_timeout,
extra_env={**env, 'DISPLAY': '', 'WAYLAND_DISPLAY': ''}
)
def _test_number_of_draws_script():
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
# animated=True tells matplotlib to only draw the artist when we
# explicitly request it
ln, = ax.plot([0, 1], [1, 2], animated=True)
# make sure the window is raised, but the script keeps going
plt.show(block=False)
plt.pause(0.3)
# Connect to draw_event to count the occurrences
fig.canvas.mpl_connect('draw_event', print)
# get copy of entire figure (everything inside fig.bbox)
# sans animated artist
bg = fig.canvas.copy_from_bbox(fig.bbox)
# draw the animated artist, this uses a cached renderer
ax.draw_artist(ln)
# show the result to the screen
fig.canvas.blit(fig.bbox)
for j in range(10):
# reset the background back in the canvas state, screen unchanged
fig.canvas.restore_region(bg)
# Create a **new** artist here, this is poor usage of blitting
# but good for testing to make sure that this doesn't create
# excessive draws
ln, = ax.plot([0, 1], [1, 2])
# render the artist, updating the canvas state, but not the screen
ax.draw_artist(ln)
# copy the image to the GUI state, but screen might not changed yet
fig.canvas.blit(fig.bbox)
# flush any pending GUI events, re-painting the screen if needed
fig.canvas.flush_events()
# Let the event loop process everything before leaving
plt.pause(0.1)
_blit_backends = _get_testable_interactive_backends()
for param in _blit_backends:
backend = param.values[0]["MPLBACKEND"]
if backend == "gtk3cairo":
# copy_from_bbox only works when rendering to an ImageSurface
param.marks.append(
pytest.mark.skip("gtk3cairo does not support blitting"))
elif backend == "gtk4cairo":
# copy_from_bbox only works when rendering to an ImageSurface
param.marks.append(
pytest.mark.skip("gtk4cairo does not support blitting"))
elif backend == "wx":
param.marks.append(
pytest.mark.skip("wx does not support blitting"))
elif (backend == 'tkagg' and
('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
sys.platform == 'darwin' and
sys.version_info[:2] < (3, 11)
):
param.marks.append( # https://github.com/actions/setup-python/issues/649
pytest.mark.xfail('Tk version mismatch on Azure macOS CI')
)
@pytest.mark.parametrize("env", _blit_backends)
# subprocesses can struggle to get the display, so rerun a few times
@pytest.mark.flaky(reruns=4)
def test_blitting_events(env):
proc = _run_helper(
_test_number_of_draws_script, timeout=_test_timeout, extra_env=env)
# Count the number of draw_events we got. We could count some initial
# canvas draws (which vary in number by backend), but the critical
# check here is that it isn't 10 draws, which would be called if
# blitting is not properly implemented
ndraws = proc.stdout.count("DrawEvent")
assert 0 < ndraws < 5
def _impl_test_interactive_timers():
# A timer with <1 millisecond gets converted to int and therefore 0
# milliseconds, which the mac framework interprets as singleshot.
# We only want singleshot if we specify that ourselves, otherwise we want
# a repeating timer
from unittest.mock import Mock
import matplotlib.pyplot as plt
pause_time = 0.5
fig = plt.figure()
plt.pause(pause_time)
timer = fig.canvas.new_timer(0.1)
mock = Mock()
timer.add_callback(mock)
timer.start()
plt.pause(pause_time)
timer.stop()
assert mock.call_count > 1
# Now turn it into a single shot timer and verify only one gets triggered
mock.call_count = 0
timer.single_shot = True
timer.start()
plt.pause(pause_time)
assert mock.call_count == 1
# Make sure we can start the timer a second time
timer.start()
plt.pause(pause_time)
assert mock.call_count == 2
plt.close("all")
@pytest.mark.parametrize("env", _get_testable_interactive_backends())
def test_interactive_timers(env):
if env["MPLBACKEND"] == "gtk3cairo" and os.getenv("CI"):
pytest.skip("gtk3cairo timers do not work in remote CI")
if env["MPLBACKEND"] == "wx":
pytest.skip("wx backend is deprecated; tests failed on appveyor")
_run_helper(_impl_test_interactive_timers,
timeout=_test_timeout, extra_env=env)
def _test_sigint_impl(backend, target_name, kwargs):
import sys
import matplotlib.pyplot as plt
import os
import threading
plt.switch_backend(backend)
def interrupter():
if sys.platform == 'win32':
import win32api
win32api.GenerateConsoleCtrlEvent(0, 0)
else:
import signal
os.kill(os.getpid(), signal.SIGINT)
target = getattr(plt, target_name)
timer = threading.Timer(1, interrupter)
fig = plt.figure()
fig.canvas.mpl_connect(
'draw_event',
lambda *args: print('DRAW', flush=True)
)
fig.canvas.mpl_connect(
'draw_event',
lambda *args: timer.start()
)
try:
target(**kwargs)
except KeyboardInterrupt:
print('SUCCESS', flush=True)
@pytest.mark.parametrize("env", _get_testable_interactive_backends())
@pytest.mark.parametrize("target, kwargs", [
('show', {'block': True}),
('pause', {'interval': 10})
])
def test_sigint(env, target, kwargs):
backend = env.get("MPLBACKEND")
if not backend.startswith(("qt", "macosx")):
pytest.skip("SIGINT currently only tested on qt and macosx")
proc = _WaitForStringPopen(
[sys.executable, "-c",
inspect.getsource(_test_sigint_impl) +
f"\n_test_sigint_impl({backend!r}, {target!r}, {kwargs!r})"])
try:
proc.wait_for('DRAW')
stdout, _ = proc.communicate(timeout=_test_timeout)
except Exception:
proc.kill()
stdout, _ = proc.communicate()
raise
assert 'SUCCESS' in stdout
def _test_other_signal_before_sigint_impl(backend, target_name, kwargs):
import signal
import matplotlib.pyplot as plt
plt.switch_backend(backend)
target = getattr(plt, target_name)
fig = plt.figure()
fig.canvas.mpl_connect('draw_event', lambda *args: print('DRAW', flush=True))
timer = fig.canvas.new_timer(interval=1)
timer.single_shot = True
timer.add_callback(print, 'SIGUSR1', flush=True)
def custom_signal_handler(signum, frame):
timer.start()
signal.signal(signal.SIGUSR1, custom_signal_handler)
try:
target(**kwargs)
except KeyboardInterrupt:
print('SUCCESS', flush=True)
@pytest.mark.skipif(sys.platform == 'win32',
reason='No other signal available to send on Windows')
@pytest.mark.parametrize("env", _get_testable_interactive_backends())
@pytest.mark.parametrize("target, kwargs", [
('show', {'block': True}),
('pause', {'interval': 10})
])
def test_other_signal_before_sigint(env, target, kwargs, request):
backend = env.get("MPLBACKEND")
if not backend.startswith(("qt", "macosx")):
pytest.skip("SIGINT currently only tested on qt and macosx")
if backend == "macosx":
request.node.add_marker(pytest.mark.xfail(reason="macosx backend is buggy"))
if sys.platform == "darwin" and target == "show":
# We've not previously had these toolkits installed on CI, and so were never
# aware that this was crashing. However, we've had little luck reproducing it
# locally, so mark it xfail for now. For more information, see
# https://github.com/matplotlib/matplotlib/issues/27984
request.node.add_marker(
pytest.mark.xfail(reason="Qt backend is buggy on macOS"))
proc = _WaitForStringPopen(
[sys.executable, "-c",
inspect.getsource(_test_other_signal_before_sigint_impl) +
"\n_test_other_signal_before_sigint_impl("
f"{backend!r}, {target!r}, {kwargs!r})"])
try:
proc.wait_for('DRAW')
os.kill(proc.pid, signal.SIGUSR1)
proc.wait_for('SIGUSR1')
os.kill(proc.pid, signal.SIGINT)
stdout, _ = proc.communicate(timeout=_test_timeout)
except Exception:
proc.kill()
stdout, _ = proc.communicate()
raise
print(stdout)
assert 'SUCCESS' in stdout
@@ -0,0 +1,48 @@
import builtins
import os
import sys
import textwrap
from matplotlib.testing import subprocess_run_for_testing
def test_simple():
assert 1 + 1 == 2
def test_override_builtins():
import pylab # type: ignore[import]
ok_to_override = {
'__name__',
'__doc__',
'__package__',
'__loader__',
'__spec__',
'any',
'all',
'sum',
'divmod'
}
overridden = {key for key in {*dir(pylab)} & {*dir(builtins)}
if getattr(pylab, key) != getattr(builtins, key)}
assert overridden <= ok_to_override
def test_lazy_imports():
source = textwrap.dedent("""
import sys
import matplotlib.figure
import matplotlib.backend_bases
import matplotlib.pyplot
assert 'matplotlib._tri' not in sys.modules
assert 'matplotlib._qhull' not in sys.modules
assert 'matplotlib._contour' not in sys.modules
assert 'urllib.request' not in sys.modules
""")
subprocess_run_for_testing(
[sys.executable, '-c', source],
env={**os.environ, "MPLBACKEND": "", "MATPLOTLIBRC": os.devnull},
check=True)
@@ -0,0 +1,175 @@
from io import BytesIO
import platform
import numpy as np
from matplotlib.testing.decorators import image_comparison
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import matplotlib.patches as mpatches
from matplotlib.ticker import FuncFormatter
@image_comparison(['bbox_inches_tight'], remove_text=True,
savefig_kwarg={'bbox_inches': 'tight'})
def test_bbox_inches_tight():
#: Test that a figure saved using bbox_inches='tight' is clipped correctly
data = [[66386, 174296, 75131, 577908, 32015],
[58230, 381139, 78045, 99308, 160454],
[89135, 80552, 152558, 497981, 603535],
[78415, 81858, 150656, 193263, 69638],
[139361, 331509, 343164, 781380, 52269]]
col_labels = row_labels = [''] * 5
rows = len(data)
ind = np.arange(len(col_labels)) + 0.3 # the x locations for the groups
cell_text = []
width = 0.4 # the width of the bars
yoff = np.zeros(len(col_labels))
# the bottom values for stacked bar chart
fig, ax = plt.subplots(1, 1)
for row in range(rows):
ax.bar(ind, data[row], width, bottom=yoff, align='edge', color='b')
yoff = yoff + data[row]
cell_text.append([''])
plt.xticks([])
plt.xlim(0, 5)
plt.legend([''] * 5, loc=(1.2, 0.2))
fig.legend([''] * 5, bbox_to_anchor=(0, 0.2), loc='lower left')
# Add a table at the bottom of the axes
cell_text.reverse()
plt.table(cellText=cell_text, rowLabels=row_labels, colLabels=col_labels,
loc='bottom')
@image_comparison(['bbox_inches_tight_suptile_legend'],
savefig_kwarg={'bbox_inches': 'tight'},
tol=0 if platform.machine() == 'x86_64' else 0.02)
def test_bbox_inches_tight_suptile_legend():
plt.plot(np.arange(10), label='a straight line')
plt.legend(bbox_to_anchor=(0.9, 1), loc='upper left')
plt.title('Axis title')
plt.suptitle('Figure title')
# put an extra long y tick on to see that the bbox is accounted for
def y_formatter(y, pos):
if int(y) == 4:
return 'The number 4'
else:
return str(y)
plt.gca().yaxis.set_major_formatter(FuncFormatter(y_formatter))
plt.xlabel('X axis')
@image_comparison(['bbox_inches_tight_suptile_non_default.png'],
savefig_kwarg={'bbox_inches': 'tight'},
tol=0.1) # large tolerance because only testing clipping.
def test_bbox_inches_tight_suptitle_non_default():
fig, ax = plt.subplots()
fig.suptitle('Booo', x=0.5, y=1.1)
@image_comparison(['bbox_inches_tight_layout.png'], remove_text=True,
style='mpl20',
savefig_kwarg=dict(bbox_inches='tight', pad_inches='layout'))
def test_bbox_inches_tight_layout_constrained():
fig, ax = plt.subplots(layout='constrained')
fig.get_layout_engine().set(h_pad=0.5)
ax.set_aspect('equal')
def test_bbox_inches_tight_layout_notconstrained(tmp_path):
# pad_inches='layout' should be ignored when not using constrained/
# compressed layout. Smoke test that savefig doesn't error in this case.
fig, ax = plt.subplots()
fig.savefig(tmp_path / 'foo.png', bbox_inches='tight', pad_inches='layout')
@image_comparison(['bbox_inches_tight_clipping'],
remove_text=True, savefig_kwarg={'bbox_inches': 'tight'})
def test_bbox_inches_tight_clipping():
# tests bbox clipping on scatter points, and path clipping on a patch
# to generate an appropriately tight bbox
plt.scatter(np.arange(10), np.arange(10))
ax = plt.gca()
ax.set_xlim(0, 5)
ax.set_ylim(0, 5)
# make a massive rectangle and clip it with a path
patch = mpatches.Rectangle([-50, -50], 100, 100,
transform=ax.transData,
facecolor='blue', alpha=0.5)
path = mpath.Path.unit_regular_star(5).deepcopy()
path.vertices *= 0.25
patch.set_clip_path(path, transform=ax.transAxes)
plt.gcf().artists.append(patch)
@image_comparison(['bbox_inches_tight_raster'],
remove_text=True, savefig_kwarg={'bbox_inches': 'tight'})
def test_bbox_inches_tight_raster():
"""Test rasterization with tight_layout"""
fig, ax = plt.subplots()
ax.plot([1.0, 2.0], rasterized=True)
def test_only_on_non_finite_bbox():
fig, ax = plt.subplots()
ax.annotate("", xy=(0, float('nan')))
ax.set_axis_off()
# we only need to test that it does not error out on save
fig.savefig(BytesIO(), bbox_inches='tight', format='png')
def test_tight_pcolorfast():
fig, ax = plt.subplots()
ax.pcolorfast(np.arange(4).reshape((2, 2)))
ax.set(ylim=(0, .1))
buf = BytesIO()
fig.savefig(buf, bbox_inches="tight")
buf.seek(0)
height, width, _ = plt.imread(buf).shape
# Previously, the bbox would include the area of the image clipped out by
# the axes, resulting in a very tall image given the y limits of (0, 0.1).
assert width > height
def test_noop_tight_bbox():
from PIL import Image
x_size, y_size = (10, 7)
dpi = 100
# make the figure just the right size up front
fig = plt.figure(frameon=False, dpi=dpi, figsize=(x_size/dpi, y_size/dpi))
ax = fig.add_axes((0, 0, 1, 1))
ax.set_axis_off()
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
data = np.arange(x_size * y_size).reshape(y_size, x_size)
ax.imshow(data, rasterized=True)
# When a rasterized Artist is included, a mixed-mode renderer does
# additional bbox adjustment. It should also be a no-op, and not affect the
# next save.
fig.savefig(BytesIO(), bbox_inches='tight', pad_inches=0, format='pdf')
out = BytesIO()
fig.savefig(out, bbox_inches='tight', pad_inches=0)
out.seek(0)
im = np.asarray(Image.open(out))
assert (im[:, :, 3] == 255).all()
assert not (im[:, :, :3] == 255).all()
assert im.shape == (7, 10, 4)
@image_comparison(['bbox_inches_fixed_aspect'], extensions=['png'],
remove_text=True, savefig_kwarg={'bbox_inches': 'tight'})
def test_bbox_inches_fixed_aspect():
with plt.rc_context({'figure.constrained_layout.use': True}):
fig, ax = plt.subplots()
ax.plot([0, 1])
ax.set_xlim(0, 1)
ax.set_aspect('equal')
@@ -0,0 +1,17 @@
"""
Tests specific to the bezier module.
"""
from matplotlib.bezier import inside_circle, split_bezier_intersecting_with_closedpath
def test_split_bezier_with_large_values():
# These numbers come from gh-27753
arrow_path = [(96950809781500.0, 804.7503795623779),
(96950809781500.0, 859.6242585800646),
(96950809781500.0, 914.4981375977513)]
in_f = inside_circle(96950809781500.0, 804.7503795623779, 0.06)
split_bezier_intersecting_with_closedpath(arrow_path, in_f)
# All we are testing is that this completes
# The failure case is an infinite loop resulting from floating point precision
# pytest will timeout if that occurs
@@ -0,0 +1,331 @@
"""Catch all for categorical functions"""
import warnings
import pytest
import numpy as np
import matplotlib as mpl
from matplotlib.axes import Axes
import matplotlib.pyplot as plt
import matplotlib.category as cat
from matplotlib.testing.decorators import check_figures_equal
class TestUnitData:
test_cases = [('single', (["hello world"], [0])),
('unicode', (["Здравствуйте мир"], [0])),
('mixed', (['A', "np.nan", 'B', "3.14", "мир"],
[0, 1, 2, 3, 4]))]
ids, data = zip(*test_cases)
@pytest.mark.parametrize("data, locs", data, ids=ids)
def test_unit(self, data, locs):
unit = cat.UnitData(data)
assert list(unit._mapping.keys()) == data
assert list(unit._mapping.values()) == locs
def test_update(self):
data = ['a', 'd']
locs = [0, 1]
data_update = ['b', 'd', 'e']
unique_data = ['a', 'd', 'b', 'e']
updated_locs = [0, 1, 2, 3]
unit = cat.UnitData(data)
assert list(unit._mapping.keys()) == data
assert list(unit._mapping.values()) == locs
unit.update(data_update)
assert list(unit._mapping.keys()) == unique_data
assert list(unit._mapping.values()) == updated_locs
failing_test_cases = [("number", 3.14), ("nan", np.nan),
("list", [3.14, 12]), ("mixed type", ["A", 2])]
fids, fdata = zip(*test_cases)
@pytest.mark.parametrize("fdata", fdata, ids=fids)
def test_non_string_fails(self, fdata):
with pytest.raises(TypeError):
cat.UnitData(fdata)
@pytest.mark.parametrize("fdata", fdata, ids=fids)
def test_non_string_update_fails(self, fdata):
unitdata = cat.UnitData()
with pytest.raises(TypeError):
unitdata.update(fdata)
class FakeAxis:
def __init__(self, units):
self.units = units
class TestStrCategoryConverter:
"""
Based on the pandas conversion and factorization tests:
ref: /pandas/tseries/tests/test_converter.py
/pandas/tests/test_algos.py:TestFactorize
"""
test_cases = [("unicode", ["Здравствуйте мир"]),
("ascii", ["hello world"]),
("single", ['a', 'b', 'c']),
("integer string", ["1", "2"]),
("single + values>10", ["A", "B", "C", "D", "E", "F", "G",
"H", "I", "J", "K", "L", "M", "N",
"O", "P", "Q", "R", "S", "T", "U",
"V", "W", "X", "Y", "Z"])]
ids, values = zip(*test_cases)
failing_test_cases = [("mixed", [3.14, 'A', np.inf]),
("string integer", ['42', 42])]
fids, fvalues = zip(*failing_test_cases)
@pytest.fixture(autouse=True)
def mock_axis(self, request):
self.cc = cat.StrCategoryConverter()
# self.unit should be probably be replaced with real mock unit
self.unit = cat.UnitData()
self.ax = FakeAxis(self.unit)
@pytest.mark.parametrize("vals", values, ids=ids)
def test_convert(self, vals):
np.testing.assert_allclose(self.cc.convert(vals, self.ax.units,
self.ax),
range(len(vals)))
@pytest.mark.parametrize("value", ["hi", "мир"], ids=["ascii", "unicode"])
def test_convert_one_string(self, value):
assert self.cc.convert(value, self.unit, self.ax) == 0
@pytest.mark.parametrize("fvals", fvalues, ids=fids)
def test_convert_fail(self, fvals):
with pytest.raises(TypeError):
self.cc.convert(fvals, self.unit, self.ax)
def test_axisinfo(self):
axis = self.cc.axisinfo(self.unit, self.ax)
assert isinstance(axis.majloc, cat.StrCategoryLocator)
assert isinstance(axis.majfmt, cat.StrCategoryFormatter)
def test_default_units(self):
assert isinstance(self.cc.default_units(["a"], self.ax), cat.UnitData)
PLOT_LIST = [Axes.scatter, Axes.plot, Axes.bar]
PLOT_IDS = ["scatter", "plot", "bar"]
class TestStrCategoryLocator:
def test_StrCategoryLocator(self):
locs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
unit = cat.UnitData([str(j) for j in locs])
ticks = cat.StrCategoryLocator(unit._mapping)
np.testing.assert_array_equal(ticks.tick_values(None, None), locs)
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_StrCategoryLocatorPlot(self, plotter):
ax = plt.figure().subplots()
plotter(ax, [1, 2, 3], ["a", "b", "c"])
np.testing.assert_array_equal(ax.yaxis.major.locator(), range(3))
class TestStrCategoryFormatter:
test_cases = [("ascii", ["hello", "world", "hi"]),
("unicode", ["Здравствуйте", "привет"])]
ids, cases = zip(*test_cases)
@pytest.mark.parametrize("ydata", cases, ids=ids)
def test_StrCategoryFormatter(self, ydata):
unit = cat.UnitData(ydata)
labels = cat.StrCategoryFormatter(unit._mapping)
for i, d in enumerate(ydata):
assert labels(i, i) == d
assert labels(i, None) == d
@pytest.mark.parametrize("ydata", cases, ids=ids)
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_StrCategoryFormatterPlot(self, ydata, plotter):
ax = plt.figure().subplots()
plotter(ax, range(len(ydata)), ydata)
for i, d in enumerate(ydata):
assert ax.yaxis.major.formatter(i) == d
assert ax.yaxis.major.formatter(i+1) == ""
def axis_test(axis, labels):
ticks = list(range(len(labels)))
np.testing.assert_array_equal(axis.get_majorticklocs(), ticks)
graph_labels = [axis.major.formatter(i, i) for i in ticks]
# _text also decodes bytes as utf-8.
assert graph_labels == [cat.StrCategoryFormatter._text(l) for l in labels]
assert list(axis.units._mapping.keys()) == [l for l in labels]
assert list(axis.units._mapping.values()) == ticks
class TestPlotBytes:
bytes_cases = [('string list', ['a', 'b', 'c']),
('bytes list', [b'a', b'b', b'c']),
('bytes ndarray', np.array([b'a', b'b', b'c']))]
bytes_ids, bytes_data = zip(*bytes_cases)
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
@pytest.mark.parametrize("bdata", bytes_data, ids=bytes_ids)
def test_plot_bytes(self, plotter, bdata):
ax = plt.figure().subplots()
counts = np.array([4, 6, 5])
plotter(ax, bdata, counts)
axis_test(ax.xaxis, bdata)
class TestPlotNumlike:
numlike_cases = [('string list', ['1', '11', '3']),
('string ndarray', np.array(['1', '11', '3'])),
('bytes list', [b'1', b'11', b'3']),
('bytes ndarray', np.array([b'1', b'11', b'3']))]
numlike_ids, numlike_data = zip(*numlike_cases)
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
@pytest.mark.parametrize("ndata", numlike_data, ids=numlike_ids)
def test_plot_numlike(self, plotter, ndata):
ax = plt.figure().subplots()
counts = np.array([4, 6, 5])
plotter(ax, ndata, counts)
axis_test(ax.xaxis, ndata)
class TestPlotTypes:
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_plot_unicode(self, plotter):
ax = plt.figure().subplots()
words = ['Здравствуйте', 'привет']
plotter(ax, words, [0, 1])
axis_test(ax.xaxis, words)
@pytest.fixture
def test_data(self):
self.x = ["hello", "happy", "world"]
self.xy = [2, 6, 3]
self.y = ["Python", "is", "fun"]
self.yx = [3, 4, 5]
@pytest.mark.usefixtures("test_data")
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_plot_xaxis(self, test_data, plotter):
ax = plt.figure().subplots()
plotter(ax, self.x, self.xy)
axis_test(ax.xaxis, self.x)
@pytest.mark.usefixtures("test_data")
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_plot_yaxis(self, test_data, plotter):
ax = plt.figure().subplots()
plotter(ax, self.yx, self.y)
axis_test(ax.yaxis, self.y)
@pytest.mark.usefixtures("test_data")
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_plot_xyaxis(self, test_data, plotter):
ax = plt.figure().subplots()
plotter(ax, self.x, self.y)
axis_test(ax.xaxis, self.x)
axis_test(ax.yaxis, self.y)
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_update_plot(self, plotter):
ax = plt.figure().subplots()
plotter(ax, ['a', 'b'], ['e', 'g'])
plotter(ax, ['a', 'b', 'd'], ['f', 'a', 'b'])
plotter(ax, ['b', 'c', 'd'], ['g', 'e', 'd'])
axis_test(ax.xaxis, ['a', 'b', 'd', 'c'])
axis_test(ax.yaxis, ['e', 'g', 'f', 'a', 'b', 'd'])
def test_update_plot_heterogenous_plotter(self):
ax = plt.figure().subplots()
ax.scatter(['a', 'b'], ['e', 'g'])
ax.plot(['a', 'b', 'd'], ['f', 'a', 'b'])
ax.bar(['b', 'c', 'd'], ['g', 'e', 'd'])
axis_test(ax.xaxis, ['a', 'b', 'd', 'c'])
axis_test(ax.yaxis, ['e', 'g', 'f', 'a', 'b', 'd'])
failing_test_cases = [("mixed", ['A', 3.14]),
("number integer", ['1', 1]),
("string integer", ['42', 42]),
("missing", ['12', np.nan])]
fids, fvalues = zip(*failing_test_cases)
plotters = [Axes.scatter, Axes.bar,
pytest.param(Axes.plot, marks=pytest.mark.xfail)]
@pytest.mark.parametrize("plotter", plotters)
@pytest.mark.parametrize("xdata", fvalues, ids=fids)
def test_mixed_type_exception(self, plotter, xdata):
ax = plt.figure().subplots()
with pytest.raises(TypeError):
plotter(ax, xdata, [1, 2])
@pytest.mark.parametrize("plotter", plotters)
@pytest.mark.parametrize("xdata", fvalues, ids=fids)
def test_mixed_type_update_exception(self, plotter, xdata):
ax = plt.figure().subplots()
with pytest.raises(TypeError):
plotter(ax, [0, 3], [1, 3])
plotter(ax, xdata, [1, 2])
@mpl.style.context('default')
@check_figures_equal(extensions=["png"])
def test_overriding_units_in_plot(fig_test, fig_ref):
from datetime import datetime
t0 = datetime(2018, 3, 1)
t1 = datetime(2018, 3, 2)
t2 = datetime(2018, 3, 3)
t3 = datetime(2018, 3, 4)
ax_test = fig_test.subplots()
ax_ref = fig_ref.subplots()
for ax, kwargs in zip([ax_test, ax_ref],
({}, dict(xunits=None, yunits=None))):
# First call works
ax.plot([t0, t1], ["V1", "V2"], **kwargs)
x_units = ax.xaxis.units
y_units = ax.yaxis.units
# this should not raise
ax.plot([t2, t3], ["V1", "V2"], **kwargs)
# assert that we have not re-set the units attribute at all
assert x_units is ax.xaxis.units
assert y_units is ax.yaxis.units
def test_no_deprecation_on_empty_data():
"""
Smoke test to check that no deprecation warning is emitted. See #22640.
"""
f, ax = plt.subplots()
ax.xaxis.update_units(["a", "b"])
ax.plot([], [])
def test_hist():
fig, ax = plt.subplots()
n, bins, patches = ax.hist(['a', 'b', 'a', 'c', 'ff'])
assert n.shape == (10,)
np.testing.assert_allclose(n, [2., 0., 0., 1., 0., 0., 1., 0., 0., 1.])
def test_set_lim():
# Numpy 1.25 deprecated casting [2.] to float, catch_warnings added to error
# with numpy 1.25 and prior to the change from gh-26597
# can be removed once the minimum numpy version has expired the warning
f, ax = plt.subplots()
ax.plot(["a", "b", "c", "d"], [1, 2, 3, 4])
with warnings.catch_warnings():
ax.set_xlim("b", "c")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,73 @@
from pathlib import Path
import shutil
import pytest
from pytest import approx
from matplotlib.testing.compare import compare_images
from matplotlib.testing.decorators import _image_directories
# Tests of the image comparison algorithm.
@pytest.mark.parametrize(
'im1, im2, tol, expect_rms',
[
# Comparison of an image and the same image with minor differences.
# This expects the images to compare equal under normal tolerance, and
# have a small RMS.
('basn3p02.png', 'basn3p02-minorchange.png', 10, None),
# Now test with no tolerance.
('basn3p02.png', 'basn3p02-minorchange.png', 0, 6.50646),
# Comparison with an image that is shifted by 1px in the X axis.
('basn3p02.png', 'basn3p02-1px-offset.png', 0, 90.15611),
# Comparison with an image with half the pixels shifted by 1px in the X
# axis.
('basn3p02.png', 'basn3p02-half-1px-offset.png', 0, 63.75),
# Comparison of an image and the same image scrambled.
# This expects the images to compare completely different, with a very
# large RMS.
# Note: The image has been scrambled in a specific way, by having
# each color component of each pixel randomly placed somewhere in the
# image. It contains exactly the same number of pixels of each color
# value of R, G and B, but in a totally different position.
# Test with no tolerance to make sure that we pick up even a very small
# RMS error.
('basn3p02.png', 'basn3p02-scrambled.png', 0, 172.63582),
# Comparison of an image and a slightly brighter image.
# The two images are solid color, with the second image being exactly 1
# color value brighter.
# This expects the images to compare equal under normal tolerance, and
# have an RMS of exactly 1.
('all127.png', 'all128.png', 0, 1),
# Now test the reverse comparison.
('all128.png', 'all127.png', 0, 1),
])
def test_image_comparison_expect_rms(im1, im2, tol, expect_rms, tmp_path,
monkeypatch):
"""
Compare two images, expecting a particular RMS error.
im1 and im2 are filenames relative to the baseline_dir directory.
tol is the tolerance to pass to compare_images.
expect_rms is the expected RMS value, or None. If None, the test will
succeed if compare_images succeeds. Otherwise, the test will succeed if
compare_images fails and returns an RMS error almost equal to this value.
"""
# Change the working directory using monkeypatch to use a temporary
# test specific directory
monkeypatch.chdir(tmp_path)
baseline_dir, result_dir = map(Path, _image_directories(lambda: "dummy"))
# Copy "test" image to result_dir, so that compare_images writes
# the diff to result_dir, rather than to the source tree
result_im2 = result_dir / im1
shutil.copyfile(baseline_dir / im2, result_im2)
results = compare_images(
baseline_dir / im1, result_im2, tol=tol, in_decorator=True)
if expect_rms is None:
assert results is None
else:
assert results is not None
assert results['rms'] == approx(expect_rms, abs=1e-4)
@@ -0,0 +1,743 @@
import gc
import platform
import numpy as np
import pytest
import matplotlib as mpl
from matplotlib.testing.decorators import image_comparison
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
from matplotlib import gridspec, ticker
pytestmark = [
pytest.mark.usefixtures('text_placeholders')
]
def example_plot(ax, fontsize=12, nodec=False):
ax.plot([1, 2])
ax.locator_params(nbins=3)
if not nodec:
ax.set_xlabel('x-label', fontsize=fontsize)
ax.set_ylabel('y-label', fontsize=fontsize)
ax.set_title('Title', fontsize=fontsize)
else:
ax.set_xticklabels([])
ax.set_yticklabels([])
def example_pcolor(ax, fontsize=12):
dx, dy = 0.6, 0.6
y, x = np.mgrid[slice(-3, 3 + dy, dy),
slice(-3, 3 + dx, dx)]
z = (1 - x / 2. + x ** 5 + y ** 3) * np.exp(-x ** 2 - y ** 2)
pcm = ax.pcolormesh(x, y, z[:-1, :-1], cmap='RdBu_r', vmin=-1., vmax=1.,
rasterized=True)
ax.set_xlabel('x-label', fontsize=fontsize)
ax.set_ylabel('y-label', fontsize=fontsize)
ax.set_title('Title', fontsize=fontsize)
return pcm
@image_comparison(['constrained_layout1.png'], style='mpl20')
def test_constrained_layout1():
"""Test constrained_layout for a single subplot"""
fig = plt.figure(layout="constrained")
ax = fig.add_subplot()
example_plot(ax, fontsize=24)
@image_comparison(['constrained_layout2.png'], style='mpl20')
def test_constrained_layout2():
"""Test constrained_layout for 2x2 subplots"""
fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
example_plot(ax, fontsize=24)
@image_comparison(['constrained_layout3.png'], style='mpl20')
def test_constrained_layout3():
"""Test constrained_layout for colorbars with subplots"""
fig, axs = plt.subplots(2, 2, layout="constrained")
for nn, ax in enumerate(axs.flat):
pcm = example_pcolor(ax, fontsize=24)
if nn == 3:
pad = 0.08
else:
pad = 0.02 # default
fig.colorbar(pcm, ax=ax, pad=pad)
@image_comparison(['constrained_layout4.png'], style='mpl20')
def test_constrained_layout4():
"""Test constrained_layout for a single colorbar with subplots"""
fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
pcm = example_pcolor(ax, fontsize=24)
fig.colorbar(pcm, ax=axs, pad=0.01, shrink=0.6)
@image_comparison(['constrained_layout5.png'], style='mpl20')
def test_constrained_layout5():
"""
Test constrained_layout for a single colorbar with subplots,
colorbar bottom
"""
fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
pcm = example_pcolor(ax, fontsize=24)
fig.colorbar(pcm, ax=axs,
use_gridspec=False, pad=0.01, shrink=0.6,
location='bottom')
@image_comparison(['constrained_layout6.png'], style='mpl20')
def test_constrained_layout6():
"""Test constrained_layout for nested gridspecs"""
fig = plt.figure(layout="constrained")
gs = fig.add_gridspec(1, 2, figure=fig)
gsl = gs[0].subgridspec(2, 2)
gsr = gs[1].subgridspec(1, 2)
axsl = []
for gs in gsl:
ax = fig.add_subplot(gs)
axsl += [ax]
example_plot(ax, fontsize=12)
ax.set_xlabel('x-label\nMultiLine')
axsr = []
for gs in gsr:
ax = fig.add_subplot(gs)
axsr += [ax]
pcm = example_pcolor(ax, fontsize=12)
fig.colorbar(pcm, ax=axsr,
pad=0.01, shrink=0.99, location='bottom',
ticks=ticker.MaxNLocator(nbins=5))
def test_identical_subgridspec():
fig = plt.figure(constrained_layout=True)
GS = fig.add_gridspec(2, 1)
GSA = GS[0].subgridspec(1, 3)
GSB = GS[1].subgridspec(1, 3)
axa = []
axb = []
for i in range(3):
axa += [fig.add_subplot(GSA[i])]
axb += [fig.add_subplot(GSB[i])]
fig.draw_without_rendering()
# check first row above second
assert axa[0].get_position().y0 > axb[0].get_position().y1
def test_constrained_layout7():
"""Test for proper warning if fig not set in GridSpec"""
with pytest.warns(
UserWarning, match=('There are no gridspecs with layoutgrids. '
'Possibly did not call parent GridSpec with '
'the "figure" keyword')):
fig = plt.figure(layout="constrained")
gs = gridspec.GridSpec(1, 2)
gsl = gridspec.GridSpecFromSubplotSpec(2, 2, gs[0])
gsr = gridspec.GridSpecFromSubplotSpec(1, 2, gs[1])
for gs in gsl:
fig.add_subplot(gs)
# need to trigger a draw to get warning
fig.draw_without_rendering()
@image_comparison(['constrained_layout8.png'], style='mpl20')
def test_constrained_layout8():
"""Test for gridspecs that are not completely full"""
fig = plt.figure(figsize=(10, 5), layout="constrained")
gs = gridspec.GridSpec(3, 5, figure=fig)
axs = []
for j in [0, 1]:
if j == 0:
ilist = [1]
else:
ilist = [0, 4]
for i in ilist:
ax = fig.add_subplot(gs[j, i])
axs += [ax]
example_pcolor(ax, fontsize=9)
if i > 0:
ax.set_ylabel('')
if j < 1:
ax.set_xlabel('')
ax.set_title('')
ax = fig.add_subplot(gs[2, :])
axs += [ax]
pcm = example_pcolor(ax, fontsize=9)
fig.colorbar(pcm, ax=axs, pad=0.01, shrink=0.6)
@image_comparison(['constrained_layout9.png'], style='mpl20')
def test_constrained_layout9():
"""Test for handling suptitle and for sharex and sharey"""
fig, axs = plt.subplots(2, 2, layout="constrained",
sharex=False, sharey=False)
for ax in axs.flat:
pcm = example_pcolor(ax, fontsize=24)
ax.set_xlabel('')
ax.set_ylabel('')
ax.set_aspect(2.)
fig.colorbar(pcm, ax=axs, pad=0.01, shrink=0.6)
fig.suptitle('Test Suptitle', fontsize=28)
@image_comparison(['constrained_layout10.png'], style='mpl20',
tol=0 if platform.machine() == 'x86_64' else 0.032)
def test_constrained_layout10():
"""Test for handling legend outside axis"""
fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
ax.plot(np.arange(12), label='This is a label')
ax.legend(loc='center left', bbox_to_anchor=(0.8, 0.5))
@image_comparison(['constrained_layout11.png'], style='mpl20')
def test_constrained_layout11():
"""Test for multiple nested gridspecs"""
fig = plt.figure(layout="constrained", figsize=(13, 3))
gs0 = gridspec.GridSpec(1, 2, figure=fig)
gsl = gridspec.GridSpecFromSubplotSpec(1, 2, gs0[0])
gsl0 = gridspec.GridSpecFromSubplotSpec(2, 2, gsl[1])
ax = fig.add_subplot(gs0[1])
example_plot(ax, fontsize=9)
axs = []
for gs in gsl0:
ax = fig.add_subplot(gs)
axs += [ax]
pcm = example_pcolor(ax, fontsize=9)
fig.colorbar(pcm, ax=axs, shrink=0.6, aspect=70.)
ax = fig.add_subplot(gsl[0])
example_plot(ax, fontsize=9)
@image_comparison(['constrained_layout11rat.png'], style='mpl20')
def test_constrained_layout11rat():
"""Test for multiple nested gridspecs with width_ratios"""
fig = plt.figure(layout="constrained", figsize=(10, 3))
gs0 = gridspec.GridSpec(1, 2, figure=fig, width_ratios=[6, 1])
gsl = gridspec.GridSpecFromSubplotSpec(1, 2, gs0[0])
gsl0 = gridspec.GridSpecFromSubplotSpec(2, 2, gsl[1], height_ratios=[2, 1])
ax = fig.add_subplot(gs0[1])
example_plot(ax, fontsize=9)
axs = []
for gs in gsl0:
ax = fig.add_subplot(gs)
axs += [ax]
pcm = example_pcolor(ax, fontsize=9)
fig.colorbar(pcm, ax=axs, shrink=0.6, aspect=70.)
ax = fig.add_subplot(gsl[0])
example_plot(ax, fontsize=9)
@image_comparison(['constrained_layout12.png'], style='mpl20')
def test_constrained_layout12():
"""Test that very unbalanced labeling still works."""
fig = plt.figure(layout="constrained", figsize=(6, 8))
gs0 = gridspec.GridSpec(6, 2, figure=fig)
ax1 = fig.add_subplot(gs0[:3, 1])
ax2 = fig.add_subplot(gs0[3:, 1])
example_plot(ax1, fontsize=18)
example_plot(ax2, fontsize=18)
ax = fig.add_subplot(gs0[0:2, 0])
example_plot(ax, nodec=True)
ax = fig.add_subplot(gs0[2:4, 0])
example_plot(ax, nodec=True)
ax = fig.add_subplot(gs0[4:, 0])
example_plot(ax, nodec=True)
ax.set_xlabel('x-label')
@image_comparison(['constrained_layout13.png'], style='mpl20')
def test_constrained_layout13():
"""Test that padding works."""
fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
pcm = example_pcolor(ax, fontsize=12)
fig.colorbar(pcm, ax=ax, shrink=0.6, aspect=20., pad=0.02)
with pytest.raises(TypeError):
fig.get_layout_engine().set(wpad=1, hpad=2)
fig.get_layout_engine().set(w_pad=24./72., h_pad=24./72.)
@image_comparison(['constrained_layout14.png'], style='mpl20')
def test_constrained_layout14():
"""Test that padding works."""
fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
pcm = example_pcolor(ax, fontsize=12)
fig.colorbar(pcm, ax=ax, shrink=0.6, aspect=20., pad=0.02)
fig.get_layout_engine().set(
w_pad=3./72., h_pad=3./72.,
hspace=0.2, wspace=0.2)
@image_comparison(['constrained_layout15.png'], style='mpl20')
def test_constrained_layout15():
"""Test that rcparams work."""
mpl.rcParams['figure.constrained_layout.use'] = True
fig, axs = plt.subplots(2, 2)
for ax in axs.flat:
example_plot(ax, fontsize=12)
@image_comparison(['constrained_layout16.png'], style='mpl20')
def test_constrained_layout16():
"""Test ax.set_position."""
fig, ax = plt.subplots(layout="constrained")
example_plot(ax, fontsize=12)
ax2 = fig.add_axes([0.2, 0.2, 0.4, 0.4])
@image_comparison(['constrained_layout17.png'], style='mpl20')
def test_constrained_layout17():
"""Test uneven gridspecs"""
fig = plt.figure(layout="constrained")
gs = gridspec.GridSpec(3, 3, figure=fig)
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[0, 1:])
ax3 = fig.add_subplot(gs[1:, 0:2])
ax4 = fig.add_subplot(gs[1:, -1])
example_plot(ax1)
example_plot(ax2)
example_plot(ax3)
example_plot(ax4)
def test_constrained_layout18():
"""Test twinx"""
fig, ax = plt.subplots(layout="constrained")
ax2 = ax.twinx()
example_plot(ax)
example_plot(ax2, fontsize=24)
fig.draw_without_rendering()
assert all(ax.get_position().extents == ax2.get_position().extents)
def test_constrained_layout19():
"""Test twiny"""
fig, ax = plt.subplots(layout="constrained")
ax2 = ax.twiny()
example_plot(ax)
example_plot(ax2, fontsize=24)
ax2.set_title('')
ax.set_title('')
fig.draw_without_rendering()
assert all(ax.get_position().extents == ax2.get_position().extents)
def test_constrained_layout20():
"""Smoke test cl does not mess up added Axes"""
gx = np.linspace(-5, 5, 4)
img = np.hypot(gx, gx[:, None])
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
mesh = ax.pcolormesh(gx, gx, img[:-1, :-1])
fig.colorbar(mesh)
def test_constrained_layout21():
"""#11035: repeated calls to suptitle should not alter the layout"""
fig, ax = plt.subplots(layout="constrained")
fig.suptitle("Suptitle0")
fig.draw_without_rendering()
extents0 = np.copy(ax.get_position().extents)
fig.suptitle("Suptitle1")
fig.draw_without_rendering()
extents1 = np.copy(ax.get_position().extents)
np.testing.assert_allclose(extents0, extents1)
def test_constrained_layout22():
"""#11035: suptitle should not be include in CL if manually positioned"""
fig, ax = plt.subplots(layout="constrained")
fig.draw_without_rendering()
extents0 = np.copy(ax.get_position().extents)
fig.suptitle("Suptitle", y=0.5)
fig.draw_without_rendering()
extents1 = np.copy(ax.get_position().extents)
np.testing.assert_allclose(extents0, extents1)
def test_constrained_layout23():
"""
Comment in #11035: suptitle used to cause an exception when
reusing a figure w/ CL with ``clear=True``.
"""
for i in range(2):
fig = plt.figure(layout="constrained", clear=True, num="123")
gs = fig.add_gridspec(1, 2)
sub = gs[0].subgridspec(2, 2)
fig.suptitle(f"Suptitle{i}")
@image_comparison(['test_colorbar_location.png'],
remove_text=True, style='mpl20')
def test_colorbar_location():
"""
Test that colorbar handling is as expected for various complicated
cases...
"""
# Remove this line when this test image is regenerated.
plt.rcParams['pcolormesh.snap'] = False
fig, axs = plt.subplots(4, 5, layout="constrained")
for ax in axs.flat:
pcm = example_pcolor(ax)
ax.set_xlabel('')
ax.set_ylabel('')
fig.colorbar(pcm, ax=axs[:, 1], shrink=0.4)
fig.colorbar(pcm, ax=axs[-1, :2], shrink=0.5, location='bottom')
fig.colorbar(pcm, ax=axs[0, 2:], shrink=0.5, location='bottom', pad=0.05)
fig.colorbar(pcm, ax=axs[-2, 3:], shrink=0.5, location='top')
fig.colorbar(pcm, ax=axs[0, 0], shrink=0.5, location='left')
fig.colorbar(pcm, ax=axs[1:3, 2], shrink=0.5, location='right')
def test_hidden_axes():
# test that if we make an Axes not visible that constrained_layout
# still works. Note the axes still takes space in the layout
# (as does a gridspec slot that is empty)
fig, axs = plt.subplots(2, 2, layout="constrained")
axs[0, 1].set_visible(False)
fig.draw_without_rendering()
extents1 = np.copy(axs[0, 0].get_position().extents)
np.testing.assert_allclose(
extents1, [0.046918, 0.541204, 0.477409, 0.980555], rtol=1e-5)
def test_colorbar_align():
for location in ['right', 'left', 'top', 'bottom']:
fig, axs = plt.subplots(2, 2, layout="constrained")
cbs = []
for nn, ax in enumerate(axs.flat):
ax.tick_params(direction='in')
pc = example_pcolor(ax)
cb = fig.colorbar(pc, ax=ax, location=location, shrink=0.6,
pad=0.04)
cbs += [cb]
cb.ax.tick_params(direction='in')
if nn != 1:
cb.ax.xaxis.set_ticks([])
cb.ax.yaxis.set_ticks([])
ax.set_xticklabels([])
ax.set_yticklabels([])
fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72,
hspace=0.1, wspace=0.1)
fig.draw_without_rendering()
if location in ['left', 'right']:
np.testing.assert_allclose(cbs[0].ax.get_position().x0,
cbs[2].ax.get_position().x0)
np.testing.assert_allclose(cbs[1].ax.get_position().x0,
cbs[3].ax.get_position().x0)
else:
np.testing.assert_allclose(cbs[0].ax.get_position().y0,
cbs[1].ax.get_position().y0)
np.testing.assert_allclose(cbs[2].ax.get_position().y0,
cbs[3].ax.get_position().y0)
@image_comparison(['test_colorbars_no_overlapV.png'], style='mpl20')
def test_colorbars_no_overlapV():
fig = plt.figure(figsize=(2, 4), layout="constrained")
axs = fig.subplots(2, 1, sharex=True, sharey=True)
for ax in axs:
ax.yaxis.set_major_formatter(ticker.NullFormatter())
ax.tick_params(axis='both', direction='in')
im = ax.imshow([[1, 2], [3, 4]])
fig.colorbar(im, ax=ax, orientation="vertical")
fig.suptitle("foo")
@image_comparison(['test_colorbars_no_overlapH.png'], style='mpl20')
def test_colorbars_no_overlapH():
fig = plt.figure(figsize=(4, 2), layout="constrained")
fig.suptitle("foo")
axs = fig.subplots(1, 2, sharex=True, sharey=True)
for ax in axs:
ax.yaxis.set_major_formatter(ticker.NullFormatter())
ax.tick_params(axis='both', direction='in')
im = ax.imshow([[1, 2], [3, 4]])
fig.colorbar(im, ax=ax, orientation="horizontal")
def test_manually_set_position():
fig, axs = plt.subplots(1, 2, layout="constrained")
axs[0].set_position([0.2, 0.2, 0.3, 0.3])
fig.draw_without_rendering()
pp = axs[0].get_position()
np.testing.assert_allclose(pp, [[0.2, 0.2], [0.5, 0.5]])
fig, axs = plt.subplots(1, 2, layout="constrained")
axs[0].set_position([0.2, 0.2, 0.3, 0.3])
pc = axs[0].pcolormesh(np.random.rand(20, 20))
fig.colorbar(pc, ax=axs[0])
fig.draw_without_rendering()
pp = axs[0].get_position()
np.testing.assert_allclose(pp, [[0.2, 0.2], [0.44, 0.5]])
@image_comparison(['test_bboxtight.png'],
remove_text=True, style='mpl20',
savefig_kwarg={'bbox_inches': 'tight'})
def test_bboxtight():
fig, ax = plt.subplots(layout="constrained")
ax.set_aspect(1.)
@image_comparison(['test_bbox.png'],
remove_text=True, style='mpl20',
savefig_kwarg={'bbox_inches':
mtransforms.Bbox([[0.5, 0], [2.5, 2]])})
def test_bbox():
fig, ax = plt.subplots(layout="constrained")
ax.set_aspect(1.)
def test_align_labels():
"""
Tests for a bug in which constrained layout and align_ylabels on
three unevenly sized subplots, one of whose y tick labels include
negative numbers, drives the non-negative subplots' y labels off
the edge of the plot
"""
fig, (ax3, ax1, ax2) = plt.subplots(3, 1, layout="constrained",
figsize=(6.4, 8),
gridspec_kw={"height_ratios": (1, 1,
0.7)})
ax1.set_ylim(0, 1)
ax1.set_ylabel("Label")
ax2.set_ylim(-1.5, 1.5)
ax2.set_ylabel("Label")
ax3.set_ylim(0, 1)
ax3.set_ylabel("Label")
fig.align_ylabels(axs=(ax3, ax1, ax2))
fig.draw_without_rendering()
after_align = [ax1.yaxis.label.get_window_extent(),
ax2.yaxis.label.get_window_extent(),
ax3.yaxis.label.get_window_extent()]
# ensure labels are approximately aligned
np.testing.assert_allclose([after_align[0].x0, after_align[2].x0],
after_align[1].x0, rtol=0, atol=1e-05)
# ensure labels do not go off the edge
assert after_align[0].x0 >= 1
def test_suplabels():
fig, ax = plt.subplots(layout="constrained")
fig.draw_without_rendering()
pos0 = ax.get_tightbbox(fig.canvas.get_renderer())
fig.supxlabel('Boo')
fig.supylabel('Booy')
fig.draw_without_rendering()
pos = ax.get_tightbbox(fig.canvas.get_renderer())
assert pos.y0 > pos0.y0 + 10.0
assert pos.x0 > pos0.x0 + 10.0
fig, ax = plt.subplots(layout="constrained")
fig.draw_without_rendering()
pos0 = ax.get_tightbbox(fig.canvas.get_renderer())
# check that specifying x (y) doesn't ruin the layout
fig.supxlabel('Boo', x=0.5)
fig.supylabel('Boo', y=0.5)
fig.draw_without_rendering()
pos = ax.get_tightbbox(fig.canvas.get_renderer())
assert pos.y0 > pos0.y0 + 10.0
assert pos.x0 > pos0.x0 + 10.0
def test_gridspec_addressing():
fig = plt.figure()
gs = fig.add_gridspec(3, 3)
sp = fig.add_subplot(gs[0:, 1:])
fig.draw_without_rendering()
def test_discouraged_api():
fig, ax = plt.subplots(constrained_layout=True)
fig.draw_without_rendering()
with pytest.warns(PendingDeprecationWarning,
match="will be deprecated"):
fig, ax = plt.subplots()
fig.set_constrained_layout(True)
fig.draw_without_rendering()
with pytest.warns(PendingDeprecationWarning,
match="will be deprecated"):
fig, ax = plt.subplots()
fig.set_constrained_layout({'w_pad': 0.02, 'h_pad': 0.02})
fig.draw_without_rendering()
def test_kwargs():
fig, ax = plt.subplots(constrained_layout={'h_pad': 0.02})
fig.draw_without_rendering()
def test_rect():
fig, ax = plt.subplots(layout='constrained')
fig.get_layout_engine().set(rect=[0, 0, 0.5, 0.5])
fig.draw_without_rendering()
ppos = ax.get_position()
assert ppos.x1 < 0.5
assert ppos.y1 < 0.5
fig, ax = plt.subplots(layout='constrained')
fig.get_layout_engine().set(rect=[0.2, 0.2, 0.3, 0.3])
fig.draw_without_rendering()
ppos = ax.get_position()
assert ppos.x1 < 0.5
assert ppos.y1 < 0.5
assert ppos.x0 > 0.2
assert ppos.y0 > 0.2
def test_compressed1():
fig, axs = plt.subplots(3, 2, layout='compressed',
sharex=True, sharey=True)
for ax in axs.flat:
pc = ax.imshow(np.random.randn(20, 20))
fig.colorbar(pc, ax=axs)
fig.draw_without_rendering()
pos = axs[0, 0].get_position()
np.testing.assert_allclose(pos.x0, 0.2381, atol=1e-2)
pos = axs[0, 1].get_position()
np.testing.assert_allclose(pos.x1, 0.7024, atol=1e-3)
# wider than tall
fig, axs = plt.subplots(2, 3, layout='compressed',
sharex=True, sharey=True, figsize=(5, 4))
for ax in axs.flat:
pc = ax.imshow(np.random.randn(20, 20))
fig.colorbar(pc, ax=axs)
fig.draw_without_rendering()
pos = axs[0, 0].get_position()
np.testing.assert_allclose(pos.x0, 0.05653, atol=1e-3)
np.testing.assert_allclose(pos.y1, 0.8603, atol=1e-2)
pos = axs[1, 2].get_position()
np.testing.assert_allclose(pos.x1, 0.8728, atol=1e-3)
np.testing.assert_allclose(pos.y0, 0.1808, atol=1e-2)
def test_compressed_suptitle():
fig, (ax0, ax1) = plt.subplots(
nrows=2, figsize=(4, 10), layout="compressed",
gridspec_kw={"height_ratios": (1 / 4, 3 / 4), "hspace": 0})
ax0.axis("equal")
ax0.set_box_aspect(1/3)
ax1.axis("equal")
ax1.set_box_aspect(1)
title = fig.suptitle("Title")
fig.draw_without_rendering()
assert title.get_position()[1] == pytest.approx(0.7491, abs=1e-3)
title = fig.suptitle("Title", y=0.98)
fig.draw_without_rendering()
assert title.get_position()[1] == 0.98
title = fig.suptitle("Title", in_layout=False)
fig.draw_without_rendering()
assert title.get_position()[1] == 0.98
@pytest.mark.parametrize('arg, state', [
(True, True),
(False, False),
({}, True),
({'rect': None}, True)
])
def test_set_constrained_layout(arg, state):
fig, ax = plt.subplots(constrained_layout=arg)
assert fig.get_constrained_layout() is state
def test_constrained_toggle():
fig, ax = plt.subplots()
with pytest.warns(PendingDeprecationWarning):
fig.set_constrained_layout(True)
assert fig.get_constrained_layout()
fig.set_constrained_layout(False)
assert not fig.get_constrained_layout()
fig.set_constrained_layout(True)
assert fig.get_constrained_layout()
def test_layout_leak():
# Make sure there aren't any cyclic references when using LayoutGrid
# GH #25853
fig = plt.figure(constrained_layout=True, figsize=(10, 10))
fig.add_subplot()
fig.draw_without_rendering()
plt.close("all")
del fig
gc.collect()
assert not any(isinstance(obj, mpl._layoutgrid.LayoutGrid)
for obj in gc.get_objects())
def test_submerged_subfig():
"""
Test that the submerged margin logic does not get called multiple times
on same axes if it is already in a subfigure
"""
fig = plt.figure(figsize=(4, 5), layout='constrained')
figures = fig.subfigures(3, 1)
axs = []
for f in figures.flatten():
gs = f.add_gridspec(2, 2)
for i in range(2):
axs += [f.add_subplot(gs[i, 0])]
axs[-1].plot()
f.add_subplot(gs[:, 1]).plot()
fig.draw_without_rendering()
for ax in axs[1:]:
assert np.allclose(ax.get_position().bounds[-1],
axs[0].get_position().bounds[-1], atol=1e-6)
@@ -0,0 +1,37 @@
import numpy as np
import matplotlib.pyplot as plt
def test_stem_remove():
ax = plt.gca()
st = ax.stem([1, 2], [1, 2])
st.remove()
def test_errorbar_remove():
# Regression test for a bug that caused remove to fail when using
# fmt='none'
ax = plt.gca()
eb = ax.errorbar([1], [1])
eb.remove()
eb = ax.errorbar([1], [1], xerr=1)
eb.remove()
eb = ax.errorbar([1], [1], yerr=2)
eb.remove()
eb = ax.errorbar([1], [1], xerr=[2], yerr=2)
eb.remove()
eb = ax.errorbar([1], [1], fmt='none')
eb.remove()
def test_nonstring_label():
# Test for #26824
plt.bar(np.arange(10), np.random.rand(10), label=1)
plt.legend()
@@ -0,0 +1,839 @@
import datetime
import platform
import re
from unittest import mock
import contourpy
import numpy as np
from numpy.testing import assert_array_almost_equal, assert_array_almost_equal_nulp
import matplotlib as mpl
from matplotlib import pyplot as plt, rc_context, ticker
from matplotlib.colors import LogNorm, same_color
import matplotlib.patches as mpatches
from matplotlib.testing.decorators import check_figures_equal, image_comparison
import pytest
def test_contour_shape_1d_valid():
x = np.arange(10)
y = np.arange(9)
z = np.random.random((9, 10))
fig, ax = plt.subplots()
ax.contour(x, y, z)
def test_contour_shape_2d_valid():
x = np.arange(10)
y = np.arange(9)
xg, yg = np.meshgrid(x, y)
z = np.random.random((9, 10))
fig, ax = plt.subplots()
ax.contour(xg, yg, z)
@pytest.mark.parametrize("args, message", [
((np.arange(9), np.arange(9), np.empty((9, 10))),
'Length of x (9) must match number of columns in z (10)'),
((np.arange(10), np.arange(10), np.empty((9, 10))),
'Length of y (10) must match number of rows in z (9)'),
((np.empty((10, 10)), np.arange(10), np.empty((9, 10))),
'Number of dimensions of x (2) and y (1) do not match'),
((np.arange(10), np.empty((10, 10)), np.empty((9, 10))),
'Number of dimensions of x (1) and y (2) do not match'),
((np.empty((9, 9)), np.empty((9, 10)), np.empty((9, 10))),
'Shapes of x (9, 9) and z (9, 10) do not match'),
((np.empty((9, 10)), np.empty((9, 9)), np.empty((9, 10))),
'Shapes of y (9, 9) and z (9, 10) do not match'),
((np.empty((3, 3, 3)), np.empty((3, 3, 3)), np.empty((9, 10))),
'Inputs x and y must be 1D or 2D, not 3D'),
((np.empty((3, 3, 3)), np.empty((3, 3, 3)), np.empty((3, 3, 3))),
'Input z must be 2D, not 3D'),
(([[0]],), # github issue 8197
'Input z must be at least a (2, 2) shaped array, but has shape (1, 1)'),
(([0], [0], [[0]]),
'Input z must be at least a (2, 2) shaped array, but has shape (1, 1)'),
])
def test_contour_shape_error(args, message):
fig, ax = plt.subplots()
with pytest.raises(TypeError, match=re.escape(message)):
ax.contour(*args)
def test_contour_no_valid_levels():
fig, ax = plt.subplots()
# no warning for empty levels.
ax.contour(np.random.rand(9, 9), levels=[])
# no warning if levels is given and is not within the range of z.
cs = ax.contour(np.arange(81).reshape((9, 9)), levels=[100])
# ... and if fmt is given.
ax.clabel(cs, fmt={100: '%1.2f'})
# no warning if z is uniform.
ax.contour(np.ones((9, 9)))
def test_contour_Nlevels():
# A scalar levels arg or kwarg should trigger auto level generation.
# https://github.com/matplotlib/matplotlib/issues/11913
z = np.arange(12).reshape((3, 4))
fig, ax = plt.subplots()
cs1 = ax.contour(z, 5)
assert len(cs1.levels) > 1
cs2 = ax.contour(z, levels=5)
assert (cs1.levels == cs2.levels).all()
@check_figures_equal(extensions=['png'])
def test_contour_set_paths(fig_test, fig_ref):
cs_test = fig_test.subplots().contour([[0, 1], [1, 2]])
cs_ref = fig_ref.subplots().contour([[1, 0], [2, 1]])
cs_test.set_paths(cs_ref.get_paths())
@image_comparison(['contour_manual_labels'], remove_text=True, style='mpl20', tol=0.26)
def test_contour_manual_labels():
x, y = np.meshgrid(np.arange(0, 10), np.arange(0, 10))
z = np.max(np.dstack([abs(x), abs(y)]), 2)
plt.figure(figsize=(6, 2), dpi=200)
cs = plt.contour(x, y, z)
pts = np.array([(1.0, 3.0), (1.0, 4.4), (1.0, 6.0)])
plt.clabel(cs, manual=pts)
pts = np.array([(2.0, 3.0), (2.0, 4.4), (2.0, 6.0)])
plt.clabel(cs, manual=pts, fontsize='small', colors=('r', 'g'))
def test_contour_manual_moveto():
x = np.linspace(-10, 10)
y = np.linspace(-10, 10)
X, Y = np.meshgrid(x, y)
Z = X**2 * 1 / Y**2 - 1
contours = plt.contour(X, Y, Z, levels=[0, 100])
# This point lies on the `MOVETO` line for the 100 contour
# but is actually closest to the 0 contour
point = (1.3, 1)
clabels = plt.clabel(contours, manual=[point])
# Ensure that the 0 contour was chosen, not the 100 contour
assert clabels[0].get_text() == "0"
@image_comparison(['contour_disconnected_segments'],
remove_text=True, style='mpl20', extensions=['png'])
def test_contour_label_with_disconnected_segments():
x, y = np.mgrid[-1:1:21j, -1:1:21j]
z = 1 / np.sqrt(0.01 + (x + 0.3) ** 2 + y ** 2)
z += 1 / np.sqrt(0.01 + (x - 0.3) ** 2 + y ** 2)
plt.figure()
cs = plt.contour(x, y, z, levels=[7])
cs.clabel(manual=[(0.2, 0.1)])
@image_comparison(['contour_manual_colors_and_levels.png'], remove_text=True,
tol=0 if platform.machine() == 'x86_64' else 0.018)
def test_given_colors_levels_and_extends():
# Remove this line when this test image is regenerated.
plt.rcParams['pcolormesh.snap'] = False
_, axs = plt.subplots(2, 4)
data = np.arange(12).reshape(3, 4)
colors = ['red', 'yellow', 'pink', 'blue', 'black']
levels = [2, 4, 8, 10]
for i, ax in enumerate(axs.flat):
filled = i % 2 == 0.
extend = ['neither', 'min', 'max', 'both'][i // 2]
if filled:
# If filled, we have 3 colors with no extension,
# 4 colors with one extension, and 5 colors with both extensions
first_color = 1 if extend in ['max', 'neither'] else None
last_color = -1 if extend in ['min', 'neither'] else None
c = ax.contourf(data, colors=colors[first_color:last_color],
levels=levels, extend=extend)
else:
# If not filled, we have 4 levels and 4 colors
c = ax.contour(data, colors=colors[:-1],
levels=levels, extend=extend)
plt.colorbar(c, ax=ax)
@image_comparison(['contourf_hatch_colors'],
remove_text=True, style='mpl20', extensions=['png'])
def test_hatch_colors():
fig, ax = plt.subplots()
cf = ax.contourf([[0, 1], [1, 2]], hatches=['-', '/', '\\', '//'], cmap='gray')
cf.set_edgecolors(["blue", "grey", "yellow", "red"])
@pytest.mark.parametrize('color, extend', [('darkred', 'neither'),
('darkred', 'both'),
(('r', 0.5), 'neither'),
((0.1, 0.2, 0.5, 0.3), 'neither')])
def test_single_color_and_extend(color, extend):
z = [[0, 1], [1, 2]]
_, ax = plt.subplots()
levels = [0.5, 0.75, 1, 1.25, 1.5]
cs = ax.contour(z, levels=levels, colors=color, extend=extend)
for c in cs.get_edgecolors():
assert same_color(c, color)
@image_comparison(['contour_log_locator.svg'], style='mpl20', remove_text=False)
def test_log_locator_levels():
fig, ax = plt.subplots()
N = 100
x = np.linspace(-3.0, 3.0, N)
y = np.linspace(-2.0, 2.0, N)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-X**2 - Y**2)
Z2 = np.exp(-(X * 10)**2 - (Y * 10)**2)
data = Z1 + 50 * Z2
c = ax.contourf(data, locator=ticker.LogLocator())
assert_array_almost_equal(c.levels, np.power(10.0, np.arange(-6, 3)))
cb = fig.colorbar(c, ax=ax)
assert_array_almost_equal(cb.ax.get_yticks(), c.levels)
@image_comparison(['contour_datetime_axis.png'], style='mpl20')
def test_contour_datetime_axis():
fig = plt.figure()
fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15)
base = datetime.datetime(2013, 1, 1)
x = np.array([base + datetime.timedelta(days=d) for d in range(20)])
y = np.arange(20)
z1, z2 = np.meshgrid(np.arange(20), np.arange(20))
z = z1 * z2
plt.subplot(221)
plt.contour(x, y, z)
plt.subplot(222)
plt.contourf(x, y, z)
x = np.repeat(x[np.newaxis], 20, axis=0)
y = np.repeat(y[:, np.newaxis], 20, axis=1)
plt.subplot(223)
plt.contour(x, y, z)
plt.subplot(224)
plt.contourf(x, y, z)
for ax in fig.get_axes():
for label in ax.get_xticklabels():
label.set_ha('right')
label.set_rotation(30)
@image_comparison(['contour_test_label_transforms.png'],
remove_text=True, style='mpl20', tol=1.1)
def test_labels():
# Adapted from pylab_examples example code: contour_demo.py
# see issues #2475, #2843, and #2818 for explanation
delta = 0.025
x = np.arange(-3.0, 3.0, delta)
y = np.arange(-2.0, 2.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi)
Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) /
(2 * np.pi * 0.5 * 1.5))
# difference of Gaussians
Z = 10.0 * (Z2 - Z1)
fig, ax = plt.subplots(1, 1)
CS = ax.contour(X, Y, Z)
disp_units = [(216, 177), (359, 290), (521, 406)]
data_units = [(-2, .5), (0, -1.5), (2.8, 1)]
CS.clabel()
for x, y in data_units:
CS.add_label_near(x, y, inline=True, transform=None)
for x, y in disp_units:
CS.add_label_near(x, y, inline=True, transform=False)
def test_label_contour_start():
# Set up data and figure/axes that result in automatic labelling adding the
# label to the start of a contour
_, ax = plt.subplots(dpi=100)
lats = lons = np.linspace(-np.pi / 2, np.pi / 2, 50)
lons, lats = np.meshgrid(lons, lats)
wave = 0.75 * (np.sin(2 * lats) ** 8) * np.cos(4 * lons)
mean = 0.5 * np.cos(2 * lats) * ((np.sin(2 * lats)) ** 2 + 2)
data = wave + mean
cs = ax.contour(lons, lats, data)
with mock.patch.object(
cs, '_split_path_and_get_label_rotation',
wraps=cs._split_path_and_get_label_rotation) as mocked_splitter:
# Smoke test that we can add the labels
cs.clabel(fontsize=9)
# Verify at least one label was added to the start of a contour. I.e. the
# splitting method was called with idx=0 at least once.
idxs = [cargs[0][1] for cargs in mocked_splitter.call_args_list]
assert 0 in idxs
@image_comparison(['contour_corner_mask_False.png', 'contour_corner_mask_True.png'],
remove_text=True, tol=1.88)
def test_corner_mask():
n = 60
mask_level = 0.95
noise_amp = 1.0
np.random.seed([1])
x, y = np.meshgrid(np.linspace(0, 2.0, n), np.linspace(0, 2.0, n))
z = np.cos(7*x)*np.sin(8*y) + noise_amp*np.random.rand(n, n)
mask = np.random.rand(n, n) >= mask_level
z = np.ma.array(z, mask=mask)
for corner_mask in [False, True]:
plt.figure()
plt.contourf(z, corner_mask=corner_mask)
def test_contourf_decreasing_levels():
# github issue 5477.
z = [[0.1, 0.3], [0.5, 0.7]]
plt.figure()
with pytest.raises(ValueError):
plt.contourf(z, [1.0, 0.0])
def test_contourf_symmetric_locator():
# github issue 7271
z = np.arange(12).reshape((3, 4))
locator = plt.MaxNLocator(nbins=4, symmetric=True)
cs = plt.contourf(z, locator=locator)
assert_array_almost_equal(cs.levels, np.linspace(-12, 12, 5))
def test_circular_contour_warning():
# Check that almost circular contours don't throw a warning
x, y = np.meshgrid(np.linspace(-2, 2, 4), np.linspace(-2, 2, 4))
r = np.hypot(x, y)
plt.figure()
cs = plt.contour(x, y, r)
plt.clabel(cs)
@pytest.mark.parametrize("use_clabeltext, contour_zorder, clabel_zorder",
[(True, 123, 1234), (False, 123, 1234),
(True, 123, None), (False, 123, None)])
def test_clabel_zorder(use_clabeltext, contour_zorder, clabel_zorder):
x, y = np.meshgrid(np.arange(0, 10), np.arange(0, 10))
z = np.max(np.dstack([abs(x), abs(y)]), 2)
fig, (ax1, ax2) = plt.subplots(ncols=2)
cs = ax1.contour(x, y, z, zorder=contour_zorder)
cs_filled = ax2.contourf(x, y, z, zorder=contour_zorder)
clabels1 = cs.clabel(zorder=clabel_zorder, use_clabeltext=use_clabeltext)
clabels2 = cs_filled.clabel(zorder=clabel_zorder,
use_clabeltext=use_clabeltext)
if clabel_zorder is None:
expected_clabel_zorder = 2+contour_zorder
else:
expected_clabel_zorder = clabel_zorder
for clabel in clabels1:
assert clabel.get_zorder() == expected_clabel_zorder
for clabel in clabels2:
assert clabel.get_zorder() == expected_clabel_zorder
def test_clabel_with_large_spacing():
# When the inline spacing is large relative to the contour, it may cause the
# entire contour to be removed. In current implementation, one line segment is
# retained between the identified points.
# This behavior may be worth reconsidering, but check to be sure we do not produce
# an invalid path, which results in an error at clabel call time.
# see gh-27045 for more information
x = y = np.arange(-3.0, 3.01, 0.05)
X, Y = np.meshgrid(x, y)
Z = np.exp(-X**2 - Y**2)
fig, ax = plt.subplots()
contourset = ax.contour(X, Y, Z, levels=[0.01, 0.2, .5, .8])
ax.clabel(contourset, inline_spacing=100)
# tol because ticks happen to fall on pixel boundaries so small
# floating point changes in tick location flip which pixel gets
# the tick.
@image_comparison(['contour_log_extension.png'],
remove_text=True, style='mpl20',
tol=1.444)
def test_contourf_log_extension():
# Remove this line when this test image is regenerated.
plt.rcParams['pcolormesh.snap'] = False
# Test that contourf with lognorm is extended correctly
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(10, 5))
fig.subplots_adjust(left=0.05, right=0.95)
# make data set with large range e.g. between 1e-8 and 1e10
data_exp = np.linspace(-7.5, 9.5, 1200)
data = np.power(10, data_exp).reshape(30, 40)
# make manual levels e.g. between 1e-4 and 1e-6
levels_exp = np.arange(-4., 7.)
levels = np.power(10., levels_exp)
# original data
c1 = ax1.contourf(data,
norm=LogNorm(vmin=data.min(), vmax=data.max()))
# just show data in levels
c2 = ax2.contourf(data, levels=levels,
norm=LogNorm(vmin=levels.min(), vmax=levels.max()),
extend='neither')
# extend data from levels
c3 = ax3.contourf(data, levels=levels,
norm=LogNorm(vmin=levels.min(), vmax=levels.max()),
extend='both')
cb = plt.colorbar(c1, ax=ax1)
assert cb.ax.get_ylim() == (1e-8, 1e10)
cb = plt.colorbar(c2, ax=ax2)
assert_array_almost_equal_nulp(cb.ax.get_ylim(), np.array((1e-4, 1e6)))
cb = plt.colorbar(c3, ax=ax3)
@image_comparison(['contour_addlines.png'], remove_text=True, style='mpl20',
tol=0.03 if platform.machine() == 'x86_64' else 0.15)
# tolerance is because image changed minutely when tick finding on
# colorbars was cleaned up...
def test_contour_addlines():
# Remove this line when this test image is regenerated.
plt.rcParams['pcolormesh.snap'] = False
fig, ax = plt.subplots()
np.random.seed(19680812)
X = np.random.rand(10, 10)*10000
pcm = ax.pcolormesh(X)
# add 1000 to make colors visible...
cont = ax.contour(X+1000)
cb = fig.colorbar(pcm)
cb.add_lines(cont)
assert_array_almost_equal(cb.ax.get_ylim(), [114.3091, 9972.30735], 3)
@image_comparison(baseline_images=['contour_uneven'],
extensions=['png'], remove_text=True, style='mpl20')
def test_contour_uneven():
# Remove this line when this test image is regenerated.
plt.rcParams['pcolormesh.snap'] = False
z = np.arange(24).reshape(4, 6)
fig, axs = plt.subplots(1, 2)
ax = axs[0]
cs = ax.contourf(z, levels=[2, 4, 6, 10, 20])
fig.colorbar(cs, ax=ax, spacing='proportional')
ax = axs[1]
cs = ax.contourf(z, levels=[2, 4, 6, 10, 20])
fig.colorbar(cs, ax=ax, spacing='uniform')
@pytest.mark.parametrize(
"rc_lines_linewidth, rc_contour_linewidth, call_linewidths, expected", [
(1.23, None, None, 1.23),
(1.23, 4.24, None, 4.24),
(1.23, 4.24, 5.02, 5.02)
])
def test_contour_linewidth(
rc_lines_linewidth, rc_contour_linewidth, call_linewidths, expected):
with rc_context(rc={"lines.linewidth": rc_lines_linewidth,
"contour.linewidth": rc_contour_linewidth}):
fig, ax = plt.subplots()
X = np.arange(4*3).reshape(4, 3)
cs = ax.contour(X, linewidths=call_linewidths)
assert cs.get_linewidths()[0] == expected
@pytest.mark.backend("pdf")
def test_label_nonagg():
# This should not crash even if the canvas doesn't have a get_renderer().
plt.clabel(plt.contour([[1, 2], [3, 4]]))
@image_comparison(baseline_images=['contour_closed_line_loop'],
extensions=['png'], remove_text=True)
def test_contour_closed_line_loop():
# github issue 19568.
z = [[0, 0, 0], [0, 2, 0], [0, 0, 0], [2, 1, 2]]
fig, ax = plt.subplots(figsize=(2, 2))
ax.contour(z, [0.5], linewidths=[20], alpha=0.7)
ax.set_xlim(-0.1, 2.1)
ax.set_ylim(-0.1, 3.1)
def test_quadcontourset_reuse():
# If QuadContourSet returned from one contour(f) call is passed as first
# argument to another the underlying C++ contour generator will be reused.
x, y = np.meshgrid([0.0, 1.0], [0.0, 1.0])
z = x + y
fig, ax = plt.subplots()
qcs1 = ax.contourf(x, y, z)
qcs2 = ax.contour(x, y, z)
assert qcs2._contour_generator != qcs1._contour_generator
qcs3 = ax.contour(qcs1, z)
assert qcs3._contour_generator == qcs1._contour_generator
@image_comparison(baseline_images=['contour_manual'],
extensions=['png'], remove_text=True, tol=0.89)
def test_contour_manual():
# Manually specifying contour lines/polygons to plot.
from matplotlib.contour import ContourSet
fig, ax = plt.subplots(figsize=(4, 4))
cmap = 'viridis'
# Segments only (no 'kind' codes).
lines0 = [[[2, 0], [1, 2], [1, 3]]] # Single line.
lines1 = [[[3, 0], [3, 2]], [[3, 3], [3, 4]]] # Two lines.
filled01 = [[[0, 0], [0, 4], [1, 3], [1, 2], [2, 0]]]
filled12 = [[[2, 0], [3, 0], [3, 2], [1, 3], [1, 2]], # Two polygons.
[[1, 4], [3, 4], [3, 3]]]
ContourSet(ax, [0, 1, 2], [filled01, filled12], filled=True, cmap=cmap)
ContourSet(ax, [1, 2], [lines0, lines1], linewidths=3, colors=['r', 'k'])
# Segments and kind codes (1 = MOVETO, 2 = LINETO, 79 = CLOSEPOLY).
segs = [[[4, 0], [7, 0], [7, 3], [4, 3], [4, 0],
[5, 1], [5, 2], [6, 2], [6, 1], [5, 1]]]
kinds = [[1, 2, 2, 2, 79, 1, 2, 2, 2, 79]] # Polygon containing hole.
ContourSet(ax, [2, 3], [segs], [kinds], filled=True, cmap=cmap)
ContourSet(ax, [2], [segs], [kinds], colors='k', linewidths=3)
@image_comparison(baseline_images=['contour_line_start_on_corner_edge'],
extensions=['png'], remove_text=True)
def test_contour_line_start_on_corner_edge():
fig, ax = plt.subplots(figsize=(6, 5))
x, y = np.meshgrid([0, 1, 2, 3, 4], [0, 1, 2])
z = 1.2 - (x - 2)**2 + (y - 1)**2
mask = np.zeros_like(z, dtype=bool)
mask[1, 1] = mask[1, 3] = True
z = np.ma.array(z, mask=mask)
filled = ax.contourf(x, y, z, corner_mask=True)
cbar = fig.colorbar(filled)
lines = ax.contour(x, y, z, corner_mask=True, colors='k')
cbar.add_lines(lines)
def test_find_nearest_contour():
xy = np.indices((15, 15))
img = np.exp(-np.pi * (np.sum((xy - 5)**2, 0)/5.**2))
cs = plt.contour(img, 10)
nearest_contour = cs.find_nearest_contour(1, 1, pixel=False)
expected_nearest = (1, 0, 33, 1.965966, 1.965966, 1.866183)
assert_array_almost_equal(nearest_contour, expected_nearest)
nearest_contour = cs.find_nearest_contour(8, 1, pixel=False)
expected_nearest = (1, 0, 5, 7.550173, 1.587542, 0.547550)
assert_array_almost_equal(nearest_contour, expected_nearest)
nearest_contour = cs.find_nearest_contour(2, 5, pixel=False)
expected_nearest = (3, 0, 21, 1.884384, 5.023335, 0.013911)
assert_array_almost_equal(nearest_contour, expected_nearest)
nearest_contour = cs.find_nearest_contour(2, 5, indices=(5, 7), pixel=False)
expected_nearest = (5, 0, 16, 2.628202, 5.0, 0.394638)
assert_array_almost_equal(nearest_contour, expected_nearest)
def test_find_nearest_contour_no_filled():
xy = np.indices((15, 15))
img = np.exp(-np.pi * (np.sum((xy - 5)**2, 0)/5.**2))
cs = plt.contourf(img, 10)
with pytest.raises(ValueError, match="Method does not support filled contours"):
cs.find_nearest_contour(1, 1, pixel=False)
with pytest.raises(ValueError, match="Method does not support filled contours"):
cs.find_nearest_contour(1, 10, indices=(5, 7), pixel=False)
with pytest.raises(ValueError, match="Method does not support filled contours"):
cs.find_nearest_contour(2, 5, indices=(2, 7), pixel=True)
@mpl.style.context("default")
def test_contour_autolabel_beyond_powerlimits():
ax = plt.figure().add_subplot()
cs = plt.contour(np.geomspace(1e-6, 1e-4, 100).reshape(10, 10),
levels=[.25e-5, 1e-5, 4e-5])
ax.clabel(cs)
# Currently, the exponent is missing, but that may be fixed in the future.
assert {text.get_text() for text in ax.texts} == {"0.25", "1.00", "4.00"}
def test_contourf_legend_elements():
from matplotlib.patches import Rectangle
x = np.arange(1, 10)
y = x.reshape(-1, 1)
h = x * y
cs = plt.contourf(h, levels=[10, 30, 50],
colors=['#FFFF00', '#FF00FF', '#00FFFF'],
extend='both')
cs.cmap.set_over('red')
cs.cmap.set_under('blue')
cs.changed()
artists, labels = cs.legend_elements()
assert labels == ['$x \\leq -1e+250s$',
'$10.0 < x \\leq 30.0$',
'$30.0 < x \\leq 50.0$',
'$x > 1e+250s$']
expected_colors = ('blue', '#FFFF00', '#FF00FF', 'red')
assert all(isinstance(a, Rectangle) for a in artists)
assert all(same_color(a.get_facecolor(), c)
for a, c in zip(artists, expected_colors))
def test_contour_legend_elements():
x = np.arange(1, 10)
y = x.reshape(-1, 1)
h = x * y
colors = ['blue', '#00FF00', 'red']
cs = plt.contour(h, levels=[10, 30, 50],
colors=colors,
extend='both')
artists, labels = cs.legend_elements()
assert labels == ['$x = 10.0$', '$x = 30.0$', '$x = 50.0$']
assert all(isinstance(a, mpl.lines.Line2D) for a in artists)
assert all(same_color(a.get_color(), c)
for a, c in zip(artists, colors))
@pytest.mark.parametrize(
"algorithm, klass",
[('mpl2005', contourpy.Mpl2005ContourGenerator),
('mpl2014', contourpy.Mpl2014ContourGenerator),
('serial', contourpy.SerialContourGenerator),
('threaded', contourpy.ThreadedContourGenerator),
('invalid', None)])
def test_algorithm_name(algorithm, klass):
z = np.array([[1.0, 2.0], [3.0, 4.0]])
if klass is not None:
cs = plt.contourf(z, algorithm=algorithm)
assert isinstance(cs._contour_generator, klass)
else:
with pytest.raises(ValueError):
plt.contourf(z, algorithm=algorithm)
@pytest.mark.parametrize(
"algorithm", ['mpl2005', 'mpl2014', 'serial', 'threaded'])
def test_algorithm_supports_corner_mask(algorithm):
z = np.array([[1.0, 2.0], [3.0, 4.0]])
# All algorithms support corner_mask=False
plt.contourf(z, algorithm=algorithm, corner_mask=False)
# Only some algorithms support corner_mask=True
if algorithm != 'mpl2005':
plt.contourf(z, algorithm=algorithm, corner_mask=True)
else:
with pytest.raises(ValueError):
plt.contourf(z, algorithm=algorithm, corner_mask=True)
@image_comparison(baseline_images=['contour_all_algorithms'],
extensions=['png'], remove_text=True, tol=0.06)
def test_all_algorithms():
algorithms = ['mpl2005', 'mpl2014', 'serial', 'threaded']
rng = np.random.default_rng(2981)
x, y = np.meshgrid(np.linspace(0.0, 1.0, 10), np.linspace(0.0, 1.0, 6))
z = np.sin(15*x)*np.cos(10*y) + rng.normal(scale=0.5, size=(6, 10))
mask = np.zeros_like(z, dtype=bool)
mask[3, 7] = True
z = np.ma.array(z, mask=mask)
_, axs = plt.subplots(2, 2)
for ax, algorithm in zip(axs.ravel(), algorithms):
ax.contourf(x, y, z, algorithm=algorithm)
ax.contour(x, y, z, algorithm=algorithm, colors='k')
ax.set_title(algorithm)
def test_subfigure_clabel():
# Smoke test for gh#23173
delta = 0.025
x = np.arange(-3.0, 3.0, delta)
y = np.arange(-2.0, 2.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-(X**2) - Y**2)
Z2 = np.exp(-((X - 1) ** 2) - (Y - 1) ** 2)
Z = (Z1 - Z2) * 2
fig = plt.figure()
figs = fig.subfigures(nrows=1, ncols=2)
for f in figs:
ax = f.subplots()
CS = ax.contour(X, Y, Z)
ax.clabel(CS, inline=True, fontsize=10)
ax.set_title("Simplest default with labels")
@pytest.mark.parametrize(
"style", ['solid', 'dashed', 'dashdot', 'dotted'])
def test_linestyles(style):
delta = 0.025
x = np.arange(-3.0, 3.0, delta)
y = np.arange(-2.0, 2.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-X**2 - Y**2)
Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
Z = (Z1 - Z2) * 2
# Positive contour defaults to solid
fig1, ax1 = plt.subplots()
CS1 = ax1.contour(X, Y, Z, 6, colors='k')
ax1.clabel(CS1, fontsize=9, inline=True)
ax1.set_title('Single color - positive contours solid (default)')
assert CS1.linestyles is None # default
# Change linestyles using linestyles kwarg
fig2, ax2 = plt.subplots()
CS2 = ax2.contour(X, Y, Z, 6, colors='k', linestyles=style)
ax2.clabel(CS2, fontsize=9, inline=True)
ax2.set_title(f'Single color - positive contours {style}')
assert CS2.linestyles == style
# Ensure linestyles do not change when negative_linestyles is defined
fig3, ax3 = plt.subplots()
CS3 = ax3.contour(X, Y, Z, 6, colors='k', linestyles=style,
negative_linestyles='dashdot')
ax3.clabel(CS3, fontsize=9, inline=True)
ax3.set_title(f'Single color - positive contours {style}')
assert CS3.linestyles == style
@pytest.mark.parametrize(
"style", ['solid', 'dashed', 'dashdot', 'dotted'])
def test_negative_linestyles(style):
delta = 0.025
x = np.arange(-3.0, 3.0, delta)
y = np.arange(-2.0, 2.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-X**2 - Y**2)
Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
Z = (Z1 - Z2) * 2
# Negative contour defaults to dashed
fig1, ax1 = plt.subplots()
CS1 = ax1.contour(X, Y, Z, 6, colors='k')
ax1.clabel(CS1, fontsize=9, inline=True)
ax1.set_title('Single color - negative contours dashed (default)')
assert CS1.negative_linestyles == 'dashed' # default
# Change negative_linestyles using rcParams
plt.rcParams['contour.negative_linestyle'] = style
fig2, ax2 = plt.subplots()
CS2 = ax2.contour(X, Y, Z, 6, colors='k')
ax2.clabel(CS2, fontsize=9, inline=True)
ax2.set_title(f'Single color - negative contours {style}'
'(using rcParams)')
assert CS2.negative_linestyles == style
# Change negative_linestyles using negative_linestyles kwarg
fig3, ax3 = plt.subplots()
CS3 = ax3.contour(X, Y, Z, 6, colors='k', negative_linestyles=style)
ax3.clabel(CS3, fontsize=9, inline=True)
ax3.set_title(f'Single color - negative contours {style}')
assert CS3.negative_linestyles == style
# Ensure negative_linestyles do not change when linestyles is defined
fig4, ax4 = plt.subplots()
CS4 = ax4.contour(X, Y, Z, 6, colors='k', linestyles='dashdot',
negative_linestyles=style)
ax4.clabel(CS4, fontsize=9, inline=True)
ax4.set_title(f'Single color - negative contours {style}')
assert CS4.negative_linestyles == style
def test_contour_remove():
ax = plt.figure().add_subplot()
orig_children = ax.get_children()
cs = ax.contour(np.arange(16).reshape((4, 4)))
cs.clabel()
assert ax.get_children() != orig_children
cs.remove()
assert ax.get_children() == orig_children
def test_contour_no_args():
fig, ax = plt.subplots()
data = [[0, 1], [1, 0]]
with pytest.raises(TypeError, match=r"contour\(\) takes from 1 to 4"):
ax.contour(Z=data)
def test_contour_clip_path():
fig, ax = plt.subplots()
data = [[0, 1], [1, 0]]
circle = mpatches.Circle([0.5, 0.5], 0.5, transform=ax.transAxes)
cs = ax.contour(data, clip_path=circle)
assert cs.get_clip_path() is not None
def test_bool_autolevel():
x, y = np.random.rand(2, 9)
z = (np.arange(9) % 2).reshape((3, 3)).astype(bool)
m = [[False, False, False], [False, True, False], [False, False, False]]
assert plt.contour(z.tolist()).levels.tolist() == [.5]
assert plt.contour(z).levels.tolist() == [.5]
assert plt.contour(np.ma.array(z, mask=m)).levels.tolist() == [.5]
assert plt.contourf(z.tolist()).levels.tolist() == [0, .5, 1]
assert plt.contourf(z).levels.tolist() == [0, .5, 1]
assert plt.contourf(np.ma.array(z, mask=m)).levels.tolist() == [0, .5, 1]
z = z.ravel()
assert plt.tricontour(x, y, z.tolist()).levels.tolist() == [.5]
assert plt.tricontour(x, y, z).levels.tolist() == [.5]
assert plt.tricontourf(x, y, z.tolist()).levels.tolist() == [0, .5, 1]
assert plt.tricontourf(x, y, z).levels.tolist() == [0, .5, 1]
def test_all_nan():
x = np.array([[np.nan, np.nan], [np.nan, np.nan]])
assert_array_almost_equal(plt.contour(x).levels,
[-1e-13, -7.5e-14, -5e-14, -2.4e-14, 0.0,
2.4e-14, 5e-14, 7.5e-14, 1e-13])
def test_allsegs_allkinds():
x, y = np.meshgrid(np.arange(0, 10, 2), np.arange(0, 10, 2))
z = np.sin(x) * np.cos(y)
cs = plt.contour(x, y, z, levels=[0, 0.5])
# Expect two levels, the first with 5 segments and the second with 4.
for result in [cs.allsegs, cs.allkinds]:
assert len(result) == 2
assert len(result[0]) == 5
assert len(result[1]) == 4
@@ -0,0 +1,175 @@
import contextlib
from io import StringIO
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pytest
from cycler import cycler
def test_colorcycle_basic():
fig, ax = plt.subplots()
ax.set_prop_cycle(cycler('color', ['r', 'g', 'y']))
for _ in range(4):
ax.plot(range(10), range(10))
assert [l.get_color() for l in ax.lines] == ['r', 'g', 'y', 'r']
def test_marker_cycle():
fig, ax = plt.subplots()
ax.set_prop_cycle(cycler('c', ['r', 'g', 'y']) +
cycler('marker', ['.', '*', 'x']))
for _ in range(4):
ax.plot(range(10), range(10))
assert [l.get_color() for l in ax.lines] == ['r', 'g', 'y', 'r']
assert [l.get_marker() for l in ax.lines] == ['.', '*', 'x', '.']
def test_valid_marker_cycles():
fig, ax = plt.subplots()
ax.set_prop_cycle(cycler(marker=[1, "+", ".", 4]))
def test_marker_cycle_kwargs_arrays_iterators():
fig, ax = plt.subplots()
ax.set_prop_cycle(c=np.array(['r', 'g', 'y']),
marker=iter(['.', '*', 'x']))
for _ in range(4):
ax.plot(range(10), range(10))
assert [l.get_color() for l in ax.lines] == ['r', 'g', 'y', 'r']
assert [l.get_marker() for l in ax.lines] == ['.', '*', 'x', '.']
def test_linestylecycle_basic():
fig, ax = plt.subplots()
ax.set_prop_cycle(cycler('ls', ['-', '--', ':']))
for _ in range(4):
ax.plot(range(10), range(10))
assert [l.get_linestyle() for l in ax.lines] == ['-', '--', ':', '-']
def test_fillcycle_basic():
fig, ax = plt.subplots()
ax.set_prop_cycle(cycler('c', ['r', 'g', 'y']) +
cycler('hatch', ['xx', 'O', '|-']) +
cycler('linestyle', ['-', '--', ':']))
for _ in range(4):
ax.fill(range(10), range(10))
assert ([p.get_facecolor() for p in ax.patches]
== [mpl.colors.to_rgba(c) for c in ['r', 'g', 'y', 'r']])
assert [p.get_hatch() for p in ax.patches] == ['xx', 'O', '|-', 'xx']
assert [p.get_linestyle() for p in ax.patches] == ['-', '--', ':', '-']
def test_fillcycle_ignore():
fig, ax = plt.subplots()
ax.set_prop_cycle(cycler('color', ['r', 'g', 'y']) +
cycler('hatch', ['xx', 'O', '|-']) +
cycler('marker', ['.', '*', 'D']))
t = range(10)
# Should not advance the cycler, even though there is an
# unspecified property in the cycler "marker".
# "marker" is not a Polygon property, and should be ignored.
ax.fill(t, t, 'r', hatch='xx')
# Allow the cycler to advance, but specify some properties
ax.fill(t, t, hatch='O')
ax.fill(t, t)
ax.fill(t, t)
assert ([p.get_facecolor() for p in ax.patches]
== [mpl.colors.to_rgba(c) for c in ['r', 'r', 'g', 'y']])
assert [p.get_hatch() for p in ax.patches] == ['xx', 'O', 'O', '|-']
def test_property_collision_plot():
fig, ax = plt.subplots()
ax.set_prop_cycle('linewidth', [2, 4])
t = range(10)
for c in range(1, 4):
ax.plot(t, t, lw=0.1)
ax.plot(t, t)
ax.plot(t, t)
assert [l.get_linewidth() for l in ax.lines] == [0.1, 0.1, 0.1, 2, 4]
def test_property_collision_fill():
fig, ax = plt.subplots()
ax.set_prop_cycle(linewidth=[2, 3, 4, 5, 6], facecolor='bgcmy')
t = range(10)
for c in range(1, 4):
ax.fill(t, t, lw=0.1)
ax.fill(t, t)
ax.fill(t, t)
assert ([p.get_facecolor() for p in ax.patches]
== [mpl.colors.to_rgba(c) for c in 'bgcmy'])
assert [p.get_linewidth() for p in ax.patches] == [0.1, 0.1, 0.1, 5, 6]
def test_valid_input_forms():
fig, ax = plt.subplots()
# These should not raise an error.
ax.set_prop_cycle(None)
ax.set_prop_cycle(cycler('linewidth', [1, 2]))
ax.set_prop_cycle('color', 'rgywkbcm')
ax.set_prop_cycle('lw', (1, 2))
ax.set_prop_cycle('linewidth', [1, 2])
ax.set_prop_cycle('linewidth', iter([1, 2]))
ax.set_prop_cycle('linewidth', np.array([1, 2]))
ax.set_prop_cycle('color', np.array([[1, 0, 0],
[0, 1, 0],
[0, 0, 1]]))
ax.set_prop_cycle('dashes', [[], [13, 2], [8, 3, 1, 3]])
ax.set_prop_cycle(lw=[1, 2], color=['k', 'w'], ls=['-', '--'])
ax.set_prop_cycle(lw=np.array([1, 2]),
color=np.array(['k', 'w']),
ls=np.array(['-', '--']))
def test_cycle_reset():
fig, ax = plt.subplots()
prop0 = StringIO()
prop1 = StringIO()
prop2 = StringIO()
with contextlib.redirect_stdout(prop0):
plt.getp(ax.plot([1, 2], label="label")[0])
ax.set_prop_cycle(linewidth=[10, 9, 4])
with contextlib.redirect_stdout(prop1):
plt.getp(ax.plot([1, 2], label="label")[0])
assert prop1.getvalue() != prop0.getvalue()
ax.set_prop_cycle(None)
with contextlib.redirect_stdout(prop2):
plt.getp(ax.plot([1, 2], label="label")[0])
assert prop2.getvalue() == prop0.getvalue()
def test_invalid_input_forms():
fig, ax = plt.subplots()
with pytest.raises((TypeError, ValueError)):
ax.set_prop_cycle(1)
with pytest.raises((TypeError, ValueError)):
ax.set_prop_cycle([1, 2])
with pytest.raises((TypeError, ValueError)):
ax.set_prop_cycle('color', 'fish')
with pytest.raises((TypeError, ValueError)):
ax.set_prop_cycle('linewidth', 1)
with pytest.raises((TypeError, ValueError)):
ax.set_prop_cycle('linewidth', {1, 2})
with pytest.raises((TypeError, ValueError)):
ax.set_prop_cycle(linewidth=1, color='r')
with pytest.raises((TypeError, ValueError)):
ax.set_prop_cycle('foobar', [1, 2])
with pytest.raises((TypeError, ValueError)):
ax.set_prop_cycle(foobar=[1, 2])
with pytest.raises((TypeError, ValueError)):
ax.set_prop_cycle(cycler(foobar=[1, 2]))
with pytest.raises(ValueError):
ax.set_prop_cycle(cycler(color='rgb', c='cmy'))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,865 @@
import datetime
import numpy as np
import pytest
import matplotlib.pyplot as plt
import matplotlib as mpl
class TestDatetimePlotting:
@mpl.style.context("default")
def test_annotate(self):
mpl.rcParams["date.converter"] = 'concise'
fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, layout="constrained")
start_date = datetime.datetime(2023, 10, 1)
dates = [start_date + datetime.timedelta(days=i) for i in range(31)]
data = list(range(1, 32))
test_text = "Test Text"
ax1.plot(dates, data)
ax1.annotate(text=test_text, xy=(dates[15], data[15]))
ax2.plot(data, dates)
ax2.annotate(text=test_text, xy=(data[5], dates[26]))
ax3.plot(dates, dates)
ax3.annotate(text=test_text, xy=(dates[15], dates[3]))
ax4.plot(dates, dates)
ax4.annotate(text=test_text, xy=(dates[5], dates[30]),
xytext=(dates[1], dates[7]), arrowprops=dict(facecolor='red'))
@pytest.mark.xfail(reason="Test for arrow not written yet")
@mpl.style.context("default")
def test_arrow(self):
fig, ax = plt.subplots()
ax.arrow(...)
@mpl.style.context("default")
def test_axhline(self):
mpl.rcParams["date.converter"] = 'concise'
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout='constrained')
ax1.set_ylim(bottom=datetime.datetime(2020, 4, 1),
top=datetime.datetime(2020, 8, 1))
ax2.set_ylim(bottom=np.datetime64('2005-01-01'),
top=np.datetime64('2005-04-01'))
ax3.set_ylim(bottom=datetime.datetime(2023, 9, 1),
top=datetime.datetime(2023, 11, 1))
ax1.axhline(y=datetime.datetime(2020, 6, 3), xmin=0.5, xmax=0.7)
ax2.axhline(np.datetime64('2005-02-25T03:30'), xmin=0.1, xmax=0.9)
ax3.axhline(y=datetime.datetime(2023, 10, 24), xmin=0.4, xmax=0.7)
@mpl.style.context("default")
def test_axhspan(self):
mpl.rcParams["date.converter"] = 'concise'
start_date = datetime.datetime(2023, 1, 1)
dates = [start_date + datetime.timedelta(days=i) for i in range(31)]
numbers = list(range(1, 32))
fig, (ax1, ax2, ax3) = plt.subplots(3, 1,
constrained_layout=True,
figsize=(10, 12))
ax1.plot(dates, numbers, marker='o', color='blue')
for i in range(0, 31, 2):
ax1.axhspan(ymin=i+1, ymax=i+2, facecolor='green', alpha=0.5)
ax1.set_title('Datetime vs. Number')
ax1.set_xlabel('Date')
ax1.set_ylabel('Number')
ax2.plot(numbers, dates, marker='o', color='blue')
for i in range(0, 31, 2):
ymin = start_date + datetime.timedelta(days=i)
ymax = ymin + datetime.timedelta(days=1)
ax2.axhspan(ymin=ymin, ymax=ymax, facecolor='green', alpha=0.5)
ax2.set_title('Number vs. Datetime')
ax2.set_xlabel('Number')
ax2.set_ylabel('Date')
ax3.plot(dates, dates, marker='o', color='blue')
for i in range(0, 31, 2):
ymin = start_date + datetime.timedelta(days=i)
ymax = ymin + datetime.timedelta(days=1)
ax3.axhspan(ymin=ymin, ymax=ymax, facecolor='green', alpha=0.5)
ax3.set_title('Datetime vs. Datetime')
ax3.set_xlabel('Date')
ax3.set_ylabel('Date')
@pytest.mark.xfail(reason="Test for axline not written yet")
@mpl.style.context("default")
def test_axline(self):
fig, ax = plt.subplots()
ax.axline(...)
@mpl.style.context("default")
def test_axvline(self):
mpl.rcParams["date.converter"] = 'concise'
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout='constrained')
ax1.set_xlim(left=datetime.datetime(2020, 4, 1),
right=datetime.datetime(2020, 8, 1))
ax2.set_xlim(left=np.datetime64('2005-01-01'),
right=np.datetime64('2005-04-01'))
ax3.set_xlim(left=datetime.datetime(2023, 9, 1),
right=datetime.datetime(2023, 11, 1))
ax1.axvline(x=datetime.datetime(2020, 6, 3), ymin=0.5, ymax=0.7)
ax2.axvline(np.datetime64('2005-02-25T03:30'), ymin=0.1, ymax=0.9)
ax3.axvline(x=datetime.datetime(2023, 10, 24), ymin=0.4, ymax=0.7)
@mpl.style.context("default")
def test_axvspan(self):
mpl.rcParams["date.converter"] = 'concise'
start_date = datetime.datetime(2023, 1, 1)
dates = [start_date + datetime.timedelta(days=i) for i in range(31)]
numbers = list(range(1, 32))
fig, (ax1, ax2, ax3) = plt.subplots(3, 1,
constrained_layout=True,
figsize=(10, 12))
ax1.plot(dates, numbers, marker='o', color='blue')
for i in range(0, 31, 2):
xmin = start_date + datetime.timedelta(days=i)
xmax = xmin + datetime.timedelta(days=1)
ax1.axvspan(xmin=xmin, xmax=xmax, facecolor='red', alpha=0.5)
ax1.set_title('Datetime vs. Number')
ax1.set_xlabel('Date')
ax1.set_ylabel('Number')
ax2.plot(numbers, dates, marker='o', color='blue')
for i in range(0, 31, 2):
ax2.axvspan(xmin=i+1, xmax=i+2, facecolor='red', alpha=0.5)
ax2.set_title('Number vs. Datetime')
ax2.set_xlabel('Number')
ax2.set_ylabel('Date')
ax3.plot(dates, dates, marker='o', color='blue')
for i in range(0, 31, 2):
xmin = start_date + datetime.timedelta(days=i)
xmax = xmin + datetime.timedelta(days=1)
ax3.axvspan(xmin=xmin, xmax=xmax, facecolor='red', alpha=0.5)
ax3.set_title('Datetime vs. Datetime')
ax3.set_xlabel('Date')
ax3.set_ylabel('Date')
@mpl.style.context("default")
def test_bar(self):
mpl.rcParams["date.converter"] = "concise"
fig, (ax1, ax2) = plt.subplots(2, 1, layout="constrained")
x_dates = np.array(
[
datetime.datetime(2020, 6, 30),
datetime.datetime(2020, 7, 22),
datetime.datetime(2020, 8, 3),
datetime.datetime(2020, 9, 14),
],
dtype=np.datetime64,
)
x_ranges = [8800, 2600, 8500, 7400]
x = np.datetime64(datetime.datetime(2020, 6, 1))
ax1.bar(x_dates, x_ranges, width=np.timedelta64(4, "D"))
ax2.bar(np.arange(4), x_dates - x, bottom=x)
@mpl.style.context("default")
def test_bar_label(self):
# Generate some example data with dateTime inputs
date_list = [datetime.datetime(2023, 1, 1) +
datetime.timedelta(days=i) for i in range(5)]
values = [10, 20, 15, 25, 30]
# Creating the plot
fig, ax = plt.subplots(1, 1, figsize=(10, 8), layout='constrained')
bars = ax.bar(date_list, values)
# Add labels to the bars using bar_label
ax.bar_label(bars, labels=[f'{val}%' for val in values],
label_type='edge', color='black')
@mpl.style.context("default")
def test_barbs(self):
plt.rcParams["date.converter"] = 'concise'
start_date = datetime.datetime(2022, 2, 8, 22)
dates = [start_date + datetime.timedelta(hours=i) for i in range(12)]
numbers = np.sin(np.linspace(0, 2 * np.pi, 12))
u = np.ones(12) * 10
v = np.arange(0, 120, 10)
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 6))
axes[0].barbs(dates, numbers, u, v, length=7)
axes[0].set_title('Datetime vs. Numeric Data')
axes[0].set_xlabel('Datetime')
axes[0].set_ylabel('Numeric Data')
axes[1].barbs(numbers, dates, u, v, length=7)
axes[1].set_title('Numeric vs. Datetime Data')
axes[1].set_xlabel('Numeric Data')
axes[1].set_ylabel('Datetime')
@mpl.style.context("default")
def test_barh(self):
mpl.rcParams["date.converter"] = 'concise'
fig, (ax1, ax2) = plt.subplots(2, 1, layout='constrained')
birth_date = np.array([datetime.datetime(2020, 4, 10),
datetime.datetime(2020, 5, 30),
datetime.datetime(2020, 10, 12),
datetime.datetime(2020, 11, 15)])
year_start = datetime.datetime(2020, 1, 1)
year_end = datetime.datetime(2020, 12, 31)
age = [21, 53, 20, 24]
ax1.set_xlabel('Age')
ax1.set_ylabel('Birth Date')
ax1.barh(birth_date, width=age, height=datetime.timedelta(days=10))
ax2.set_xlim(left=year_start, right=year_end)
ax2.set_xlabel('Birth Date')
ax2.set_ylabel('Order of Birth Dates')
ax2.barh(np.arange(4), birth_date-year_start, left=year_start)
@pytest.mark.xfail(reason="Test for boxplot not written yet")
@mpl.style.context("default")
def test_boxplot(self):
fig, ax = plt.subplots()
ax.boxplot(...)
@mpl.style.context("default")
def test_broken_barh(self):
# Horizontal bar plot with gaps
mpl.rcParams["date.converter"] = 'concise'
fig, ax = plt.subplots()
ax.broken_barh([(datetime.datetime(2023, 1, 4), datetime.timedelta(days=2)),
(datetime.datetime(2023, 1, 8), datetime.timedelta(days=3))],
(10, 9), facecolors='tab:blue')
ax.broken_barh([(datetime.datetime(2023, 1, 2), datetime.timedelta(days=1)),
(datetime.datetime(2023, 1, 4), datetime.timedelta(days=4))],
(20, 9), facecolors=('tab:red'))
@mpl.style.context("default")
def test_bxp(self):
mpl.rcParams["date.converter"] = 'concise'
fig, ax = plt.subplots()
data = [{
"med": datetime.datetime(2020, 1, 15),
"q1": datetime.datetime(2020, 1, 10),
"q3": datetime.datetime(2020, 1, 20),
"whislo": datetime.datetime(2020, 1, 5),
"whishi": datetime.datetime(2020, 1, 25),
"fliers": [
datetime.datetime(2020, 1, 3),
datetime.datetime(2020, 1, 27)
]
}]
ax.bxp(data, orientation='horizontal')
ax.xaxis.set_major_formatter(mpl.dates.DateFormatter("%Y-%m-%d"))
ax.set_title('Box plot with datetime data')
@pytest.mark.xfail(reason="Test for clabel not written yet")
@mpl.style.context("default")
def test_clabel(self):
fig, ax = plt.subplots()
ax.clabel(...)
@mpl.style.context("default")
def test_contour(self):
mpl.rcParams["date.converter"] = "concise"
range_threshold = 10
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout="constrained")
x_dates = np.array(
[datetime.datetime(2023, 10, delta) for delta in range(1, range_threshold)]
)
y_dates = np.array(
[datetime.datetime(2023, 10, delta) for delta in range(1, range_threshold)]
)
x_ranges = np.array(range(1, range_threshold))
y_ranges = np.array(range(1, range_threshold))
X_dates, Y_dates = np.meshgrid(x_dates, y_dates)
X_ranges, Y_ranges = np.meshgrid(x_ranges, y_ranges)
Z_ranges = np.cos(X_ranges / 4) + np.sin(Y_ranges / 4)
ax1.contour(X_dates, Y_dates, Z_ranges)
ax2.contour(X_dates, Y_ranges, Z_ranges)
ax3.contour(X_ranges, Y_dates, Z_ranges)
@mpl.style.context("default")
def test_contourf(self):
mpl.rcParams["date.converter"] = "concise"
range_threshold = 10
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout="constrained")
x_dates = np.array(
[datetime.datetime(2023, 10, delta) for delta in range(1, range_threshold)]
)
y_dates = np.array(
[datetime.datetime(2023, 10, delta) for delta in range(1, range_threshold)]
)
x_ranges = np.array(range(1, range_threshold))
y_ranges = np.array(range(1, range_threshold))
X_dates, Y_dates = np.meshgrid(x_dates, y_dates)
X_ranges, Y_ranges = np.meshgrid(x_ranges, y_ranges)
Z_ranges = np.cos(X_ranges / 4) + np.sin(Y_ranges / 4)
ax1.contourf(X_dates, Y_dates, Z_ranges)
ax2.contourf(X_dates, Y_ranges, Z_ranges)
ax3.contourf(X_ranges, Y_dates, Z_ranges)
@mpl.style.context("default")
def test_errorbar(self):
mpl.rcParams["date.converter"] = "concise"
fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, layout="constrained")
limit = 7
start_date = datetime.datetime(2023, 1, 1)
x_dates = np.array([datetime.datetime(2023, 10, d) for d in range(1, limit)])
y_dates = np.array([datetime.datetime(2023, 10, d) for d in range(1, limit)])
x_date_error = datetime.timedelta(days=1)
y_date_error = datetime.timedelta(days=1)
x_values = list(range(1, limit))
y_values = list(range(1, limit))
x_value_error = 0.5
y_value_error = 0.5
ax1.errorbar(x_dates, y_values,
yerr=y_value_error,
capsize=10,
barsabove=True,
label='Data')
ax2.errorbar(x_values, y_dates,
xerr=x_value_error, yerr=y_date_error,
errorevery=(1, 2),
fmt='-o', label='Data')
ax3.errorbar(x_dates, y_dates,
xerr=x_date_error, yerr=y_date_error,
lolims=True, xlolims=True,
label='Data')
ax4.errorbar(x_dates, y_values,
xerr=x_date_error, yerr=y_value_error,
uplims=True, xuplims=True,
label='Data')
@mpl.style.context("default")
def test_eventplot(self):
mpl.rcParams["date.converter"] = "concise"
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout="constrained")
x_dates1 = np.array([datetime.datetime(2020, 6, 30),
datetime.datetime(2020, 7, 22),
datetime.datetime(2020, 8, 3),
datetime.datetime(2020, 9, 14),],
dtype=np.datetime64,
)
ax1.eventplot(x_dates1)
np.random.seed(19680801)
start_date = datetime.datetime(2020, 7, 1)
end_date = datetime.datetime(2020, 10, 15)
date_range = end_date - start_date
dates1 = start_date + np.random.rand(30) * date_range
dates2 = start_date + np.random.rand(10) * date_range
dates3 = start_date + np.random.rand(50) * date_range
colors1 = ['C1', 'C2', 'C3']
lineoffsets1 = np.array([1, 6, 8])
linelengths1 = [5, 2, 3]
ax2.eventplot([dates1, dates2, dates3],
colors=colors1,
lineoffsets=lineoffsets1,
linelengths=linelengths1)
lineoffsets2 = np.array([
datetime.datetime(2020, 7, 1),
datetime.datetime(2020, 7, 15),
datetime.datetime(2020, 8, 1)
], dtype=np.datetime64)
ax3.eventplot([dates1, dates2, dates3],
colors=colors1,
lineoffsets=lineoffsets2,
linelengths=linelengths1)
@mpl.style.context("default")
def test_fill(self):
mpl.rcParams["date.converter"] = "concise"
fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, layout="constrained")
np.random.seed(19680801)
x_base_date = datetime.datetime(2023, 1, 1)
x_dates = [x_base_date]
for _ in range(1, 5):
x_base_date += datetime.timedelta(days=np.random.randint(1, 5))
x_dates.append(x_base_date)
y_base_date = datetime.datetime(2023, 1, 1)
y_dates = [y_base_date]
for _ in range(1, 5):
y_base_date += datetime.timedelta(days=np.random.randint(1, 5))
y_dates.append(y_base_date)
x_values = np.random.rand(5) * 5
y_values = np.random.rand(5) * 5 - 2
ax1.fill(x_dates, y_values)
ax2.fill(x_values, y_dates)
ax3.fill(x_values, y_values)
ax4.fill(x_dates, y_dates)
@mpl.style.context("default")
def test_fill_between(self):
mpl.rcParams["date.converter"] = "concise"
np.random.seed(19680801)
y_base_date = datetime.datetime(2023, 1, 1)
y_dates1 = [y_base_date]
for i in range(1, 10):
y_base_date += datetime.timedelta(days=np.random.randint(1, 5))
y_dates1.append(y_base_date)
y_dates2 = [y_base_date]
for i in range(1, 10):
y_base_date += datetime.timedelta(days=np.random.randint(1, 5))
y_dates2.append(y_base_date)
x_values = np.random.rand(10) * 10
x_values.sort()
y_values1 = np.random.rand(10) * 10
y_values2 = y_values1 + np.random.rand(10) * 10
y_values1.sort()
y_values2.sort()
x_base_date = datetime.datetime(2023, 1, 1)
x_dates = [x_base_date]
for i in range(1, 10):
x_base_date += datetime.timedelta(days=np.random.randint(1, 10))
x_dates.append(x_base_date)
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout="constrained")
ax1.fill_between(x_values, y_dates1, y_dates2)
ax2.fill_between(x_dates, y_values1, y_values2)
ax3.fill_between(x_dates, y_dates1, y_dates2)
@mpl.style.context("default")
def test_fill_betweenx(self):
mpl.rcParams["date.converter"] = "concise"
np.random.seed(19680801)
x_base_date = datetime.datetime(2023, 1, 1)
x_dates1 = [x_base_date]
for i in range(1, 10):
x_base_date += datetime.timedelta(days=np.random.randint(1, 5))
x_dates1.append(x_base_date)
x_dates2 = [x_base_date]
for i in range(1, 10):
x_base_date += datetime.timedelta(days=np.random.randint(1, 5))
x_dates2.append(x_base_date)
y_values = np.random.rand(10) * 10
y_values.sort()
x_values1 = np.random.rand(10) * 10
x_values2 = x_values1 + np.random.rand(10) * 10
x_values1.sort()
x_values2.sort()
y_base_date = datetime.datetime(2023, 1, 1)
y_dates = [y_base_date]
for i in range(1, 10):
y_base_date += datetime.timedelta(days=np.random.randint(1, 10))
y_dates.append(y_base_date)
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, layout="constrained")
ax1.fill_betweenx(y_values, x_dates1, x_dates2)
ax2.fill_betweenx(y_dates, x_values1, x_values2)
ax3.fill_betweenx(y_dates, x_dates1, x_dates2)
@pytest.mark.xfail(reason="Test for hexbin not written yet")
@mpl.style.context("default")
def test_hexbin(self):
fig, ax = plt.subplots()
ax.hexbin(...)
@mpl.style.context("default")
def test_hist(self):
mpl.rcParams["date.converter"] = 'concise'
start_date = datetime.datetime(2023, 10, 1)
time_delta = datetime.timedelta(days=1)
values1 = np.random.randint(1, 10, 30)
values2 = np.random.randint(1, 10, 30)
values3 = np.random.randint(1, 10, 30)
bin_edges = [start_date + i * time_delta for i in range(31)]
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, constrained_layout=True)
ax1.hist(
[start_date + i * time_delta for i in range(30)],
bins=10,
weights=values1
)
ax2.hist(
[start_date + i * time_delta for i in range(30)],
bins=10,
weights=values2
)
ax3.hist(
[start_date + i * time_delta for i in range(30)],
bins=10,
weights=values3
)
fig, (ax4, ax5, ax6) = plt.subplots(3, 1, constrained_layout=True)
ax4.hist(
[start_date + i * time_delta for i in range(30)],
bins=bin_edges,
weights=values1
)
ax5.hist(
[start_date + i * time_delta for i in range(30)],
bins=bin_edges,
weights=values2
)
ax6.hist(
[start_date + i * time_delta for i in range(30)],
bins=bin_edges,
weights=values3
)
@pytest.mark.xfail(reason="Test for hist2d not written yet")
@mpl.style.context("default")
def test_hist2d(self):
fig, ax = plt.subplots()
ax.hist2d(...)
@mpl.style.context("default")
def test_hlines(self):
mpl.rcParams["date.converter"] = 'concise'
fig, axs = plt.subplots(2, 4, layout='constrained')
dateStrs = ['2023-03-08',
'2023-04-09',
'2023-05-13',
'2023-07-28',
'2023-12-24']
dates = [datetime.datetime(2023, m*2, 10) for m in range(1, 6)]
date_start = [datetime.datetime(2023, 6, d) for d in range(5, 30, 5)]
date_end = [datetime.datetime(2023, 7, d) for d in range(5, 30, 5)]
npDates = [np.datetime64(s) for s in dateStrs]
axs[0, 0].hlines(y=dates,
xmin=[0.1, 0.2, 0.3, 0.4, 0.5],
xmax=[0.5, 0.6, 0.7, 0.8, 0.9])
axs[0, 1].hlines(dates,
xmin=datetime.datetime(2020, 5, 10),
xmax=datetime.datetime(2020, 5, 31))
axs[0, 2].hlines(dates,
xmin=date_start,
xmax=date_end)
axs[0, 3].hlines(dates,
xmin=0.45,
xmax=0.65)
axs[1, 0].hlines(y=npDates,
xmin=[0.5, 0.6, 0.7, 0.8, 0.9],
xmax=[0.1, 0.2, 0.3, 0.4, 0.5])
axs[1, 2].hlines(y=npDates,
xmin=date_start,
xmax=date_end)
axs[1, 1].hlines(npDates,
xmin=datetime.datetime(2020, 5, 10),
xmax=datetime.datetime(2020, 5, 31))
axs[1, 3].hlines(npDates,
xmin=0.45,
xmax=0.65)
@mpl.style.context("default")
def test_imshow(self):
fig, ax = plt.subplots()
a = np.diag(range(5))
dt_start = datetime.datetime(2010, 11, 1)
dt_end = datetime.datetime(2010, 11, 11)
extent = (dt_start, dt_end, dt_start, dt_end)
ax.imshow(a, extent=extent)
ax.tick_params(axis="x", labelrotation=90)
@pytest.mark.xfail(reason="Test for loglog not written yet")
@mpl.style.context("default")
def test_loglog(self):
fig, ax = plt.subplots()
ax.loglog(...)
@mpl.style.context("default")
def test_matshow(self):
a = np.diag(range(5))
dt_start = datetime.datetime(1980, 4, 15)
dt_end = datetime.datetime(2020, 11, 11)
extent = (dt_start, dt_end, dt_start, dt_end)
fig, ax = plt.subplots()
ax.matshow(a, extent=extent)
for label in ax.get_xticklabels():
label.set_rotation(90)
@pytest.mark.xfail(reason="Test for pcolor not written yet")
@mpl.style.context("default")
def test_pcolor(self):
fig, ax = plt.subplots()
ax.pcolor(...)
@pytest.mark.xfail(reason="Test for pcolorfast not written yet")
@mpl.style.context("default")
def test_pcolorfast(self):
fig, ax = plt.subplots()
ax.pcolorfast(...)
@pytest.mark.xfail(reason="Test for pcolormesh not written yet")
@mpl.style.context("default")
def test_pcolormesh(self):
fig, ax = plt.subplots()
ax.pcolormesh(...)
@mpl.style.context("default")
def test_plot(self):
mpl.rcParams["date.converter"] = 'concise'
N = 6
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout='constrained')
x = np.array([datetime.datetime(2023, 9, n) for n in range(1, N)])
ax1.plot(x, range(1, N))
ax2.plot(range(1, N), x)
ax3.plot(x, x)
@mpl.style.context("default")
def test_plot_date(self):
mpl.rcParams["date.converter"] = "concise"
range_threshold = 10
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout="constrained")
x_dates = np.array(
[datetime.datetime(2023, 10, delta) for delta in range(1, range_threshold)]
)
y_dates = np.array(
[datetime.datetime(2023, 10, delta) for delta in range(1, range_threshold)]
)
x_ranges = np.array(range(1, range_threshold))
y_ranges = np.array(range(1, range_threshold))
with pytest.warns(mpl.MatplotlibDeprecationWarning):
ax1.plot_date(x_dates, y_dates)
ax2.plot_date(x_dates, y_ranges)
ax3.plot_date(x_ranges, y_dates)
@pytest.mark.xfail(reason="Test for quiver not written yet")
@mpl.style.context("default")
def test_quiver(self):
fig, ax = plt.subplots()
ax.quiver(...)
@mpl.style.context("default")
def test_scatter(self):
mpl.rcParams["date.converter"] = 'concise'
base = datetime.datetime(2005, 2, 1)
dates = [base + datetime.timedelta(hours=(2 * i)) for i in range(10)]
N = len(dates)
np.random.seed(19680801)
y = np.cumsum(np.random.randn(N))
fig, axs = plt.subplots(3, 1, layout='constrained', figsize=(6, 6))
# datetime array on x axis
axs[0].scatter(dates, y)
for label in axs[0].get_xticklabels():
label.set_rotation(40)
label.set_horizontalalignment('right')
# datetime on y axis
axs[1].scatter(y, dates)
# datetime on both x, y axes
axs[2].scatter(dates, dates)
for label in axs[2].get_xticklabels():
label.set_rotation(40)
label.set_horizontalalignment('right')
@pytest.mark.xfail(reason="Test for semilogx not written yet")
@mpl.style.context("default")
def test_semilogx(self):
fig, ax = plt.subplots()
ax.semilogx(...)
@pytest.mark.xfail(reason="Test for semilogy not written yet")
@mpl.style.context("default")
def test_semilogy(self):
fig, ax = plt.subplots()
ax.semilogy(...)
@mpl.style.context("default")
def test_stackplot(self):
mpl.rcParams["date.converter"] = 'concise'
N = 10
stacked_nums = np.tile(np.arange(1, N), (4, 1))
dates = np.array([datetime.datetime(2020 + i, 1, 1) for i in range(N - 1)])
fig, ax = plt.subplots(layout='constrained')
ax.stackplot(dates, stacked_nums)
@mpl.style.context("default")
def test_stairs(self):
mpl.rcParams["date.converter"] = 'concise'
start_date = datetime.datetime(2023, 12, 1)
time_delta = datetime.timedelta(days=1)
baseline_date = datetime.datetime(1980, 1, 1)
bin_edges = [start_date + i * time_delta for i in range(31)]
edge_int = np.arange(31)
np.random.seed(123456)
values1 = np.random.randint(1, 100, 30)
values2 = [start_date + datetime.timedelta(days=int(i))
for i in np.random.randint(1, 10000, 30)]
values3 = [start_date + datetime.timedelta(days=int(i))
for i in np.random.randint(-10000, 10000, 30)]
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, constrained_layout=True)
ax1.stairs(values1, edges=bin_edges)
ax2.stairs(values2, edges=edge_int, baseline=baseline_date)
ax3.stairs(values3, edges=bin_edges, baseline=baseline_date)
@mpl.style.context("default")
def test_stem(self):
mpl.rcParams["date.converter"] = "concise"
fig, (ax1, ax2, ax3, ax4, ax5, ax6) = plt.subplots(6, 1, layout="constrained")
limit_value = 10
above = datetime.datetime(2023, 9, 18)
below = datetime.datetime(2023, 11, 18)
x_ranges = np.arange(1, limit_value)
y_ranges = np.arange(1, limit_value)
x_dates = np.array(
[datetime.datetime(2023, 10, n) for n in range(1, limit_value)]
)
y_dates = np.array(
[datetime.datetime(2023, 10, n) for n in range(1, limit_value)]
)
ax1.stem(x_dates, y_dates, bottom=above)
ax2.stem(x_dates, y_ranges, bottom=5)
ax3.stem(x_ranges, y_dates, bottom=below)
ax4.stem(x_ranges, y_dates, orientation="horizontal", bottom=above)
ax5.stem(x_dates, y_ranges, orientation="horizontal", bottom=5)
ax6.stem(x_ranges, y_dates, orientation="horizontal", bottom=below)
@mpl.style.context("default")
def test_step(self):
mpl.rcParams["date.converter"] = "concise"
N = 6
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout='constrained')
x = np.array([datetime.datetime(2023, 9, n) for n in range(1, N)])
ax1.step(x, range(1, N))
ax2.step(range(1, N), x)
ax3.step(x, x)
@pytest.mark.xfail(reason="Test for streamplot not written yet")
@mpl.style.context("default")
def test_streamplot(self):
fig, ax = plt.subplots()
ax.streamplot(...)
@mpl.style.context("default")
def test_text(self):
mpl.rcParams["date.converter"] = 'concise'
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout="constrained")
limit_value = 10
font_properties = {'family': 'serif', 'size': 12, 'weight': 'bold'}
test_date = datetime.datetime(2023, 10, 1)
x_data = np.array(range(1, limit_value))
y_data = np.array(range(1, limit_value))
x_dates = np.array(
[datetime.datetime(2023, 10, n) for n in range(1, limit_value)]
)
y_dates = np.array(
[datetime.datetime(2023, 10, n) for n in range(1, limit_value)]
)
ax1.plot(x_dates, y_data)
ax1.text(test_date, 5, "Inserted Text", **font_properties)
ax2.plot(x_data, y_dates)
ax2.text(7, test_date, "Inserted Text", **font_properties)
ax3.plot(x_dates, y_dates)
ax3.text(test_date, test_date, "Inserted Text", **font_properties)
@pytest.mark.xfail(reason="Test for tricontour not written yet")
@mpl.style.context("default")
def test_tricontour(self):
fig, ax = plt.subplots()
ax.tricontour(...)
@pytest.mark.xfail(reason="Test for tricontourf not written yet")
@mpl.style.context("default")
def test_tricontourf(self):
fig, ax = plt.subplots()
ax.tricontourf(...)
@pytest.mark.xfail(reason="Test for tripcolor not written yet")
@mpl.style.context("default")
def test_tripcolor(self):
fig, ax = plt.subplots()
ax.tripcolor(...)
@pytest.mark.xfail(reason="Test for triplot not written yet")
@mpl.style.context("default")
def test_triplot(self):
fig, ax = plt.subplots()
ax.triplot(...)
@pytest.mark.xfail(reason="Test for violin not written yet")
@mpl.style.context("default")
def test_violin(self):
fig, ax = plt.subplots()
ax.violin(...)
@pytest.mark.xfail(reason="Test for violinplot not written yet")
@mpl.style.context("default")
def test_violinplot(self):
fig, ax = plt.subplots()
ax.violinplot(...)
@mpl.style.context("default")
def test_vlines(self):
mpl.rcParams["date.converter"] = 'concise'
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout='constrained')
ax1.set_xlim(left=datetime.datetime(2023, 1, 1),
right=datetime.datetime(2023, 6, 30))
ax1.vlines(x=[datetime.datetime(2023, 2, 10),
datetime.datetime(2023, 5, 18),
datetime.datetime(2023, 6, 6)],
ymin=[0, 0.25, 0.5],
ymax=[0.25, 0.5, 0.75])
ax2.set_xlim(left=0,
right=0.5)
ax2.vlines(x=[0.3, 0.35],
ymin=[np.datetime64('2023-03-20'), np.datetime64('2023-03-31')],
ymax=[np.datetime64('2023-05-01'), np.datetime64('2023-05-16')])
ax3.set_xlim(left=datetime.datetime(2023, 7, 1),
right=datetime.datetime(2023, 12, 31))
ax3.vlines(x=[datetime.datetime(2023, 9, 1), datetime.datetime(2023, 12, 10)],
ymin=datetime.datetime(2023, 1, 15),
ymax=datetime.datetime(2023, 1, 30))
@@ -0,0 +1,218 @@
"""
Test output reproducibility.
"""
import os
import sys
import pytest
import matplotlib as mpl
from matplotlib import pyplot as plt
from matplotlib.cbook import get_sample_data
from matplotlib.collections import PathCollection
from matplotlib.image import BboxImage
from matplotlib.offsetbox import AnchoredOffsetbox, AuxTransformBox
from matplotlib.patches import Circle, PathPatch
from matplotlib.path import Path
from matplotlib.testing import subprocess_run_for_testing
from matplotlib.testing._markers import needs_ghostscript, needs_usetex
import matplotlib.testing.compare
from matplotlib.text import TextPath
from matplotlib.transforms import IdentityTransform
def _save_figure(objects='mhip', fmt="pdf", usetex=False):
mpl.use(fmt)
mpl.rcParams.update({'svg.hashsalt': 'asdf', 'text.usetex': usetex})
fig = plt.figure()
if 'm' in objects:
# use different markers...
ax1 = fig.add_subplot(1, 6, 1)
x = range(10)
ax1.plot(x, [1] * 10, marker='D')
ax1.plot(x, [2] * 10, marker='x')
ax1.plot(x, [3] * 10, marker='^')
ax1.plot(x, [4] * 10, marker='H')
ax1.plot(x, [5] * 10, marker='v')
if 'h' in objects:
# also use different hatch patterns
ax2 = fig.add_subplot(1, 6, 2)
bars = (ax2.bar(range(1, 5), range(1, 5)) +
ax2.bar(range(1, 5), [6] * 4, bottom=range(1, 5)))
ax2.set_xticks([1.5, 2.5, 3.5, 4.5])
patterns = ('-', '+', 'x', '\\', '*', 'o', 'O', '.')
for bar, pattern in zip(bars, patterns):
bar.set_hatch(pattern)
if 'i' in objects:
# also use different images
A = [[1, 2, 3], [2, 3, 1], [3, 1, 2]]
fig.add_subplot(1, 6, 3).imshow(A, interpolation='nearest')
A = [[1, 3, 2], [1, 2, 3], [3, 1, 2]]
fig.add_subplot(1, 6, 4).imshow(A, interpolation='bilinear')
A = [[2, 3, 1], [1, 2, 3], [2, 1, 3]]
fig.add_subplot(1, 6, 5).imshow(A, interpolation='bicubic')
if 'p' in objects:
# clipping support class, copied from demo_text_path.py gallery example
class PathClippedImagePatch(PathPatch):
"""
The given image is used to draw the face of the patch. Internally,
it uses BboxImage whose clippath set to the path of the patch.
FIXME : The result is currently dpi dependent.
"""
def __init__(self, path, bbox_image, **kwargs):
super().__init__(path, **kwargs)
self.bbox_image = BboxImage(
self.get_window_extent, norm=None, origin=None)
self.bbox_image.set_data(bbox_image)
def set_facecolor(self, color):
"""Simply ignore facecolor."""
super().set_facecolor("none")
def draw(self, renderer=None):
# the clip path must be updated every draw. any solution? -JJ
self.bbox_image.set_clip_path(self._path, self.get_transform())
self.bbox_image.draw(renderer)
super().draw(renderer)
# add a polar projection
px = fig.add_subplot(projection="polar")
pimg = px.imshow([[2]])
pimg.set_clip_path(Circle((0, 1), radius=0.3333))
# add a text-based clipping path (origin: demo_text_path.py)
(ax1, ax2) = fig.subplots(2)
arr = plt.imread(get_sample_data("grace_hopper.jpg"))
text_path = TextPath((0, 0), "!?", size=150)
p = PathClippedImagePatch(text_path, arr, ec="k")
offsetbox = AuxTransformBox(IdentityTransform())
offsetbox.add_artist(p)
ao = AnchoredOffsetbox(loc='upper left', child=offsetbox, frameon=True,
borderpad=0.2)
ax1.add_artist(ao)
# add a 2x2 grid of path-clipped axes (origin: test_artist.py)
exterior = Path.unit_rectangle().deepcopy()
exterior.vertices *= 4
exterior.vertices -= 2
interior = Path.unit_circle().deepcopy()
interior.vertices = interior.vertices[::-1]
clip_path = Path.make_compound_path(exterior, interior)
star = Path.unit_regular_star(6).deepcopy()
star.vertices *= 2.6
(row1, row2) = fig.subplots(2, 2, sharex=True, sharey=True)
for row in (row1, row2):
ax1, ax2 = row
collection = PathCollection([star], lw=5, edgecolor='blue',
facecolor='red', alpha=0.7, hatch='*')
collection.set_clip_path(clip_path, ax1.transData)
ax1.add_collection(collection)
patch = PathPatch(star, lw=5, edgecolor='blue', facecolor='red',
alpha=0.7, hatch='*')
patch.set_clip_path(clip_path, ax2.transData)
ax2.add_patch(patch)
ax1.set_xlim([-3, 3])
ax1.set_ylim([-3, 3])
x = range(5)
ax = fig.add_subplot(1, 6, 6)
ax.plot(x, x)
ax.set_title('A string $1+2+\\sigma$')
ax.set_xlabel('A string $1+2+\\sigma$')
ax.set_ylabel('A string $1+2+\\sigma$')
stdout = getattr(sys.stdout, 'buffer', sys.stdout)
fig.savefig(stdout, format=fmt)
@pytest.mark.parametrize(
"objects, fmt, usetex", [
("", "pdf", False),
("m", "pdf", False),
("h", "pdf", False),
("i", "pdf", False),
("mhip", "pdf", False),
("mhip", "ps", False),
pytest.param(
"mhip", "ps", True, marks=[needs_usetex, needs_ghostscript]),
("p", "svg", False),
("mhip", "svg", False),
pytest.param("mhip", "svg", True, marks=needs_usetex),
]
)
def test_determinism_check(objects, fmt, usetex):
"""
Output three times the same graphs and checks that the outputs are exactly
the same.
Parameters
----------
objects : str
Objects to be included in the test document: 'm' for markers, 'h' for
hatch patterns, 'i' for images, and 'p' for paths.
fmt : {"pdf", "ps", "svg"}
Output format.
"""
plots = [
subprocess_run_for_testing(
[sys.executable, "-R", "-c",
f"from matplotlib.tests.test_determinism import _save_figure;"
f"_save_figure({objects!r}, {fmt!r}, {usetex})"],
env={**os.environ, "SOURCE_DATE_EPOCH": "946684800",
"MPLBACKEND": "Agg"},
text=False, capture_output=True, check=True).stdout
for _ in range(3)
]
for p in plots[1:]:
if fmt == "ps" and usetex:
if p != plots[0]:
pytest.skip("failed, maybe due to ghostscript timestamps")
else:
assert p == plots[0]
@pytest.mark.parametrize(
"fmt, string", [
("pdf", b"/CreationDate (D:20000101000000Z)"),
# SOURCE_DATE_EPOCH support is not tested with text.usetex,
# because the produced timestamp comes from ghostscript:
# %%CreationDate: D:20000101000000Z00\'00\', and this could change
# with another ghostscript version.
("ps", b"%%CreationDate: Sat Jan 01 00:00:00 2000"),
]
)
def test_determinism_source_date_epoch(fmt, string):
"""
Test SOURCE_DATE_EPOCH support. Output a document with the environment
variable SOURCE_DATE_EPOCH set to 2000-01-01 00:00 UTC and check that the
document contains the timestamp that corresponds to this date (given as an
argument).
Parameters
----------
fmt : {"pdf", "ps", "svg"}
Output format.
string : bytes
Timestamp string for 2000-01-01 00:00 UTC.
"""
buf = subprocess_run_for_testing(
[sys.executable, "-R", "-c",
f"from matplotlib.tests.test_determinism import _save_figure; "
f"_save_figure('', {fmt!r})"],
env={**os.environ, "SOURCE_DATE_EPOCH": "946684800",
"MPLBACKEND": "Agg"}, capture_output=True, text=False, check=True).stdout
assert string in buf
@@ -0,0 +1,35 @@
import pytest
def test_sphinx_gallery_example_header():
"""
We have copied EXAMPLE_HEADER and modified it to include meta keywords.
This test monitors that the version we have copied is still the same as
the EXAMPLE_HEADER in sphinx-gallery. If sphinx-gallery changes its
EXAMPLE_HEADER, this test will start to fail. In that case, please update
the monkey-patching of EXAMPLE_HEADER in conf.py.
"""
pytest.importorskip('sphinx_gallery', minversion='0.16.0')
from sphinx_gallery import gen_rst
EXAMPLE_HEADER = """
.. DO NOT EDIT.
.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.
.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE:
.. "{0}"
.. LINE NUMBERS ARE GIVEN BELOW.
.. only:: html
.. note::
:class: sphx-glr-download-link-note
:ref:`Go to the end <sphx_glr_download_{1}>`
to download the full example code.{2}
.. rst-class:: sphx-glr-example-title
.. _sphx_glr_{1}:
"""
assert gen_rst.EXAMPLE_HEADER == EXAMPLE_HEADER
@@ -0,0 +1,77 @@
import json
from pathlib import Path
import shutil
import matplotlib.dviread as dr
import pytest
def test_PsfontsMap(monkeypatch):
monkeypatch.setattr(dr, 'find_tex_file', lambda x: x.decode())
filename = str(Path(__file__).parent / 'baseline_images/dviread/test.map')
fontmap = dr.PsfontsMap(filename)
# Check all properties of a few fonts
for n in [1, 2, 3, 4, 5]:
key = b'TeXfont%d' % n
entry = fontmap[key]
assert entry.texname == key
assert entry.psname == b'PSfont%d' % n
if n not in [3, 5]:
assert entry.encoding == 'font%d.enc' % n
elif n == 3:
assert entry.encoding == 'enc3.foo'
# We don't care about the encoding of TeXfont5, which specifies
# multiple encodings.
if n not in [1, 5]:
assert entry.filename == 'font%d.pfa' % n
else:
assert entry.filename == 'font%d.pfb' % n
if n == 4:
assert entry.effects == {'slant': -0.1, 'extend': 1.2}
else:
assert entry.effects == {}
# Some special cases
entry = fontmap[b'TeXfont6']
assert entry.filename is None
assert entry.encoding is None
entry = fontmap[b'TeXfont7']
assert entry.filename is None
assert entry.encoding == 'font7.enc'
entry = fontmap[b'TeXfont8']
assert entry.filename == 'font8.pfb'
assert entry.encoding is None
entry = fontmap[b'TeXfont9']
assert entry.psname == b'TeXfont9'
assert entry.filename == '/absolute/font9.pfb'
# First of duplicates only.
entry = fontmap[b'TeXfontA']
assert entry.psname == b'PSfontA1'
# Slant/Extend only works for T1 fonts.
entry = fontmap[b'TeXfontB']
assert entry.psname == b'PSfontB6'
# Subsetted TrueType must have encoding.
entry = fontmap[b'TeXfontC']
assert entry.psname == b'PSfontC3'
# Missing font
with pytest.raises(LookupError, match='no-such-font'):
fontmap[b'no-such-font']
with pytest.raises(LookupError, match='%'):
fontmap[b'%']
@pytest.mark.skipif(shutil.which("kpsewhich") is None,
reason="kpsewhich is not available")
def test_dviread():
dirpath = Path(__file__).parent / 'baseline_images/dviread'
with (dirpath / 'test.json').open() as f:
correct = json.load(f)
with dr.Dvi(str(dirpath / 'test.dvi'), None) as dvi:
data = [{'text': [[t.x, t.y,
chr(t.glyph),
t.font.texname.decode('ascii'),
round(t.font.size, 2)]
for t in page.text],
'boxes': [[b.x, b.y, b.height, b.width] for b in page.boxes]}
for page in dvi]
assert data == correct
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,409 @@
from io import BytesIO, StringIO
import gc
import multiprocessing
import os
from pathlib import Path
from PIL import Image
import shutil
import sys
import warnings
import numpy as np
import pytest
import matplotlib as mpl
from matplotlib.font_manager import (
findfont, findSystemFonts, FontEntry, FontProperties, fontManager,
json_dump, json_load, get_font, is_opentype_cff_font,
MSUserFontDirectories, _get_fontconfig_fonts, ttfFontProperty)
from matplotlib import cbook, ft2font, pyplot as plt, rc_context, figure as mfigure
from matplotlib.testing import subprocess_run_helper, subprocess_run_for_testing
has_fclist = shutil.which('fc-list') is not None
def test_font_priority():
with rc_context(rc={
'font.sans-serif':
['cmmi10', 'Bitstream Vera Sans']}):
fontfile = findfont(FontProperties(family=["sans-serif"]))
assert Path(fontfile).name == 'cmmi10.ttf'
# Smoketest get_charmap, which isn't used internally anymore
font = get_font(fontfile)
cmap = font.get_charmap()
assert len(cmap) == 131
assert cmap[8729] == 30
def test_score_weight():
assert 0 == fontManager.score_weight("regular", "regular")
assert 0 == fontManager.score_weight("bold", "bold")
assert (0 < fontManager.score_weight(400, 400) <
fontManager.score_weight("normal", "bold"))
assert (0 < fontManager.score_weight("normal", "regular") <
fontManager.score_weight("normal", "bold"))
assert (fontManager.score_weight("normal", "regular") ==
fontManager.score_weight(400, 400))
def test_json_serialization(tmp_path):
# Can't open a NamedTemporaryFile twice on Windows, so use a temporary
# directory instead.
json_dump(fontManager, tmp_path / "fontlist.json")
copy = json_load(tmp_path / "fontlist.json")
with warnings.catch_warnings():
warnings.filterwarnings('ignore', 'findfont: Font family.*not found')
for prop in ({'family': 'STIXGeneral'},
{'family': 'Bitstream Vera Sans', 'weight': 700},
{'family': 'no such font family'}):
fp = FontProperties(**prop)
assert (fontManager.findfont(fp, rebuild_if_missing=False) ==
copy.findfont(fp, rebuild_if_missing=False))
def test_otf():
fname = '/usr/share/fonts/opentype/freefont/FreeMono.otf'
if Path(fname).exists():
assert is_opentype_cff_font(fname)
for f in fontManager.ttflist:
if 'otf' in f.fname:
with open(f.fname, 'rb') as fd:
res = fd.read(4) == b'OTTO'
assert res == is_opentype_cff_font(f.fname)
@pytest.mark.skipif(sys.platform == "win32" or not has_fclist,
reason='no fontconfig installed')
def test_get_fontconfig_fonts():
assert len(_get_fontconfig_fonts()) > 1
@pytest.mark.parametrize('factor', [2, 4, 6, 8])
def test_hinting_factor(factor):
font = findfont(FontProperties(family=["sans-serif"]))
font1 = get_font(font, hinting_factor=1)
font1.clear()
font1.set_size(12, 100)
font1.set_text('abc')
expected = font1.get_width_height()
hinted_font = get_font(font, hinting_factor=factor)
hinted_font.clear()
hinted_font.set_size(12, 100)
hinted_font.set_text('abc')
# Check that hinting only changes text layout by a small (10%) amount.
np.testing.assert_allclose(hinted_font.get_width_height(), expected,
rtol=0.1)
def test_utf16m_sfnt():
try:
# seguisbi = Microsoft Segoe UI Semibold
entry = next(entry for entry in fontManager.ttflist
if Path(entry.fname).name == "seguisbi.ttf")
except StopIteration:
pytest.skip("Couldn't find seguisbi.ttf font to test against.")
else:
# Check that we successfully read "semibold" from the font's sfnt table
# and set its weight accordingly.
assert entry.weight == 600
def test_find_ttc():
fp = FontProperties(family=["WenQuanYi Zen Hei"])
if Path(findfont(fp)).name != "wqy-zenhei.ttc":
pytest.skip("Font wqy-zenhei.ttc may be missing")
fig, ax = plt.subplots()
ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp)
for fmt in ["raw", "svg", "pdf", "ps"]:
fig.savefig(BytesIO(), format=fmt)
def test_find_noto():
fp = FontProperties(family=["Noto Sans CJK SC", "Noto Sans CJK JP"])
name = Path(findfont(fp)).name
if name not in ("NotoSansCJKsc-Regular.otf", "NotoSansCJK-Regular.ttc"):
pytest.skip(f"Noto Sans CJK SC font may be missing (found {name})")
fig, ax = plt.subplots()
ax.text(0.5, 0.5, 'Hello, 你好', fontproperties=fp)
for fmt in ["raw", "svg", "pdf", "ps"]:
fig.savefig(BytesIO(), format=fmt)
def test_find_invalid(tmp_path):
with pytest.raises(FileNotFoundError):
get_font(tmp_path / 'non-existent-font-name.ttf')
with pytest.raises(FileNotFoundError):
get_font(str(tmp_path / 'non-existent-font-name.ttf'))
with pytest.raises(FileNotFoundError):
get_font(bytes(tmp_path / 'non-existent-font-name.ttf'))
# Not really public, but get_font doesn't expose non-filename constructor.
from matplotlib.ft2font import FT2Font
with pytest.raises(TypeError, match='font file or a binary-mode file'):
FT2Font(StringIO()) # type: ignore[arg-type]
@pytest.mark.skipif(sys.platform != 'linux' or not has_fclist,
reason='only Linux with fontconfig installed')
def test_user_fonts_linux(tmpdir, monkeypatch):
font_test_file = 'mpltest.ttf'
# Precondition: the test font should not be available
fonts = findSystemFonts()
if any(font_test_file in font for font in fonts):
pytest.skip(f'{font_test_file} already exists in system fonts')
# Prepare a temporary user font directory
user_fonts_dir = tmpdir.join('fonts')
user_fonts_dir.ensure(dir=True)
shutil.copyfile(Path(__file__).parent / font_test_file,
user_fonts_dir.join(font_test_file))
with monkeypatch.context() as m:
m.setenv('XDG_DATA_HOME', str(tmpdir))
_get_fontconfig_fonts.cache_clear()
# Now, the font should be available
fonts = findSystemFonts()
assert any(font_test_file in font for font in fonts)
# Make sure the temporary directory is no longer cached.
_get_fontconfig_fonts.cache_clear()
def test_addfont_as_path():
"""Smoke test that addfont() accepts pathlib.Path."""
font_test_file = 'mpltest.ttf'
path = Path(__file__).parent / font_test_file
try:
fontManager.addfont(path)
added, = (font for font in fontManager.ttflist
if font.fname.endswith(font_test_file))
fontManager.ttflist.remove(added)
finally:
to_remove = [font for font in fontManager.ttflist
if font.fname.endswith(font_test_file)]
for font in to_remove:
fontManager.ttflist.remove(font)
@pytest.mark.skipif(sys.platform != 'win32', reason='Windows only')
def test_user_fonts_win32():
if not (os.environ.get('APPVEYOR') or os.environ.get('TF_BUILD')):
pytest.xfail("This test should only run on CI (appveyor or azure) "
"as the developer's font directory should remain "
"unchanged.")
pytest.xfail("We need to update the registry for this test to work")
font_test_file = 'mpltest.ttf'
# Precondition: the test font should not be available
fonts = findSystemFonts()
if any(font_test_file in font for font in fonts):
pytest.skip(f'{font_test_file} already exists in system fonts')
user_fonts_dir = MSUserFontDirectories[0]
# Make sure that the user font directory exists (this is probably not the
# case on Windows versions < 1809)
os.makedirs(user_fonts_dir)
# Copy the test font to the user font directory
shutil.copy(Path(__file__).parent / font_test_file, user_fonts_dir)
# Now, the font should be available
fonts = findSystemFonts()
assert any(font_test_file in font for font in fonts)
def _model_handler(_):
fig, ax = plt.subplots()
fig.savefig(BytesIO(), format="pdf")
plt.close()
@pytest.mark.skipif(not hasattr(os, "register_at_fork"),
reason="Cannot register at_fork handlers")
def test_fork():
_model_handler(0) # Make sure the font cache is filled.
ctx = multiprocessing.get_context("fork")
with ctx.Pool(processes=2) as pool:
pool.map(_model_handler, range(2))
def test_missing_family(caplog):
plt.rcParams["font.sans-serif"] = ["this-font-does-not-exist"]
with caplog.at_level("WARNING"):
findfont("sans")
assert [rec.getMessage() for rec in caplog.records] == [
"findfont: Font family ['sans'] not found. "
"Falling back to DejaVu Sans.",
"findfont: Generic family 'sans' not found because none of the "
"following families were found: this-font-does-not-exist",
]
def _test_threading():
import threading
from matplotlib.ft2font import LoadFlags
import matplotlib.font_manager as fm
def loud_excepthook(args):
raise RuntimeError("error in thread!")
threading.excepthook = loud_excepthook
N = 10
b = threading.Barrier(N)
def bad_idea(n):
b.wait(timeout=5)
for j in range(100):
font = fm.get_font(fm.findfont("DejaVu Sans"))
font.set_text(str(n), 0.0, flags=LoadFlags.NO_HINTING)
threads = [
threading.Thread(target=bad_idea, name=f"bad_thread_{j}", args=(j,))
for j in range(N)
]
for t in threads:
t.start()
for t in threads:
t.join(timeout=9)
if t.is_alive():
raise RuntimeError("thread failed to join")
def test_fontcache_thread_safe():
pytest.importorskip('threading')
subprocess_run_helper(_test_threading, timeout=10)
def test_lockfilefailure(tmp_path):
# The logic here:
# 1. get a temp directory from pytest
# 2. import matplotlib which makes sure it exists
# 3. get the cache dir (where we check it is writable)
# 4. make it not writable
# 5. try to write into it via font manager
proc = subprocess_run_for_testing(
[
sys.executable,
"-c",
"import matplotlib;"
"import os;"
"p = matplotlib.get_cachedir();"
"os.chmod(p, 0o555);"
"import matplotlib.font_manager;"
],
env={**os.environ, 'MPLCONFIGDIR': str(tmp_path)},
check=True
)
def test_fontentry_dataclass():
fontent = FontEntry(name='font-name')
png = fontent._repr_png_()
img = Image.open(BytesIO(png))
assert img.width > 0
assert img.height > 0
html = fontent._repr_html_()
assert html.startswith("<img src=\"data:image/png;base64")
def test_fontentry_dataclass_invalid_path():
with pytest.raises(FileNotFoundError):
fontent = FontEntry(fname='/random', name='font-name')
fontent._repr_html_()
@pytest.mark.skipif(sys.platform == 'win32', reason='Linux or OS only')
def test_get_font_names():
paths_mpl = [cbook._get_data_path('fonts', subdir) for subdir in ['ttf']]
fonts_mpl = findSystemFonts(paths_mpl, fontext='ttf')
fonts_system = findSystemFonts(fontext='ttf')
ttf_fonts = []
for path in fonts_mpl + fonts_system:
try:
font = ft2font.FT2Font(path)
prop = ttfFontProperty(font)
ttf_fonts.append(prop.name)
except Exception:
pass
available_fonts = sorted(list(set(ttf_fonts)))
mpl_font_names = sorted(fontManager.get_font_names())
assert set(available_fonts) == set(mpl_font_names)
assert len(available_fonts) == len(mpl_font_names)
assert available_fonts == mpl_font_names
def test_donot_cache_tracebacks():
class SomeObject:
pass
def inner():
x = SomeObject()
fig = mfigure.Figure()
ax = fig.subplots()
fig.text(.5, .5, 'aardvark', family='doesnotexist')
with BytesIO() as out:
with warnings.catch_warnings():
warnings.filterwarnings('ignore')
fig.savefig(out, format='raw')
inner()
for obj in gc.get_objects():
if isinstance(obj, SomeObject):
pytest.fail("object from inner stack still alive")
def test_fontproperties_init_deprecation():
"""
Test the deprecated API of FontProperties.__init__.
The deprecation does not change behavior, it only adds a deprecation warning
via a decorator. Therefore, the purpose of this test is limited to check
which calls do and do not issue deprecation warnings. Behavior is still
tested via the existing regular tests.
"""
with pytest.warns(mpl.MatplotlibDeprecationWarning):
# multiple positional arguments
FontProperties("Times", "italic")
with pytest.warns(mpl.MatplotlibDeprecationWarning):
# Mixed positional and keyword arguments
FontProperties("Times", size=10)
with pytest.warns(mpl.MatplotlibDeprecationWarning):
# passing a family list positionally
FontProperties(["Times"])
# still accepted:
FontProperties(family="Times", style="italic")
FontProperties(family="Times")
FontProperties("Times") # works as pattern and family
FontProperties("serif-24:style=oblique:weight=bold") # pattern
# also still accepted:
# passing as pattern via family kwarg was not covered by the docs but
# historically worked. This is left unchanged for now.
# AFAICT, we cannot detect this: We can determine whether a string
# works as pattern, but that doesn't help, because there are strings
# that are both pattern and family. We would need to identify, whether
# a string is *not* a valid family.
# Since this case is not covered by docs, I've refrained from jumping
# extra hoops to detect this possible API misuse.
FontProperties(family="serif-24:style=oblique:weight=bold")
@@ -0,0 +1,77 @@
import pytest
from matplotlib.font_manager import FontProperties
# Attributes on FontProperties object to check for consistency
keys = [
"get_family",
"get_style",
"get_variant",
"get_weight",
"get_size",
]
def test_fontconfig_pattern():
"""Test converting a FontProperties to string then back."""
# Defaults
test = "defaults "
f1 = FontProperties()
s = str(f1)
f2 = FontProperties(s)
for k in keys:
assert getattr(f1, k)() == getattr(f2, k)(), test + k
# Basic inputs
test = "basic "
f1 = FontProperties(family="serif", size=20, style="italic")
s = str(f1)
f2 = FontProperties(s)
for k in keys:
assert getattr(f1, k)() == getattr(f2, k)(), test + k
# Full set of inputs.
test = "full "
f1 = FontProperties(family="sans-serif", size=24, weight="bold",
style="oblique", variant="small-caps",
stretch="expanded")
s = str(f1)
f2 = FontProperties(s)
for k in keys:
assert getattr(f1, k)() == getattr(f2, k)(), test + k
def test_fontconfig_str():
"""Test FontProperties string conversions for correctness."""
# Known good strings taken from actual font config specs on a linux box
# and modified for MPL defaults.
# Default values found by inspection.
test = "defaults "
s = ("sans\\-serif:style=normal:variant=normal:weight=normal"
":stretch=normal:size=12.0")
font = FontProperties(s)
right = FontProperties()
for k in keys:
assert getattr(font, k)() == getattr(right, k)(), test + k
test = "full "
s = ("serif-24:style=oblique:variant=small-caps:weight=bold"
":stretch=expanded")
font = FontProperties(s)
right = FontProperties(family="serif", size=24, weight="bold",
style="oblique", variant="small-caps",
stretch="expanded")
for k in keys:
assert getattr(font, k)() == getattr(right, k)(), test + k
def test_fontconfig_unknown_constant():
with pytest.raises(ValueError, match="ParseException"):
FontProperties(":unknown")
@@ -0,0 +1,943 @@
import itertools
import io
from pathlib import Path
import numpy as np
import pytest
import matplotlib as mpl
from matplotlib import ft2font
from matplotlib.testing.decorators import check_figures_equal
import matplotlib.font_manager as fm
import matplotlib.path as mpath
import matplotlib.pyplot as plt
def test_ft2image_draw_rect_filled():
width = 23
height = 42
for x0, y0, x1, y1 in itertools.product([1, 100], [2, 200], [4, 400], [8, 800]):
im = ft2font.FT2Image(width, height)
im.draw_rect_filled(x0, y0, x1, y1)
a = np.asarray(im)
assert a.dtype == np.uint8
assert a.shape == (height, width)
if x0 == 100 or y0 == 200:
# All the out-of-bounds starts should get automatically clipped.
assert np.sum(a) == 0
else:
# Otherwise, ends are clipped to the dimension, but are also _inclusive_.
filled = (min(x1 + 1, width) - x0) * (min(y1 + 1, height) - y0)
assert np.sum(a) == 255 * filled
def test_ft2font_dejavu_attrs():
file = fm.findfont('DejaVu Sans')
font = ft2font.FT2Font(file)
assert font.fname == file
# Names extracted from FontForge: Font Information → PS Names tab.
assert font.postscript_name == 'DejaVuSans'
assert font.family_name == 'DejaVu Sans'
assert font.style_name == 'Book'
assert font.num_faces == 1 # Single TTF.
assert font.num_named_instances == 0 # Not a variable font.
assert font.num_glyphs == 6241 # From compact encoding view in FontForge.
assert font.num_fixed_sizes == 0 # All glyphs are scalable.
assert font.num_charmaps == 5
# Other internal flags are set, so only check the ones we're allowed to test.
expected_flags = (ft2font.FaceFlags.SCALABLE | ft2font.FaceFlags.SFNT |
ft2font.FaceFlags.HORIZONTAL | ft2font.FaceFlags.KERNING |
ft2font.FaceFlags.GLYPH_NAMES)
assert expected_flags in font.face_flags
assert font.style_flags == ft2font.StyleFlags.NORMAL
assert font.scalable
# From FontForge: Font Information → General tab → entry name below.
assert font.units_per_EM == 2048 # Em Size.
assert font.underline_position == -175 # Underline position.
assert font.underline_thickness == 90 # Underline height.
# From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below.
assert font.ascender == 1901 # HHead Ascent.
assert font.descender == -483 # HHead Descent.
# Unconfirmed values.
assert font.height == 2384
assert font.max_advance_width == 3838
assert font.max_advance_height == 2384
assert font.bbox == (-2090, -948, 3673, 2524)
def test_ft2font_cm_attrs():
file = fm.findfont('cmtt10')
font = ft2font.FT2Font(file)
assert font.fname == file
# Names extracted from FontForge: Font Information → PS Names tab.
assert font.postscript_name == 'Cmtt10'
assert font.family_name == 'cmtt10'
assert font.style_name == 'Regular'
assert font.num_faces == 1 # Single TTF.
assert font.num_named_instances == 0 # Not a variable font.
assert font.num_glyphs == 133 # From compact encoding view in FontForge.
assert font.num_fixed_sizes == 0 # All glyphs are scalable.
assert font.num_charmaps == 2
# Other internal flags are set, so only check the ones we're allowed to test.
expected_flags = (ft2font.FaceFlags.SCALABLE | ft2font.FaceFlags.SFNT |
ft2font.FaceFlags.HORIZONTAL | ft2font.FaceFlags.GLYPH_NAMES)
assert expected_flags in font.face_flags
assert font.style_flags == ft2font.StyleFlags.NORMAL
assert font.scalable
# From FontForge: Font Information → General tab → entry name below.
assert font.units_per_EM == 2048 # Em Size.
assert font.underline_position == -143 # Underline position.
assert font.underline_thickness == 20 # Underline height.
# From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below.
assert font.ascender == 1276 # HHead Ascent.
assert font.descender == -489 # HHead Descent.
# Unconfirmed values.
assert font.height == 1765
assert font.max_advance_width == 1536
assert font.max_advance_height == 1765
assert font.bbox == (-12, -477, 1280, 1430)
def test_ft2font_stix_bold_attrs():
file = fm.findfont('STIXSizeTwoSym:bold')
font = ft2font.FT2Font(file)
assert font.fname == file
# Names extracted from FontForge: Font Information → PS Names tab.
assert font.postscript_name == 'STIXSizeTwoSym-Bold'
assert font.family_name == 'STIXSizeTwoSym'
assert font.style_name == 'Bold'
assert font.num_faces == 1 # Single TTF.
assert font.num_named_instances == 0 # Not a variable font.
assert font.num_glyphs == 20 # From compact encoding view in FontForge.
assert font.num_fixed_sizes == 0 # All glyphs are scalable.
assert font.num_charmaps == 3
# Other internal flags are set, so only check the ones we're allowed to test.
expected_flags = (ft2font.FaceFlags.SCALABLE | ft2font.FaceFlags.SFNT |
ft2font.FaceFlags.HORIZONTAL | ft2font.FaceFlags.GLYPH_NAMES)
assert expected_flags in font.face_flags
assert font.style_flags == ft2font.StyleFlags.BOLD
assert font.scalable
# From FontForge: Font Information → General tab → entry name below.
assert font.units_per_EM == 1000 # Em Size.
assert font.underline_position == -133 # Underline position.
assert font.underline_thickness == 20 # Underline height.
# From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below.
assert font.ascender == 2095 # HHead Ascent.
assert font.descender == -404 # HHead Descent.
# Unconfirmed values.
assert font.height == 2499
assert font.max_advance_width == 1130
assert font.max_advance_height == 2499
assert font.bbox == (4, -355, 1185, 2095)
def test_ft2font_invalid_args(tmp_path):
# filename argument.
with pytest.raises(TypeError, match='to a font file or a binary-mode file object'):
ft2font.FT2Font(None)
with pytest.raises(TypeError, match='to a font file or a binary-mode file object'):
ft2font.FT2Font(object()) # Not bytes or string, and has no read() method.
file = tmp_path / 'invalid-font.ttf'
file.write_text('This is not a valid font file.')
with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'),
file.open('rt') as fd):
ft2font.FT2Font(fd)
with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'),
file.open('wt') as fd):
ft2font.FT2Font(fd)
with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'),
file.open('wb') as fd):
ft2font.FT2Font(fd)
file = fm.findfont('DejaVu Sans')
# hinting_factor argument.
with pytest.raises(TypeError, match='incompatible constructor arguments'):
ft2font.FT2Font(file, 1.3)
with pytest.raises(ValueError, match='hinting_factor must be greater than 0'):
ft2font.FT2Font(file, 0)
with pytest.raises(TypeError, match='incompatible constructor arguments'):
# failing to be a list will fail before the 0
ft2font.FT2Font(file, _fallback_list=(0,)) # type: ignore[arg-type]
with pytest.raises(TypeError, match='incompatible constructor arguments'):
ft2font.FT2Font(file, _fallback_list=[0]) # type: ignore[list-item]
# kerning_factor argument.
with pytest.raises(TypeError, match='incompatible constructor arguments'):
ft2font.FT2Font(file, _kerning_factor=1.3)
def test_ft2font_clear():
file = fm.findfont('DejaVu Sans')
font = ft2font.FT2Font(file)
assert font.get_num_glyphs() == 0
assert font.get_width_height() == (0, 0)
assert font.get_bitmap_offset() == (0, 0)
font.set_text('ABabCDcd')
assert font.get_num_glyphs() == 8
assert font.get_width_height() != (0, 0)
assert font.get_bitmap_offset() != (0, 0)
font.clear()
assert font.get_num_glyphs() == 0
assert font.get_width_height() == (0, 0)
assert font.get_bitmap_offset() == (0, 0)
def test_ft2font_set_size():
file = fm.findfont('DejaVu Sans')
# Default is 12pt @ 72 dpi.
font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=1)
font.set_text('ABabCDcd')
orig = font.get_width_height()
font.set_size(24, 72)
font.set_text('ABabCDcd')
assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig)
font.set_size(12, 144)
font.set_text('ABabCDcd')
assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig)
def test_ft2font_charmaps():
def enc(name):
# We don't expose the encoding enum from FreeType, but can generate it here.
# For DejaVu, there are 5 charmaps, but only 2 have enum entries in FreeType.
e = 0
for x in name:
e <<= 8
e += ord(x)
return e
file = fm.findfont('DejaVu Sans')
font = ft2font.FT2Font(file)
assert font.num_charmaps == 5
# Unicode.
font.select_charmap(enc('unic'))
unic = font.get_charmap()
font.set_charmap(0) # Unicode platform, Unicode BMP only.
after = font.get_charmap()
assert len(after) <= len(unic)
for chr, glyph in after.items():
assert unic[chr] == glyph == font.get_char_index(chr)
font.set_charmap(1) # Unicode platform, modern subtable.
after = font.get_charmap()
assert unic == after
font.set_charmap(3) # Windows platform, Unicode BMP only.
after = font.get_charmap()
assert len(after) <= len(unic)
for chr, glyph in after.items():
assert unic[chr] == glyph == font.get_char_index(chr)
font.set_charmap(4) # Windows platform, Unicode full repertoire, modern subtable.
after = font.get_charmap()
assert unic == after
# This is just a random sample from FontForge.
glyph_names = {
'non-existent-glyph-name': 0,
'plusminus': 115,
'Racute': 278,
'perthousand': 2834,
'seveneighths': 3057,
'triagup': 3721,
'uni01D3': 405,
'uni0417': 939,
'uni2A02': 4464,
'u1D305': 5410,
'u1F0A1': 5784,
}
for name, index in glyph_names.items():
assert font.get_name_index(name) == index
if name == 'non-existent-glyph-name':
name = '.notdef'
# This doesn't always apply, but it does for DejaVu Sans.
assert font.get_glyph_name(index) == name
# Apple Roman.
font.select_charmap(enc('armn'))
armn = font.get_charmap()
font.set_charmap(2) # Macintosh platform, Roman.
after = font.get_charmap()
assert armn == after
assert len(armn) <= 256 # 8-bit encoding.
# The first 128 characters of Apple Roman match ASCII, which also matches Unicode.
for o in range(1, 128):
if o not in armn or o not in unic:
continue
assert unic[o] == armn[o]
# Check a couple things outside the ASCII set that are different in each charset.
examples = [
# (Unicode, Macintosh)
(0x2020, 0xA0), # Dagger.
(0x00B0, 0xA1), # Degree symbol.
(0x00A3, 0xA3), # Pound sign.
(0x00A7, 0xA4), # Section sign.
(0x00B6, 0xA6), # Pilcrow.
(0x221E, 0xB0), # Infinity symbol.
]
for u, m in examples:
# Though the encoding is different, the glyph should be the same.
assert unic[u] == armn[m]
_expected_sfnt_names = {
'DejaVu Sans': {
0: 'Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.\n'
'Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.\n'
'DejaVu changes are in public domain\n',
1: 'DejaVu Sans',
2: 'Book',
3: 'DejaVu Sans',
4: 'DejaVu Sans',
5: 'Version 2.35',
6: 'DejaVuSans',
8: 'DejaVu fonts team',
11: 'http://dejavu.sourceforge.net',
13: 'Fonts are (c) Bitstream (see below). '
'DejaVu changes are in public domain. '
'''Glyphs imported from Arev fonts are (c) Tavmjung Bah (see below)
Bitstream Vera Fonts Copyright
------------------------------
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of the fonts accompanying this license ("Fonts") and associated
documentation files (the "Font Software"), to reproduce and distribute the
Font Software, including without limitation the rights to use, copy, merge,
publish, distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to the
following conditions:
The above copyright and trademark notices and this permission notice shall
be included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular
the designs of glyphs or characters in the Fonts may be modified and
additional glyphs or characters may be added to the Fonts, only if the fonts
are renamed to names not containing either the words "Bitstream" or the word
"Vera".
This License becomes null and void to the extent applicable to Fonts or Font
Software that has been modified and is distributed under the "Bitstream
Vera" names.
The Font Software may be sold as part of a larger software package but no
copy of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
FONT SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome
Foundation, and Bitstream Inc., shall not be used in advertising or
otherwise to promote the sale, use or other dealings in this Font Software
without prior written authorization from the Gnome Foundation or Bitstream
Inc., respectively. For further information, contact: fonts at gnome dot
org. ''' '''
Arev Fonts Copyright
------------------------------
Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining
a copy of the fonts accompanying this license ("Fonts") and
associated documentation files (the "Font Software"), to reproduce
and distribute the modifications to the Bitstream Vera Font Software,
including without limitation the rights to use, copy, merge, publish,
distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to
the following conditions:
The above copyright and trademark notices and this permission notice
shall be included in all copies of one or more of the Font Software
typefaces.
The Font Software may be modified, altered, or added to, and in
particular the designs of glyphs or characters in the Fonts may be
modified and additional glyphs or characters may be added to the
Fonts, only if the fonts are renamed to names not containing either
the words "Tavmjong Bah" or the word "Arev".
This License becomes null and void to the extent applicable to Fonts
or Font Software that has been modified and is distributed under the ''' '''
"Tavmjong Bah Arev" names.
The Font Software may be sold as part of a larger software package but
no copy of one or more of the Font Software typefaces may be sold by
itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the name of Tavmjong Bah shall not
be used in advertising or otherwise to promote the sale, use or other
dealings in this Font Software without prior written authorization
from Tavmjong Bah. For further information, contact: tavmjong @ free
. fr.''',
14: 'http://dejavu.sourceforge.net/wiki/index.php/License',
16: 'DejaVu Sans',
17: 'Book',
},
'cmtt10': {
0: 'Copyright (C) 1994, Basil K. Malyshev. All Rights Reserved.'
'012BaKoMa Fonts Collection, Level-B.',
1: 'cmtt10',
2: 'Regular',
3: 'FontMonger:cmtt10',
4: 'cmtt10',
5: '1.1/12-Nov-94',
6: 'Cmtt10',
},
'STIXSizeTwoSym:bold': {
0: 'Copyright (c) 2001-2010 by the STI Pub Companies, consisting of the '
'American Chemical Society, the American Institute of Physics, the American '
'Mathematical Society, the American Physical Society, Elsevier, Inc., and '
'The Institute of Electrical and Electronic Engineers, Inc. Portions '
'copyright (c) 1998-2003 by MicroPress, Inc. Portions copyright (c) 1990 by '
'Elsevier, Inc. All rights reserved.',
1: 'STIXSizeTwoSym',
2: 'Bold',
3: 'FontMaster:STIXSizeTwoSym-Bold:1.0.0',
4: 'STIXSizeTwoSym-Bold',
5: 'Version 1.0.0',
6: 'STIXSizeTwoSym-Bold',
7: 'STIX Fonts(TM) is a trademark of The Institute of Electrical and '
'Electronics Engineers, Inc.',
9: 'MicroPress Inc., with final additions and corrections provided by Coen '
'Hoffman, Elsevier (retired)',
10: 'Arie de Ruiter, who in 1995 was Head of Information Technology '
'Development at Elsevier Science, made a proposal to the STI Pub group, an '
'informal group of publishers consisting of representatives from the '
'American Chemical Society (ACS), American Institute of Physics (AIP), '
'American Mathematical Society (AMS), American Physical Society (APS), '
'Elsevier, and Institute of Electrical and Electronics Engineers (IEEE). '
'De Ruiter encouraged the members to consider development of a series of '
'Web fonts, which he proposed should be called the Scientific and '
'Technical Information eXchange, or STIX, Fonts. All STI Pub member '
'organizations enthusiastically endorsed this proposal, and the STI Pub '
'group agreed to embark on what has become a twelve-year project. The goal '
'of the project was to identify all alphabetic, symbolic, and other '
'special characters used in any facet of scientific publishing and to '
'create a set of Unicode-based fonts that would be distributed free to '
'every scientist, student, and other interested party worldwide. The fonts '
'would be consistent with the emerging Unicode standard, and would permit '
'universal representation of every character. With the release of the STIX '
"fonts, de Ruiter's vision has been realized.",
11: 'http://www.stixfonts.org',
12: 'http://www.micropress-inc.com',
13: 'As a condition for receiving these fonts at no charge, each person '
'downloading the fonts must agree to some simple license terms. The '
'license is based on the SIL Open Font License '
'<http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL>. The '
'SIL License is a free and open source license specifically designed for '
'fonts and related software. The basic terms are that the recipient will '
'not remove the copyright and trademark statements from the fonts and '
'that, if the person decides to create a derivative work based on the STIX '
'Fonts but incorporating some changes or enhancements, the derivative work '
'("Modified Version") will carry a different name. The copyright and '
'trademark restrictions are part of the agreement between the STI Pub '
'companies and the typeface designer. The "renaming" restriction results '
'from the desire of the STI Pub companies to assure that the STIX Fonts '
'will continue to function in a predictable fashion for all that use them. '
'No copy of one or more of the individual Font typefaces that form the '
'STIX Fonts(TM) set may be sold by itself, but other than this one '
'restriction, licensees are free to sell the fonts either separately or as '
'part of a package that combines other software or fonts with this font '
'set.',
14: 'http://www.stixfonts.org/user_license.html',
},
}
@pytest.mark.parametrize('font_name, expected', _expected_sfnt_names.items(),
ids=_expected_sfnt_names.keys())
def test_ft2font_get_sfnt(font_name, expected):
file = fm.findfont(font_name)
font = ft2font.FT2Font(file)
sfnt = font.get_sfnt()
for name, value in expected.items():
# Macintosh, Unicode 1.0, English, name.
assert sfnt.pop((1, 0, 0, name)) == value.encode('ascii')
# Microsoft, Unicode, English United States, name.
assert sfnt.pop((3, 1, 1033, name)) == value.encode('utf-16be')
assert sfnt == {}
_expected_sfnt_tables = {
'DejaVu Sans': {
'invalid': None,
'head': {
'version': (1, 0),
'fontRevision': (2, 22937),
'checkSumAdjustment': -175678572,
'magicNumber': 0x5F0F3CF5,
'flags': 31,
'unitsPerEm': 2048,
'created': (0, 3514699492), 'modified': (0, 3514699492),
'xMin': -2090, 'yMin': -948, 'xMax': 3673, 'yMax': 2524,
'macStyle': 0,
'lowestRecPPEM': 8,
'fontDirectionHint': 0,
'indexToLocFormat': 1,
'glyphDataFormat': 0,
},
'maxp': {
'version': (1, 0),
'numGlyphs': 6241,
'maxPoints': 852, 'maxComponentPoints': 104, 'maxTwilightPoints': 16,
'maxContours': 43, 'maxComponentContours': 12,
'maxZones': 2,
'maxStorage': 153,
'maxFunctionDefs': 64,
'maxInstructionDefs': 0,
'maxStackElements': 1045,
'maxSizeOfInstructions': 534,
'maxComponentElements': 8,
'maxComponentDepth': 4,
},
'OS/2': {
'version': 1,
'xAvgCharWidth': 1038,
'usWeightClass': 400, 'usWidthClass': 5,
'fsType': 0,
'ySubscriptXSize': 1331, 'ySubscriptYSize': 1433,
'ySubscriptXOffset': 0, 'ySubscriptYOffset': 286,
'ySuperscriptXSize': 1331, 'ySuperscriptYSize': 1433,
'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 983,
'yStrikeoutSize': 102, 'yStrikeoutPosition': 530,
'sFamilyClass': 0,
'panose': b'\x02\x0b\x06\x03\x03\x08\x04\x02\x02\x04',
'ulCharRange': (3875565311, 3523280383, 170156073, 67117068),
'achVendID': b'PfEd',
'fsSelection': 64, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 65535,
},
'hhea': {
'version': (1, 0),
'ascent': 1901, 'descent': -483, 'lineGap': 0,
'advanceWidthMax': 3838,
'minLeftBearing': -2090, 'minRightBearing': -1455,
'xMaxExtent': 3673,
'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0,
'metricDataFormat': 0, 'numOfLongHorMetrics': 6226,
},
'vhea': None,
'post': {
'format': (2, 0),
'isFixedPitch': 0, 'italicAngle': (0, 0),
'underlinePosition': -130, 'underlineThickness': 90,
'minMemType42': 0, 'maxMemType42': 0,
'minMemType1': 0, 'maxMemType1': 0,
},
'pclt': None,
},
'cmtt10': {
'invalid': None,
'head': {
'version': (1, 0),
'fontRevision': (1, 0),
'checkSumAdjustment': 555110277,
'magicNumber': 0x5F0F3CF5,
'flags': 3,
'unitsPerEm': 2048,
'created': (0, 0), 'modified': (0, 0),
'xMin': -12, 'yMin': -477, 'xMax': 1280, 'yMax': 1430,
'macStyle': 0,
'lowestRecPPEM': 6,
'fontDirectionHint': 2,
'indexToLocFormat': 1,
'glyphDataFormat': 0,
},
'maxp': {
'version': (1, 0),
'numGlyphs': 133,
'maxPoints': 94, 'maxComponentPoints': 0, 'maxTwilightPoints': 12,
'maxContours': 5, 'maxComponentContours': 0,
'maxZones': 2,
'maxStorage': 6,
'maxFunctionDefs': 64,
'maxInstructionDefs': 0,
'maxStackElements': 200,
'maxSizeOfInstructions': 100,
'maxComponentElements': 4,
'maxComponentDepth': 1,
},
'OS/2': {
'version': 0,
'xAvgCharWidth': 1075,
'usWeightClass': 400, 'usWidthClass': 5,
'fsType': 0,
'ySubscriptXSize': 410, 'ySubscriptYSize': 369,
'ySubscriptXOffset': 0, 'ySubscriptYOffset': -469,
'ySuperscriptXSize': 410, 'ySuperscriptYSize': 369,
'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 1090,
'yStrikeoutSize': 102, 'yStrikeoutPosition': 530,
'sFamilyClass': 0,
'panose': b'\x02\x0b\x05\x00\x00\x00\x00\x00\x00\x00',
'ulCharRange': (0, 0, 0, 0),
'achVendID': b'\x00\x00\x00\x00',
'fsSelection': 64, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 9835,
},
'hhea': {
'version': (1, 0),
'ascent': 1276, 'descent': -489, 'lineGap': 0,
'advanceWidthMax': 1536,
'minLeftBearing': -12, 'minRightBearing': -29,
'xMaxExtent': 1280,
'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0,
'metricDataFormat': 0, 'numOfLongHorMetrics': 133,
},
'vhea': None,
'post': {
'format': (2, 0),
'isFixedPitch': 0, 'italicAngle': (0, 0),
'underlinePosition': -133, 'underlineThickness': 20,
'minMemType42': 0, 'maxMemType42': 0,
'minMemType1': 0, 'maxMemType1': 0,
},
'pclt': {
'version': (1, 0),
'fontNumber': 2147483648,
'pitch': 1075,
'xHeight': 905,
'style': 0,
'typeFamily': 0,
'capHeight': 1276,
'symbolSet': 0,
'typeFace': b'cmtt10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
'characterComplement': b'\xff\xff\xff\xff7\xff\xff\xfe',
'strokeWeight': 0,
'widthType': -5,
'serifStyle': 64,
},
},
'STIXSizeTwoSym:bold': {
'invalid': None,
'head': {
'version': (1, 0),
'fontRevision': (1, 0),
'checkSumAdjustment': 1803408080,
'magicNumber': 0x5F0F3CF5,
'flags': 11,
'unitsPerEm': 1000,
'created': (0, 3359035786), 'modified': (0, 3359035786),
'xMin': 4, 'yMin': -355, 'xMax': 1185, 'yMax': 2095,
'macStyle': 1,
'lowestRecPPEM': 8,
'fontDirectionHint': 2,
'indexToLocFormat': 0,
'glyphDataFormat': 0,
},
'maxp': {
'version': (1, 0),
'numGlyphs': 20,
'maxPoints': 37, 'maxComponentPoints': 0, 'maxTwilightPoints': 0,
'maxContours': 1, 'maxComponentContours': 0,
'maxZones': 2,
'maxStorage': 1,
'maxFunctionDefs': 64,
'maxInstructionDefs': 0,
'maxStackElements': 64,
'maxSizeOfInstructions': 0,
'maxComponentElements': 0,
'maxComponentDepth': 0,
},
'OS/2': {
'version': 2,
'xAvgCharWidth': 598,
'usWeightClass': 700, 'usWidthClass': 5,
'fsType': 0,
'ySubscriptXSize': 500, 'ySubscriptYSize': 500,
'ySubscriptXOffset': 0, 'ySubscriptYOffset': 250,
'ySuperscriptXSize': 500, 'ySuperscriptYSize': 500,
'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 500,
'yStrikeoutSize': 20, 'yStrikeoutPosition': 1037,
'sFamilyClass': 0,
'panose': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
'ulCharRange': (3, 192, 0, 0),
'achVendID': b'STIX',
'fsSelection': 32, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 10217,
},
'hhea': {
'version': (1, 0),
'ascent': 2095, 'descent': -404, 'lineGap': 0,
'advanceWidthMax': 1130,
'minLeftBearing': 0, 'minRightBearing': -55,
'xMaxExtent': 1185,
'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0,
'metricDataFormat': 0, 'numOfLongHorMetrics': 19,
},
'vhea': None,
'post': {
'format': (2, 0),
'isFixedPitch': 0, 'italicAngle': (0, 0),
'underlinePosition': -123, 'underlineThickness': 20,
'minMemType42': 0, 'maxMemType42': 0,
'minMemType1': 0, 'maxMemType1': 0,
},
'pclt': None,
},
}
@pytest.mark.parametrize('font_name', _expected_sfnt_tables.keys())
@pytest.mark.parametrize('header', _expected_sfnt_tables['DejaVu Sans'].keys())
def test_ft2font_get_sfnt_table(font_name, header):
file = fm.findfont(font_name)
font = ft2font.FT2Font(file)
assert font.get_sfnt_table(header) == _expected_sfnt_tables[font_name][header]
@pytest.mark.parametrize('left, right, unscaled, unfitted, default', [
# These are all the same class.
('A', 'A', 57, 248, 256), ('A', 'À', 57, 248, 256), ('A', 'Á', 57, 248, 256),
('A', 'Â', 57, 248, 256), ('A', 'Ã', 57, 248, 256), ('A', 'Ä', 57, 248, 256),
# And a few other random ones.
('D', 'A', -36, -156, -128), ('T', '.', -243, -1056, -1024),
('X', 'C', -149, -647, -640), ('-', 'J', 114, 495, 512),
])
def test_ft2font_get_kerning(left, right, unscaled, unfitted, default):
file = fm.findfont('DejaVu Sans')
# With unscaled, these settings should produce exact values found in FontForge.
font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0)
font.set_size(100, 100)
assert font.get_kerning(font.get_char_index(ord(left)),
font.get_char_index(ord(right)),
ft2font.Kerning.UNSCALED) == unscaled
assert font.get_kerning(font.get_char_index(ord(left)),
font.get_char_index(ord(right)),
ft2font.Kerning.UNFITTED) == unfitted
assert font.get_kerning(font.get_char_index(ord(left)),
font.get_char_index(ord(right)),
ft2font.Kerning.DEFAULT) == default
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match='Use Kerning.UNSCALED instead'):
k = ft2font.KERNING_UNSCALED
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match='Use Kerning enum values instead'):
assert font.get_kerning(font.get_char_index(ord(left)),
font.get_char_index(ord(right)),
int(k)) == unscaled
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match='Use Kerning.UNFITTED instead'):
k = ft2font.KERNING_UNFITTED
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match='Use Kerning enum values instead'):
assert font.get_kerning(font.get_char_index(ord(left)),
font.get_char_index(ord(right)),
int(k)) == unfitted
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match='Use Kerning.DEFAULT instead'):
k = ft2font.KERNING_DEFAULT
with pytest.warns(mpl.MatplotlibDeprecationWarning,
match='Use Kerning enum values instead'):
assert font.get_kerning(font.get_char_index(ord(left)),
font.get_char_index(ord(right)),
int(k)) == default
def test_ft2font_set_text():
file = fm.findfont('DejaVu Sans')
font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0)
xys = font.set_text('')
np.testing.assert_array_equal(xys, np.empty((0, 2)))
assert font.get_width_height() == (0, 0)
assert font.get_num_glyphs() == 0
assert font.get_descent() == 0
assert font.get_bitmap_offset() == (0, 0)
# This string uses all the kerning pairs defined for test_ft2font_get_kerning.
xys = font.set_text('AADAT.XC-J')
np.testing.assert_array_equal(
xys,
[(0, 0), (512, 0), (1024, 0), (1600, 0), (2112, 0), (2496, 0), (2688, 0),
(3200, 0), (3712, 0), (4032, 0)])
assert font.get_width_height() == (4288, 768)
assert font.get_num_glyphs() == 10
assert font.get_descent() == 192
assert font.get_bitmap_offset() == (6, 0)
def test_ft2font_loading():
file = fm.findfont('DejaVu Sans')
font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0)
for glyph in [font.load_char(ord('M')),
font.load_glyph(font.get_char_index(ord('M')))]:
assert glyph is not None
assert glyph.width == 576
assert glyph.height == 576
assert glyph.horiBearingX == 0
assert glyph.horiBearingY == 576
assert glyph.horiAdvance == 640
assert glyph.linearHoriAdvance == 678528
assert glyph.vertBearingX == -384
assert glyph.vertBearingY == 64
assert glyph.vertAdvance == 832
assert glyph.bbox == (54, 0, 574, 576)
assert font.get_num_glyphs() == 2 # Both count as loaded.
# But neither has been placed anywhere.
assert font.get_width_height() == (0, 0)
assert font.get_descent() == 0
assert font.get_bitmap_offset() == (0, 0)
def test_ft2font_drawing():
expected_str = (
' ',
'11 11 ',
'11 11 ',
'1 1 1 1 ',
'1 1 1 1 ',
'1 1 1 1 ',
'1 11 1 ',
'1 11 1 ',
'1 1 ',
'1 1 ',
' ',
)
expected = np.array([
[int(c) for c in line.replace(' ', '0')] for line in expected_str
])
expected *= 255
file = fm.findfont('DejaVu Sans')
font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0)
font.set_text('M')
font.draw_glyphs_to_bitmap(antialiased=False)
image = font.get_image()
np.testing.assert_array_equal(image, expected)
font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0)
glyph = font.load_char(ord('M'))
image = ft2font.FT2Image(expected.shape[1], expected.shape[0])
font.draw_glyph_to_bitmap(image, -1, 1, glyph, antialiased=False)
np.testing.assert_array_equal(image, expected)
def test_ft2font_get_path():
file = fm.findfont('DejaVu Sans')
font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0)
vertices, codes = font.get_path()
assert vertices.shape == (0, 2)
assert codes.shape == (0, )
font.load_char(ord('M'))
vertices, codes = font.get_path()
expected_vertices = np.array([
(0.843750, 9.000000), (2.609375, 9.000000), # Top left.
(4.906250, 2.875000), # Top of midpoint.
(7.218750, 9.000000), (8.968750, 9.000000), # Top right.
(8.968750, 0.000000), (7.843750, 0.000000), # Bottom right.
(7.843750, 7.906250), # Point under top right.
(5.531250, 1.734375), (4.296875, 1.734375), # Bar under midpoint.
(1.984375, 7.906250), # Point under top left.
(1.984375, 0.000000), (0.843750, 0.000000), # Bottom left.
(0.843750, 9.000000), # Back to top left corner.
(0.000000, 0.000000),
])
np.testing.assert_array_equal(vertices, expected_vertices)
expected_codes = np.full(expected_vertices.shape[0], mpath.Path.LINETO,
dtype=mpath.Path.code_type)
expected_codes[0] = mpath.Path.MOVETO
expected_codes[-1] = mpath.Path.CLOSEPOLY
np.testing.assert_array_equal(codes, expected_codes)
@pytest.mark.parametrize('family_name, file_name',
[("WenQuanYi Zen Hei", "wqy-zenhei.ttc"),
("Noto Sans CJK JP", "NotoSansCJK.ttc"),
("Noto Sans TC", "NotoSansTC-Regular.otf")]
)
def test_fallback_smoke(family_name, file_name):
fp = fm.FontProperties(family=[family_name])
if Path(fm.findfont(fp)).name != file_name:
pytest.skip(f"Font {family_name} ({file_name}) is missing")
plt.rcParams['font.size'] = 20
fig = plt.figure(figsize=(4.75, 1.85))
fig.text(0.05, 0.45, "There are 几个汉字 in between!",
family=['DejaVu Sans', family_name])
fig.text(0.05, 0.85, "There are 几个汉字 in between!",
family=[family_name])
# TODO enable fallback for other backends!
for fmt in ['png', 'raw']: # ["svg", "pdf", "ps"]:
fig.savefig(io.BytesIO(), format=fmt)
@pytest.mark.parametrize('family_name, file_name',
[("WenQuanYi Zen Hei", "wqy-zenhei"),
("Noto Sans CJK JP", "NotoSansCJK"),
("Noto Sans TC", "NotoSansTC-Regular.otf")]
)
@check_figures_equal(extensions=["png", "pdf", "eps", "svg"])
def test_font_fallback_chinese(fig_test, fig_ref, family_name, file_name):
fp = fm.FontProperties(family=[family_name])
if file_name not in Path(fm.findfont(fp)).name:
pytest.skip(f"Font {family_name} ({file_name}) is missing")
text = ["There are", "几个汉字", "in between!"]
plt.rcParams["font.size"] = 20
test_fonts = [["DejaVu Sans", family_name]] * 3
ref_fonts = [["DejaVu Sans"], [family_name], ["DejaVu Sans"]]
for j, (txt, test_font, ref_font) in enumerate(
zip(text, test_fonts, ref_fonts)
):
fig_ref.text(0.05, .85 - 0.15*j, txt, family=ref_font)
fig_test.text(0.05, .85 - 0.15*j, txt, family=test_font)
@pytest.mark.parametrize("font_list",
[['DejaVu Serif', 'DejaVu Sans'],
['DejaVu Sans Mono']],
ids=["two fonts", "one font"])
def test_fallback_missing(recwarn, font_list):
fig = plt.figure()
fig.text(.5, .5, "Hello 🙃 World!", family=font_list)
fig.canvas.draw()
assert all(isinstance(warn.message, UserWarning) for warn in recwarn)
# not sure order is guaranteed on the font listing so
assert recwarn[0].message.args[0].startswith(
"Glyph 128579 (\\N{UPSIDE-DOWN FACE}) missing from font(s)")
assert all([font in recwarn[0].message.args[0] for font in font_list])
@pytest.mark.parametrize(
"family_name, file_name",
[
("WenQuanYi Zen Hei", "wqy-zenhei"),
("Noto Sans CJK JP", "NotoSansCJK"),
("Noto Sans TC", "NotoSansTC-Regular.otf")
],
)
def test__get_fontmap(family_name, file_name):
fp = fm.FontProperties(family=[family_name])
found_file_name = Path(fm.findfont(fp)).name
if file_name not in found_file_name:
pytest.skip(f"Font {family_name} ({file_name}) is missing")
text = "There are 几个汉字 in between!"
ft = fm.get_font(
fm.fontManager._find_fonts_by_props(
fm.FontProperties(family=["DejaVu Sans", family_name])
)
)
fontmap = ft._get_fontmap(text)
for char, font in fontmap.items():
if ord(char) > 127:
assert Path(font.fname).name == found_file_name
else:
assert Path(font.fname).name == "DejaVuSans.ttf"
@@ -0,0 +1,35 @@
from importlib import import_module
from pkgutil import walk_packages
import matplotlib
import pytest
# Get the names of all matplotlib submodules,
# except for the unit tests and private modules.
module_names = [
m.name
for m in walk_packages(
path=matplotlib.__path__, prefix=f'{matplotlib.__name__}.'
)
if not m.name.startswith(__package__)
and not any(x.startswith('_') for x in m.name.split('.'))
]
@pytest.mark.parametrize('module_name', module_names)
@pytest.mark.filterwarnings('ignore::DeprecationWarning')
@pytest.mark.filterwarnings('ignore::ImportWarning')
def test_getattr(module_name):
"""
Test that __getattr__ methods raise AttributeError for unknown keys.
See #20822, #20855.
"""
try:
module = import_module(module_name)
except (ImportError, RuntimeError, OSError) as e:
# Skip modules that cannot be imported due to missing dependencies
pytest.skip(f'Cannot import {module_name} due to {e}')
key = 'THIS_SYMBOL_SHOULD_NOT_EXIST'
if hasattr(module, key):
delattr(module, key)
@@ -0,0 +1,50 @@
import matplotlib.gridspec as gridspec
import matplotlib.pyplot as plt
import pytest
def test_equal():
gs = gridspec.GridSpec(2, 1)
assert gs[0, 0] == gs[0, 0]
assert gs[:, 0] == gs[:, 0]
def test_width_ratios():
"""
Addresses issue #5835.
See at https://github.com/matplotlib/matplotlib/issues/5835.
"""
with pytest.raises(ValueError):
gridspec.GridSpec(1, 1, width_ratios=[2, 1, 3])
def test_height_ratios():
"""
Addresses issue #5835.
See at https://github.com/matplotlib/matplotlib/issues/5835.
"""
with pytest.raises(ValueError):
gridspec.GridSpec(1, 1, height_ratios=[2, 1, 3])
def test_repr():
ss = gridspec.GridSpec(3, 3)[2, 1:3]
assert repr(ss) == "GridSpec(3, 3)[2:3, 1:3]"
ss = gridspec.GridSpec(2, 2,
height_ratios=(3, 1),
width_ratios=(1, 3))
assert repr(ss) == \
"GridSpec(2, 2, height_ratios=(3, 1), width_ratios=(1, 3))"
def test_subplotspec_args():
fig, axs = plt.subplots(1, 2)
# should work:
gs = gridspec.GridSpecFromSubplotSpec(2, 1,
subplot_spec=axs[0].get_subplotspec())
assert gs.get_topmost_subplotspec() == axs[0].get_subplotspec()
with pytest.raises(TypeError, match="subplot_spec must be type SubplotSpec"):
gs = gridspec.GridSpecFromSubplotSpec(2, 1, subplot_spec=axs[0])
with pytest.raises(TypeError, match="subplot_spec must be type SubplotSpec"):
gs = gridspec.GridSpecFromSubplotSpec(2, 1, subplot_spec=axs)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,452 @@
"""
Tests specific to the lines module.
"""
import itertools
import platform
import timeit
from types import SimpleNamespace
from cycler import cycler
import numpy as np
from numpy.testing import assert_array_equal
import pytest
import matplotlib
import matplotlib as mpl
from matplotlib import _path
import matplotlib.lines as mlines
from matplotlib.markers import MarkerStyle
from matplotlib.path import Path
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
from matplotlib.testing.decorators import image_comparison, check_figures_equal
def test_segment_hits():
"""Test a problematic case."""
cx, cy = 553, 902
x, y = np.array([553., 553.]), np.array([95., 947.])
radius = 6.94
assert_array_equal(mlines.segment_hits(cx, cy, x, y, radius), [0])
# Runtimes on a loaded system are inherently flaky. Not so much that a rerun
# won't help, hopefully.
@pytest.mark.flaky(reruns=3)
def test_invisible_Line_rendering():
"""
GitHub issue #1256 identified a bug in Line.draw method
Despite visibility attribute set to False, the draw method was not
returning early enough and some pre-rendering code was executed
though not necessary.
Consequence was an excessive draw time for invisible Line instances
holding a large number of points (Npts> 10**6)
"""
# Creates big x and y data:
N = 10**7
x = np.linspace(0, 1, N)
y = np.random.normal(size=N)
# Create a plot figure:
fig = plt.figure()
ax = plt.subplot()
# Create a "big" Line instance:
l = mlines.Line2D(x, y)
l.set_visible(False)
# but don't add it to the Axis instance `ax`
# [here Interactive panning and zooming is pretty responsive]
# Time the canvas drawing:
t_no_line = min(timeit.repeat(fig.canvas.draw, number=1, repeat=3))
# (gives about 25 ms)
# Add the big invisible Line:
ax.add_line(l)
# [Now interactive panning and zooming is very slow]
# Time the canvas drawing:
t_invisible_line = min(timeit.repeat(fig.canvas.draw, number=1, repeat=3))
# gives about 290 ms for N = 10**7 pts
slowdown_factor = t_invisible_line / t_no_line
slowdown_threshold = 2 # trying to avoid false positive failures
assert slowdown_factor < slowdown_threshold
def test_set_line_coll_dash():
fig, ax = plt.subplots()
np.random.seed(0)
# Testing setting linestyles for line collections.
# This should not produce an error.
ax.contour(np.random.randn(20, 30), linestyles=[(0, (3, 3))])
def test_invalid_line_data():
with pytest.raises(RuntimeError, match='xdata must be'):
mlines.Line2D(0, [])
with pytest.raises(RuntimeError, match='ydata must be'):
mlines.Line2D([], 1)
line = mlines.Line2D([], [])
with pytest.raises(RuntimeError, match='x must be'):
line.set_xdata(0)
with pytest.raises(RuntimeError, match='y must be'):
line.set_ydata(0)
@image_comparison(['line_dashes'], remove_text=True, tol=0.003)
def test_line_dashes():
# Tolerance introduced after reordering of floating-point operations
# Remove when regenerating the images
fig, ax = plt.subplots()
ax.plot(range(10), linestyle=(0, (3, 3)), lw=5)
def test_line_colors():
fig, ax = plt.subplots()
ax.plot(range(10), color='none')
ax.plot(range(10), color='r')
ax.plot(range(10), color='.3')
ax.plot(range(10), color=(1, 0, 0, 1))
ax.plot(range(10), color=(1, 0, 0))
fig.canvas.draw()
def test_valid_colors():
line = mlines.Line2D([], [])
with pytest.raises(ValueError):
line.set_color("foobar")
def test_linestyle_variants():
fig, ax = plt.subplots()
for ls in ["-", "solid", "--", "dashed",
"-.", "dashdot", ":", "dotted",
(0, None), (0, ()), (0, []), # gh-22930
]:
ax.plot(range(10), linestyle=ls)
fig.canvas.draw()
def test_valid_linestyles():
line = mlines.Line2D([], [])
with pytest.raises(ValueError):
line.set_linestyle('aardvark')
@image_comparison(['drawstyle_variants.png'], remove_text=True,
tol=0 if platform.machine() == 'x86_64' else 0.03)
def test_drawstyle_variants():
fig, axs = plt.subplots(6)
dss = ["default", "steps-mid", "steps-pre", "steps-post", "steps", None]
# We want to check that drawstyles are properly handled even for very long
# lines (for which the subslice optimization is on); however, we need
# to zoom in so that the difference between the drawstyles is actually
# visible.
for ax, ds in zip(axs.flat, dss):
ax.plot(range(2000), drawstyle=ds)
ax.set(xlim=(0, 2), ylim=(0, 2))
@check_figures_equal(extensions=('png',))
def test_no_subslice_with_transform(fig_ref, fig_test):
ax = fig_ref.add_subplot()
x = np.arange(2000)
ax.plot(x + 2000, x)
ax = fig_test.add_subplot()
t = mtransforms.Affine2D().translate(2000.0, 0.0)
ax.plot(x, x, transform=t+ax.transData)
def test_valid_drawstyles():
line = mlines.Line2D([], [])
with pytest.raises(ValueError):
line.set_drawstyle('foobar')
def test_set_drawstyle():
x = np.linspace(0, 2*np.pi, 10)
y = np.sin(x)
fig, ax = plt.subplots()
line, = ax.plot(x, y)
line.set_drawstyle("steps-pre")
assert len(line.get_path().vertices) == 2*len(x)-1
line.set_drawstyle("default")
assert len(line.get_path().vertices) == len(x)
@image_comparison(['line_collection_dashes'], remove_text=True, style='mpl20',
tol=0 if platform.machine() == 'x86_64' else 0.65)
def test_set_line_coll_dash_image():
fig, ax = plt.subplots()
np.random.seed(0)
ax.contour(np.random.randn(20, 30), linestyles=[(0, (3, 3))])
@image_comparison(['marker_fill_styles.png'], remove_text=True)
def test_marker_fill_styles():
colors = itertools.cycle([[0, 0, 1], 'g', '#ff0000', 'c', 'm', 'y',
np.array([0, 0, 0])])
altcolor = 'lightgreen'
y = np.array([1, 1])
x = np.array([0, 9])
fig, ax = plt.subplots()
# This hard-coded list of markers correspond to an earlier iteration of
# MarkerStyle.filled_markers; the value of that attribute has changed but
# we kept the old value here to not regenerate the baseline image.
# Replace with mlines.Line2D.filled_markers when the image is regenerated.
for j, marker in enumerate("ov^<>8sp*hHDdPX"):
for i, fs in enumerate(mlines.Line2D.fillStyles):
color = next(colors)
ax.plot(j * 10 + x, y + i + .5 * (j % 2),
marker=marker,
markersize=20,
markerfacecoloralt=altcolor,
fillstyle=fs,
label=fs,
linewidth=5,
color=color,
markeredgecolor=color,
markeredgewidth=2)
ax.set_ylim([0, 7.5])
ax.set_xlim([-5, 155])
def test_markerfacecolor_fillstyle():
"""Test that markerfacecolor does not override fillstyle='none'."""
l, = plt.plot([1, 3, 2], marker=MarkerStyle('o', fillstyle='none'),
markerfacecolor='red')
assert l.get_fillstyle() == 'none'
assert l.get_markerfacecolor() == 'none'
@image_comparison(['scaled_lines'], style='default')
def test_lw_scaling():
th = np.linspace(0, 32)
fig, ax = plt.subplots()
lins_styles = ['dashed', 'dotted', 'dashdot']
cy = cycler(matplotlib.rcParams['axes.prop_cycle'])
for j, (ls, sty) in enumerate(zip(lins_styles, cy)):
for lw in np.linspace(.5, 10, 10):
ax.plot(th, j*np.ones(50) + .1 * lw, linestyle=ls, lw=lw, **sty)
def test_is_sorted_and_has_non_nan():
assert _path.is_sorted_and_has_non_nan(np.array([1, 2, 3]))
assert _path.is_sorted_and_has_non_nan(np.array([1, np.nan, 3]))
assert not _path.is_sorted_and_has_non_nan([3, 5] + [np.nan] * 100 + [0, 2])
# [2, 256] byteswapped:
assert not _path.is_sorted_and_has_non_nan(np.array([33554432, 65536], ">i4"))
n = 2 * mlines.Line2D._subslice_optim_min_size
plt.plot([np.nan] * n, range(n))
@check_figures_equal(extensions=['png'])
def test_step_markers(fig_test, fig_ref):
fig_test.subplots().step([0, 1], "-o")
fig_ref.subplots().plot([0, 0, 1], [0, 1, 1], "-o", markevery=[0, 2])
@pytest.mark.parametrize("parent", ["figure", "axes"])
@check_figures_equal(extensions=('png',))
def test_markevery(fig_test, fig_ref, parent):
np.random.seed(42)
x = np.linspace(0, 1, 14)
y = np.random.rand(len(x))
cases_test = [None, 4, (2, 5), [1, 5, 11],
[0, -1], slice(5, 10, 2),
np.arange(len(x))[y > 0.5],
0.3, (0.3, 0.4)]
cases_ref = ["11111111111111", "10001000100010", "00100001000010",
"01000100000100", "10000000000001", "00000101010000",
"01110001110110", "11011011011110", "01010011011101"]
if parent == "figure":
# float markevery ("relative to axes size") is not supported.
cases_test = cases_test[:-2]
cases_ref = cases_ref[:-2]
def add_test(x, y, *, markevery):
fig_test.add_artist(
mlines.Line2D(x, y, marker="o", markevery=markevery))
def add_ref(x, y, *, markevery):
fig_ref.add_artist(
mlines.Line2D(x, y, marker="o", markevery=markevery))
elif parent == "axes":
axs_test = iter(fig_test.subplots(3, 3).flat)
axs_ref = iter(fig_ref.subplots(3, 3).flat)
def add_test(x, y, *, markevery):
next(axs_test).plot(x, y, "-gD", markevery=markevery)
def add_ref(x, y, *, markevery):
next(axs_ref).plot(x, y, "-gD", markevery=markevery)
for case in cases_test:
add_test(x, y, markevery=case)
for case in cases_ref:
me = np.array(list(case)).astype(int).astype(bool)
add_ref(x, y, markevery=me)
def test_markevery_figure_line_unsupported_relsize():
fig = plt.figure()
fig.add_artist(mlines.Line2D([0, 1], [0, 1], marker="o", markevery=.5))
with pytest.raises(ValueError):
fig.canvas.draw()
def test_marker_as_markerstyle():
fig, ax = plt.subplots()
line, = ax.plot([2, 4, 3], marker=MarkerStyle("D"))
fig.canvas.draw()
assert line.get_marker() == "D"
# continue with smoke tests:
line.set_marker("s")
fig.canvas.draw()
line.set_marker(MarkerStyle("o"))
fig.canvas.draw()
# test Path roundtrip
triangle1 = Path._create_closed([[-1, -1], [1, -1], [0, 2]])
line2, = ax.plot([1, 3, 2], marker=MarkerStyle(triangle1), ms=22)
line3, = ax.plot([0, 2, 1], marker=triangle1, ms=22)
assert_array_equal(line2.get_marker().vertices, triangle1.vertices)
assert_array_equal(line3.get_marker().vertices, triangle1.vertices)
@image_comparison(['striped_line.png'], remove_text=True, style='mpl20')
def test_striped_lines():
rng = np.random.default_rng(19680801)
_, ax = plt.subplots()
ax.plot(rng.uniform(size=12), color='orange', gapcolor='blue',
linestyle='--', lw=5, label=' ')
ax.plot(rng.uniform(size=12), color='red', gapcolor='black',
linestyle=(0, (2, 5, 4, 2)), lw=5, label=' ', alpha=0.5)
ax.legend(handlelength=5)
@check_figures_equal(extensions=['png'])
def test_odd_dashes(fig_test, fig_ref):
fig_test.add_subplot().plot([1, 2], dashes=[1, 2, 3])
fig_ref.add_subplot().plot([1, 2], dashes=[1, 2, 3, 1, 2, 3])
def test_picking():
fig, ax = plt.subplots()
mouse_event = SimpleNamespace(x=fig.bbox.width // 2,
y=fig.bbox.height // 2 + 15)
# Default pickradius is 5, so event should not pick this line.
l0, = ax.plot([0, 1], [0, 1], picker=True)
found, indices = l0.contains(mouse_event)
assert not found
# But with a larger pickradius, this should be picked.
l1, = ax.plot([0, 1], [0, 1], picker=True, pickradius=20)
found, indices = l1.contains(mouse_event)
assert found
assert_array_equal(indices['ind'], [0])
# And if we modify the pickradius after creation, it should work as well.
l2, = ax.plot([0, 1], [0, 1], picker=True)
found, indices = l2.contains(mouse_event)
assert not found
l2.set_pickradius(20)
found, indices = l2.contains(mouse_event)
assert found
assert_array_equal(indices['ind'], [0])
@check_figures_equal(extensions=['png'])
def test_input_copy(fig_test, fig_ref):
t = np.arange(0, 6, 2)
l, = fig_test.add_subplot().plot(t, t, ".-")
t[:] = range(3)
# Trigger cache invalidation
l.set_drawstyle("steps")
fig_ref.add_subplot().plot([0, 2, 4], [0, 2, 4], ".-", drawstyle="steps")
@check_figures_equal(extensions=["png"])
def test_markevery_prop_cycle(fig_test, fig_ref):
"""Test that we can set markevery prop_cycle."""
cases = [None, 8, (30, 8), [16, 24, 30], [0, -1],
slice(100, 200, 3), 0.1, 0.3, 1.5,
(0.0, 0.1), (0.45, 0.1)]
cmap = mpl.colormaps['jet']
colors = cmap(np.linspace(0.2, 0.8, len(cases)))
x = np.linspace(-1, 1)
y = 5 * x**2
axs = fig_ref.add_subplot()
for i, markevery in enumerate(cases):
axs.plot(y - i, 'o-', markevery=markevery, color=colors[i])
matplotlib.rcParams['axes.prop_cycle'] = cycler(markevery=cases,
color=colors)
ax = fig_test.add_subplot()
for i, _ in enumerate(cases):
ax.plot(y - i, 'o-')
def test_axline_setters():
fig, ax = plt.subplots()
line1 = ax.axline((.1, .1), slope=0.6)
line2 = ax.axline((.1, .1), (.8, .4))
# Testing xy1, xy2 and slope setters.
# This should not produce an error.
line1.set_xy1((.2, .3))
line1.set_slope(2.4)
line2.set_xy1((.3, .2))
line2.set_xy2((.6, .8))
# Testing xy1, xy2 and slope getters.
# Should return the modified values.
assert line1.get_xy1() == (.2, .3)
assert line1.get_slope() == 2.4
assert line2.get_xy1() == (.3, .2)
assert line2.get_xy2() == (.6, .8)
with pytest.warns(mpl.MatplotlibDeprecationWarning):
line1.set_xy1(.2, .3)
with pytest.warns(mpl.MatplotlibDeprecationWarning):
line2.set_xy2(.6, .8)
# Testing setting xy2 and slope together.
# These test should raise a ValueError
with pytest.raises(ValueError,
match="Cannot set an 'xy2' value while 'slope' is set"):
line1.set_xy2(.2, .3)
with pytest.raises(ValueError,
match="Cannot set a 'slope' value while 'xy2' is set"):
line2.set_slope(3)
def test_axline_small_slope():
"""Test that small slopes are not coerced to zero in the transform."""
line = plt.axline((0, 0), slope=1e-14)
p1 = line.get_transform().transform_point((0, 0))
p2 = line.get_transform().transform_point((1, 1))
# y-values must be slightly different
dy = p2[1] - p1[1]
assert dy > 0
assert dy < 4e-12
@@ -0,0 +1,303 @@
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import markers
from matplotlib.path import Path
from matplotlib.testing.decorators import check_figures_equal
from matplotlib.transforms import Affine2D
import pytest
def test_marker_fillstyle():
marker_style = markers.MarkerStyle(marker='o', fillstyle='none')
assert marker_style.get_fillstyle() == 'none'
assert not marker_style.is_filled()
@pytest.mark.parametrize('marker', [
'o',
'x',
'',
'None',
r'$\frac{1}{2}$',
"$\u266B$",
1,
markers.TICKLEFT,
[[-1, 0], [1, 0]],
np.array([[-1, 0], [1, 0]]),
Path([[0, 0], [1, 0]], [Path.MOVETO, Path.LINETO]),
(5, 0), # a pentagon
(7, 1), # a 7-pointed star
(5, 2), # asterisk
(5, 0, 10), # a pentagon, rotated by 10 degrees
(7, 1, 10), # a 7-pointed star, rotated by 10 degrees
(5, 2, 10), # asterisk, rotated by 10 degrees
markers.MarkerStyle('o'),
])
def test_markers_valid(marker):
# Checking this doesn't fail.
markers.MarkerStyle(marker)
@pytest.mark.parametrize('marker', [
'square', # arbitrary string
np.array([[-0.5, 0, 1, 2, 3]]), # 1D array
(1,),
(5, 3), # second parameter of tuple must be 0, 1, or 2
(1, 2, 3, 4),
])
def test_markers_invalid(marker):
with pytest.raises(ValueError):
markers.MarkerStyle(marker)
class UnsnappedMarkerStyle(markers.MarkerStyle):
"""
A MarkerStyle where the snap threshold is force-disabled.
This is used to compare to polygon/star/asterisk markers which do not have
any snap threshold set.
"""
def _recache(self):
super()._recache()
self._snap_threshold = None
@check_figures_equal()
def test_poly_marker(fig_test, fig_ref):
ax_test = fig_test.add_subplot()
ax_ref = fig_ref.add_subplot()
# Note, some reference sizes must be different because they have unit
# *length*, while polygon markers are inscribed in a circle of unit
# *radius*. This introduces a factor of np.sqrt(2), but since size is
# squared, that becomes 2.
size = 20**2
# Squares
ax_test.scatter([0], [0], marker=(4, 0, 45), s=size)
ax_ref.scatter([0], [0], marker='s', s=size/2)
# Diamonds, with and without rotation argument
ax_test.scatter([1], [1], marker=(4, 0), s=size)
ax_ref.scatter([1], [1], marker=UnsnappedMarkerStyle('D'), s=size/2)
ax_test.scatter([1], [1.5], marker=(4, 0, 0), s=size)
ax_ref.scatter([1], [1.5], marker=UnsnappedMarkerStyle('D'), s=size/2)
# Pentagon, with and without rotation argument
ax_test.scatter([2], [2], marker=(5, 0), s=size)
ax_ref.scatter([2], [2], marker=UnsnappedMarkerStyle('p'), s=size)
ax_test.scatter([2], [2.5], marker=(5, 0, 0), s=size)
ax_ref.scatter([2], [2.5], marker=UnsnappedMarkerStyle('p'), s=size)
# Hexagon, with and without rotation argument
ax_test.scatter([3], [3], marker=(6, 0), s=size)
ax_ref.scatter([3], [3], marker='h', s=size)
ax_test.scatter([3], [3.5], marker=(6, 0, 0), s=size)
ax_ref.scatter([3], [3.5], marker='h', s=size)
# Rotated hexagon
ax_test.scatter([4], [4], marker=(6, 0, 30), s=size)
ax_ref.scatter([4], [4], marker='H', s=size)
# Octagons
ax_test.scatter([5], [5], marker=(8, 0, 22.5), s=size)
ax_ref.scatter([5], [5], marker=UnsnappedMarkerStyle('8'), s=size)
ax_test.set(xlim=(-0.5, 5.5), ylim=(-0.5, 5.5))
ax_ref.set(xlim=(-0.5, 5.5), ylim=(-0.5, 5.5))
def test_star_marker():
# We don't really have a strict equivalent to this marker, so we'll just do
# a smoke test.
size = 20**2
fig, ax = plt.subplots()
ax.scatter([0], [0], marker=(5, 1), s=size)
ax.scatter([1], [1], marker=(5, 1, 0), s=size)
ax.set(xlim=(-0.5, 0.5), ylim=(-0.5, 1.5))
# The asterisk marker is really a star with 0-size inner circle, so the ends
# are corners and get a slight bevel. The reference markers are just singular
# lines without corners, so they have no bevel, and we need to add a slight
# tolerance.
@check_figures_equal(tol=1.45)
def test_asterisk_marker(fig_test, fig_ref, request):
ax_test = fig_test.add_subplot()
ax_ref = fig_ref.add_subplot()
# Note, some reference sizes must be different because they have unit
# *length*, while asterisk markers are inscribed in a circle of unit
# *radius*. This introduces a factor of np.sqrt(2), but since size is
# squared, that becomes 2.
size = 20**2
def draw_ref_marker(y, style, size):
# As noted above, every line is doubled. Due to antialiasing, these
# doubled lines make a slight difference in the .png results.
ax_ref.scatter([y], [y], marker=UnsnappedMarkerStyle(style), s=size)
if request.getfixturevalue('ext') == 'png':
ax_ref.scatter([y], [y], marker=UnsnappedMarkerStyle(style),
s=size)
# Plus
ax_test.scatter([0], [0], marker=(4, 2), s=size)
draw_ref_marker(0, '+', size)
ax_test.scatter([0.5], [0.5], marker=(4, 2, 0), s=size)
draw_ref_marker(0.5, '+', size)
# Cross
ax_test.scatter([1], [1], marker=(4, 2, 45), s=size)
draw_ref_marker(1, 'x', size/2)
ax_test.set(xlim=(-0.5, 1.5), ylim=(-0.5, 1.5))
ax_ref.set(xlim=(-0.5, 1.5), ylim=(-0.5, 1.5))
# The bullet mathtext marker is not quite a circle, so this is not a perfect match, but
# it is close enough to confirm that the text-based marker is centred correctly. But we
# still need a small tolerance to work around that difference.
@check_figures_equal(extensions=['png'], tol=1.86)
def test_text_marker(fig_ref, fig_test):
ax_ref = fig_ref.add_subplot()
ax_test = fig_test.add_subplot()
ax_ref.plot(0, 0, marker=r'o', markersize=100, markeredgewidth=0)
ax_test.plot(0, 0, marker=r'$\bullet$', markersize=100, markeredgewidth=0)
@check_figures_equal()
def test_marker_clipping(fig_ref, fig_test):
# Plotting multiple markers can trigger different optimized paths in
# backends, so compare single markers vs multiple to ensure they are
# clipped correctly.
marker_count = len(markers.MarkerStyle.markers)
marker_size = 50
ncol = 7
nrow = marker_count // ncol + 1
width = 2 * marker_size * ncol
height = 2 * marker_size * nrow * 2
fig_ref.set_size_inches((width / fig_ref.dpi, height / fig_ref.dpi))
ax_ref = fig_ref.add_axes([0, 0, 1, 1])
fig_test.set_size_inches((width / fig_test.dpi, height / fig_ref.dpi))
ax_test = fig_test.add_axes([0, 0, 1, 1])
for i, marker in enumerate(markers.MarkerStyle.markers):
x = i % ncol
y = i // ncol * 2
# Singular markers per call.
ax_ref.plot([x, x], [y, y + 1], c='k', linestyle='-', lw=3)
ax_ref.plot(x, y, c='k',
marker=marker, markersize=marker_size, markeredgewidth=10,
fillstyle='full', markerfacecolor='white')
ax_ref.plot(x, y + 1, c='k',
marker=marker, markersize=marker_size, markeredgewidth=10,
fillstyle='full', markerfacecolor='white')
# Multiple markers in a single call.
ax_test.plot([x, x], [y, y + 1], c='k', linestyle='-', lw=3,
marker=marker, markersize=marker_size, markeredgewidth=10,
fillstyle='full', markerfacecolor='white')
ax_ref.set(xlim=(-0.5, ncol), ylim=(-0.5, 2 * nrow))
ax_test.set(xlim=(-0.5, ncol), ylim=(-0.5, 2 * nrow))
ax_ref.axis('off')
ax_test.axis('off')
def test_marker_init_transforms():
"""Test that initializing marker with transform is a simple addition."""
marker = markers.MarkerStyle("o")
t = Affine2D().translate(1, 1)
t_marker = markers.MarkerStyle("o", transform=t)
assert marker.get_transform() + t == t_marker.get_transform()
def test_marker_init_joinstyle():
marker = markers.MarkerStyle("*")
styled_marker = markers.MarkerStyle("*", joinstyle="round")
assert styled_marker.get_joinstyle() == "round"
assert marker.get_joinstyle() != "round"
def test_marker_init_captyle():
marker = markers.MarkerStyle("*")
styled_marker = markers.MarkerStyle("*", capstyle="round")
assert styled_marker.get_capstyle() == "round"
assert marker.get_capstyle() != "round"
@pytest.mark.parametrize("marker,transform,expected", [
(markers.MarkerStyle("o"), Affine2D().translate(1, 1),
Affine2D().translate(1, 1)),
(markers.MarkerStyle("o", transform=Affine2D().translate(1, 1)),
Affine2D().translate(1, 1), Affine2D().translate(2, 2)),
(markers.MarkerStyle("$|||$", transform=Affine2D().translate(1, 1)),
Affine2D().translate(1, 1), Affine2D().translate(2, 2)),
(markers.MarkerStyle(
markers.TICKLEFT, transform=Affine2D().translate(1, 1)),
Affine2D().translate(1, 1), Affine2D().translate(2, 2)),
])
def test_marker_transformed(marker, transform, expected):
new_marker = marker.transformed(transform)
assert new_marker is not marker
assert new_marker.get_user_transform() == expected
assert marker._user_transform is not new_marker._user_transform
def test_marker_rotated_invalid():
marker = markers.MarkerStyle("o")
with pytest.raises(ValueError):
new_marker = marker.rotated()
with pytest.raises(ValueError):
new_marker = marker.rotated(deg=10, rad=10)
@pytest.mark.parametrize("marker,deg,rad,expected", [
(markers.MarkerStyle("o"), 10, None, Affine2D().rotate_deg(10)),
(markers.MarkerStyle("o"), None, 0.01, Affine2D().rotate(0.01)),
(markers.MarkerStyle("o", transform=Affine2D().translate(1, 1)),
10, None, Affine2D().translate(1, 1).rotate_deg(10)),
(markers.MarkerStyle("o", transform=Affine2D().translate(1, 1)),
None, 0.01, Affine2D().translate(1, 1).rotate(0.01)),
(markers.MarkerStyle("$|||$", transform=Affine2D().translate(1, 1)),
10, None, Affine2D().translate(1, 1).rotate_deg(10)),
(markers.MarkerStyle(
markers.TICKLEFT, transform=Affine2D().translate(1, 1)),
10, None, Affine2D().translate(1, 1).rotate_deg(10)),
])
def test_marker_rotated(marker, deg, rad, expected):
new_marker = marker.rotated(deg=deg, rad=rad)
assert new_marker is not marker
assert new_marker.get_user_transform() == expected
assert marker._user_transform is not new_marker._user_transform
def test_marker_scaled():
marker = markers.MarkerStyle("1")
new_marker = marker.scaled(2)
assert new_marker is not marker
assert new_marker.get_user_transform() == Affine2D().scale(2)
assert marker._user_transform is not new_marker._user_transform
new_marker = marker.scaled(2, 3)
assert new_marker is not marker
assert new_marker.get_user_transform() == Affine2D().scale(2, 3)
assert marker._user_transform is not new_marker._user_transform
marker = markers.MarkerStyle("1", transform=Affine2D().translate(1, 1))
new_marker = marker.scaled(2)
assert new_marker is not marker
expected = Affine2D().translate(1, 1).scale(2)
assert new_marker.get_user_transform() == expected
assert marker._user_transform is not new_marker._user_transform
def test_alt_transform():
m1 = markers.MarkerStyle("o", "left")
m2 = markers.MarkerStyle("o", "left", Affine2D().rotate_deg(90))
assert m1.get_alt_transform().rotate_deg(90) == m2.get_alt_transform()
@@ -0,0 +1,560 @@
from __future__ import annotations
import io
from pathlib import Path
import platform
import re
from xml.etree import ElementTree as ET
from typing import Any
import numpy as np
from packaging.version import parse as parse_version
import pyparsing
import pytest
import matplotlib as mpl
from matplotlib.testing.decorators import check_figures_equal, image_comparison
import matplotlib.pyplot as plt
from matplotlib import mathtext, _mathtext
pyparsing_version = parse_version(pyparsing.__version__)
# If test is removed, use None as placeholder
math_tests = [
r'$a+b+\dot s+\dot{s}+\ldots$',
r'$x\hspace{-0.2}\doteq\hspace{-0.2}y$',
r'\$100.00 $\alpha \_$',
r'$\frac{\$100.00}{y}$',
r'$x y$',
r'$x+y\ x=y\ x<y\ x:y\ x,y\ x@y$',
r'$100\%y\ x*y\ x/y x\$y$',
r'$x\leftarrow y\ x\forall y\ x-y$',
r'$x \sf x \bf x {\cal X} \rm x$',
r'$x\ x\,x\;x\quad x\qquad x\!x\hspace{ 0.5 }y$',
r'$\{ \rm braces \}$',
r'$\left[\left\lfloor\frac{5}{\frac{\left(3\right)}{4}} y\right)\right]$',
r'$\left(x\right)$',
r'$\sin(x)$',
r'$x_2$',
r'$x^2$',
r'$x^2_y$',
r'$x_y^2$',
(r'$\sum _{\genfrac{}{}{0}{}{0\leq i\leq m}{0<j<n}}f\left(i,j\right)'
r'\mathcal{R}\prod_{i=\alpha_{i+1}}^\infty a_i \sin(2 \pi f x_i)'
r"\sqrt[2]{\prod^\frac{x}{2\pi^2}_\infty}$"),
r'$x = \frac{x+\frac{5}{2}}{\frac{y+3}{8}}$',
r'$dz/dt = \gamma x^2 + {\rm sin}(2\pi y+\phi)$',
r'Foo: $\alpha_{i+1}^j = {\rm sin}(2\pi f_j t_i) e^{-5 t_i/\tau}$',
None,
r'Variable $i$ is good',
r'$\Delta_i^j$',
r'$\Delta^j_{i+1}$',
r'$\ddot{o}\acute{e}\grave{e}\hat{O}\breve{\imath}\tilde{n}\vec{q}$',
r"$\arccos((x^i))$",
r"$\gamma = \frac{x=\frac{6}{8}}{y} \delta$",
r'$\limsup_{x\to\infty}$',
None,
r"$f'\quad f'''(x)\quad ''/\mathrm{yr}$",
r'$\frac{x_2888}{y}$',
r"$\sqrt[3]{\frac{X_2}{Y}}=5$",
None,
r"$\sqrt[3]{x}=5$",
r'$\frac{X}{\frac{X}{Y}}$',
r"$W^{3\beta}_{\delta_1 \rho_1 \sigma_2} = U^{3\beta}_{\delta_1 \rho_1} + \frac{1}{8 \pi 2} \int^{\alpha_2}_{\alpha_2} d \alpha^\prime_2 \left[\frac{ U^{2\beta}_{\delta_1 \rho_1} - \alpha^\prime_2U^{1\beta}_{\rho_1 \sigma_2} }{U^{0\beta}_{\rho_1 \sigma_2}}\right]$",
r'$\mathcal{H} = \int d \tau \left(\epsilon E^2 + \mu H^2\right)$',
r'$\widehat{abc}\widetilde{def}$',
'$\\Gamma \\Delta \\Theta \\Lambda \\Xi \\Pi \\Sigma \\Upsilon \\Phi \\Psi \\Omega$',
'$\\alpha \\beta \\gamma \\delta \\epsilon \\zeta \\eta \\theta \\iota \\lambda \\mu \\nu \\xi \\pi \\kappa \\rho \\sigma \\tau \\upsilon \\phi \\chi \\psi$',
# The following examples are from the MathML torture test here:
# https://www-archive.mozilla.org/projects/mathml/demo/texvsmml.xhtml
r'${x}^{2}{y}^{2}$',
r'${}_{2}F_{3}$',
r'$\frac{x+{y}^{2}}{k+1}$',
r'$x+{y}^{\frac{2}{k+1}}$',
r'$\frac{a}{b/2}$',
r'${a}_{0}+\frac{1}{{a}_{1}+\frac{1}{{a}_{2}+\frac{1}{{a}_{3}+\frac{1}{{a}_{4}}}}}$',
r'${a}_{0}+\frac{1}{{a}_{1}+\frac{1}{{a}_{2}+\frac{1}{{a}_{3}+\frac{1}{{a}_{4}}}}}$',
r'$\binom{n}{k/2}$',
r'$\binom{p}{2}{x}^{2}{y}^{p-2}-\frac{1}{1-x}\frac{1}{1-{x}^{2}}$',
r'${x}^{2y}$',
r'$\sum _{i=1}^{p}\sum _{j=1}^{q}\sum _{k=1}^{r}{a}_{ij}{b}_{jk}{c}_{ki}$',
r'$\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+\sqrt{1+x}}}}}}}$',
r'$\left(\frac{{\partial }^{2}}{\partial {x}^{2}}+\frac{{\partial }^{2}}{\partial {y}^{2}}\right){|\varphi \left(x+iy\right)|}^{2}=0$',
r'${2}^{{2}^{{2}^{x}}}$',
r'${\int }_{1}^{x}\frac{\mathrm{dt}}{t}$',
r'$\int {\int }_{D}\mathrm{dx} \mathrm{dy}$',
# mathtex doesn't support array
# 'mmltt18' : r'$f\left(x\right)=\left\{\begin{array}{cc}\hfill 1/3\hfill & \text{if_}0\le x\le 1;\hfill \\ \hfill 2/3\hfill & \hfill \text{if_}3\le x\le 4;\hfill \\ \hfill 0\hfill & \text{elsewhere.}\hfill \end{array}$',
# mathtex doesn't support stackrel
# 'mmltt19' : r'$\stackrel{\stackrel{k\text{times}}{\ufe37}}{x+...+x}$',
r'${y}_{{x}^{2}}$',
# mathtex doesn't support the "\text" command
# 'mmltt21' : r'$\sum _{p\text{\prime}}f\left(p\right)={\int }_{t>1}f\left(t\right) d\pi \left(t\right)$',
# mathtex doesn't support array
# 'mmltt23' : r'$\left(\begin{array}{cc}\hfill \left(\begin{array}{cc}\hfill a\hfill & \hfill b\hfill \\ \hfill c\hfill & \hfill d\hfill \end{array}\right)\hfill & \hfill \left(\begin{array}{cc}\hfill e\hfill & \hfill f\hfill \\ \hfill g\hfill & \hfill h\hfill \end{array}\right)\hfill \\ \hfill 0\hfill & \hfill \left(\begin{array}{cc}\hfill i\hfill & \hfill j\hfill \\ \hfill k\hfill & \hfill l\hfill \end{array}\right)\hfill \end{array}\right)$',
# mathtex doesn't support array
# 'mmltt24' : r'$det|\begin{array}{ccccc}\hfill {c}_{0}\hfill & \hfill {c}_{1}\hfill & \hfill {c}_{2}\hfill & \hfill \dots \hfill & \hfill {c}_{n}\hfill \\ \hfill {c}_{1}\hfill & \hfill {c}_{2}\hfill & \hfill {c}_{3}\hfill & \hfill \dots \hfill & \hfill {c}_{n+1}\hfill \\ \hfill {c}_{2}\hfill & \hfill {c}_{3}\hfill & \hfill {c}_{4}\hfill & \hfill \dots \hfill & \hfill {c}_{n+2}\hfill \\ \hfill \u22ee\hfill & \hfill \u22ee\hfill & \hfill \u22ee\hfill & \hfill \hfill & \hfill \u22ee\hfill \\ \hfill {c}_{n}\hfill & \hfill {c}_{n+1}\hfill & \hfill {c}_{n+2}\hfill & \hfill \dots \hfill & \hfill {c}_{2n}\hfill \end{array}|>0$',
r'${y}_{{x}_{2}}$',
r'${x}_{92}^{31415}+\pi $',
r'${x}_{{y}_{b}^{a}}^{{z}_{c}^{d}}$',
r'${y}_{3}^{\prime \prime \prime }$',
# End of the MathML torture tests.
r"$\left( \xi \left( 1 - \xi \right) \right)$", # Bug 2969451
r"$\left(2 \, a=b\right)$", # Sage bug #8125
r"$? ! &$", # github issue #466
None,
None,
r"$\left\Vert \frac{a}{b} \right\Vert \left\vert \frac{a}{b} \right\vert \left\| \frac{a}{b}\right\| \left| \frac{a}{b} \right| \Vert a \Vert \vert b \vert \| a \| | b |$",
r'$\mathring{A} \AA$',
r'$M \, M \thinspace M \/ M \> M \: M \; M \ M \enspace M \quad M \qquad M \! M$',
r'$\Cap$ $\Cup$ $\leftharpoonup$ $\barwedge$ $\rightharpoonup$',
r'$\hspace{-0.2}\dotplus\hspace{-0.2}$ $\hspace{-0.2}\doteq\hspace{-0.2}$ $\hspace{-0.2}\doteqdot\hspace{-0.2}$ $\ddots$',
r'$xyz^kx_kx^py^{p-2} d_i^jb_jc_kd x^j_i E^0 E^0_u$', # github issue #4873
r'${xyz}^k{x}_{k}{x}^{p}{y}^{p-2} {d}_{i}^{j}{b}_{j}{c}_{k}{d} {x}^{j}_{i}{E}^{0}{E}^0_u$',
r'${\int}_x^x x\oint_x^x x\int_{X}^{X}x\int_x x \int^x x \int_{x} x\int^{x}{\int}_{x} x{\int}^{x}_{x}x$',
r'testing$^{123}$',
None,
r'$6-2$; $-2$; $ -2$; ${-2}$; ${ -2}$; $20^{+3}_{-2}$',
r'$\overline{\omega}^x \frac{1}{2}_0^x$', # github issue #5444
r'$,$ $.$ $1{,}234{, }567{ , }890$ and $1,234,567,890$', # github issue 5799
r'$\left(X\right)_{a}^{b}$', # github issue 7615
r'$\dfrac{\$100.00}{y}$', # github issue #1888
r'$a=-b-c$' # github issue #28180
]
# 'svgastext' tests switch svg output to embed text as text (rather than as
# paths).
svgastext_math_tests = [
r'$-$-',
]
# 'lightweight' tests test only a single fontset (dejavusans, which is the
# default) and only png outputs, in order to minimize the size of baseline
# images.
lightweight_math_tests = [
r'$\sqrt[ab]{123}$', # github issue #8665
r'$x \overset{f}{\rightarrow} \overset{f}{x} \underset{xx}{ff} \overset{xx}{ff} \underset{f}{x} \underset{f}{\leftarrow} x$', # github issue #18241
r'$\sum x\quad\sum^nx\quad\sum_nx\quad\sum_n^nx\quad\prod x\quad\prod^nx\quad\prod_nx\quad\prod_n^nx$', # GitHub issue 18085
r'$1.$ $2.$ $19680801.$ $a.$ $b.$ $mpl.$',
r'$\text{text}_{\text{sub}}^{\text{sup}} + \text{\$foo\$} + \frac{\text{num}}{\mathbf{\text{den}}}\text{with space, curly brackets \{\}, and dash -}$',
r'$\boldsymbol{abcde} \boldsymbol{+} \boldsymbol{\Gamma + \Omega} \boldsymbol{01234} \boldsymbol{\alpha * \beta}$',
r'$\left\lbrace\frac{\left\lbrack A^b_c\right\rbrace}{\left\leftbrace D^e_f \right\rbrack}\right\rightbrace\ \left\leftparen\max_{x} \left\lgroup \frac{A}{B}\right\rgroup \right\rightparen$',
r'$\left( a\middle. b \right)$ $\left( \frac{a}{b} \middle\vert x_i \in P^S \right)$ $\left[ 1 - \middle| a\middle| + \left( x - \left\lfloor \dfrac{a}{b}\right\rfloor \right) \right]$',
r'$\sum_{\substack{k = 1\\ k \neq \lfloor n/2\rfloor}}^{n}P(i,j) \sum_{\substack{i \neq 0\\ -1 \leq i \leq 3\\ 1 \leq j \leq 5}} F^i(x,y) \sum_{\substack{\left \lfloor \frac{n}{2} \right\rfloor}} F(n)$',
]
digits = "0123456789"
uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowercase = "abcdefghijklmnopqrstuvwxyz"
uppergreek = ("\\Gamma \\Delta \\Theta \\Lambda \\Xi \\Pi \\Sigma \\Upsilon \\Phi \\Psi "
"\\Omega")
lowergreek = ("\\alpha \\beta \\gamma \\delta \\epsilon \\zeta \\eta \\theta \\iota "
"\\lambda \\mu \\nu \\xi \\pi \\kappa \\rho \\sigma \\tau \\upsilon "
"\\phi \\chi \\psi")
all = [digits, uppercase, lowercase, uppergreek, lowergreek]
# Use stubs to reserve space if tests are removed
# stub should be of the form (None, N) where N is the number of strings that
# used to be tested
# Add new tests at the end.
font_test_specs: list[tuple[None | list[str], Any]] = [
([], all),
(['mathrm'], all),
(['mathbf'], all),
(['mathit'], all),
(['mathtt'], [digits, uppercase, lowercase]),
(None, 3),
(None, 3),
(None, 3),
(['mathbb'], [digits, uppercase, lowercase,
r'\Gamma \Pi \Sigma \gamma \pi']),
(['mathrm', 'mathbb'], [digits, uppercase, lowercase,
r'\Gamma \Pi \Sigma \gamma \pi']),
(['mathbf', 'mathbb'], [digits, uppercase, lowercase,
r'\Gamma \Pi \Sigma \gamma \pi']),
(['mathcal'], [uppercase]),
(['mathfrak'], [uppercase, lowercase]),
(['mathbf', 'mathfrak'], [uppercase, lowercase]),
(['mathscr'], [uppercase, lowercase]),
(['mathsf'], [digits, uppercase, lowercase]),
(['mathrm', 'mathsf'], [digits, uppercase, lowercase]),
(['mathbf', 'mathsf'], [digits, uppercase, lowercase]),
(['mathbfit'], all),
]
font_tests: list[None | str] = []
for fonts, chars in font_test_specs:
if fonts is None:
font_tests.extend([None] * chars)
else:
wrapper = ''.join([
' '.join(fonts),
' $',
*(r'\%s{' % font for font in fonts),
'%s',
*('}' for font in fonts),
'$',
])
for font_set in chars:
font_tests.append(wrapper % font_set)
@pytest.fixture
def baseline_images(request, fontset, index, text):
if text is None:
pytest.skip("test has been removed")
return ['%s_%s_%02d' % (request.param, fontset, index)]
@pytest.mark.parametrize(
'index, text', enumerate(math_tests), ids=range(len(math_tests)))
@pytest.mark.parametrize(
'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif'])
@pytest.mark.parametrize('baseline_images', ['mathtext'], indirect=True)
@image_comparison(baseline_images=None,
tol=0.011 if platform.machine() in ('ppc64le', 's390x') else 0)
def test_mathtext_rendering(baseline_images, fontset, index, text):
mpl.rcParams['mathtext.fontset'] = fontset
fig = plt.figure(figsize=(5.25, 0.75))
fig.text(0.5, 0.5, text,
horizontalalignment='center', verticalalignment='center')
@pytest.mark.parametrize('index, text', enumerate(svgastext_math_tests),
ids=range(len(svgastext_math_tests)))
@pytest.mark.parametrize('fontset', ['cm', 'dejavusans'])
@pytest.mark.parametrize('baseline_images', ['mathtext0'], indirect=True)
@image_comparison(
baseline_images=None, extensions=['svg'],
savefig_kwarg={'metadata': { # Minimize image size.
'Creator': None, 'Date': None, 'Format': None, 'Type': None}})
def test_mathtext_rendering_svgastext(baseline_images, fontset, index, text):
mpl.rcParams['mathtext.fontset'] = fontset
mpl.rcParams['svg.fonttype'] = 'none' # Minimize image size.
fig = plt.figure(figsize=(5.25, 0.75))
fig.patch.set(visible=False) # Minimize image size.
fig.text(0.5, 0.5, text,
horizontalalignment='center', verticalalignment='center')
@pytest.mark.parametrize('index, text', enumerate(lightweight_math_tests),
ids=range(len(lightweight_math_tests)))
@pytest.mark.parametrize('fontset', ['dejavusans'])
@pytest.mark.parametrize('baseline_images', ['mathtext1'], indirect=True)
@image_comparison(baseline_images=None, extensions=['png'])
def test_mathtext_rendering_lightweight(baseline_images, fontset, index, text):
fig = plt.figure(figsize=(5.25, 0.75))
fig.text(0.5, 0.5, text, math_fontfamily=fontset,
horizontalalignment='center', verticalalignment='center')
@pytest.mark.parametrize(
'index, text', enumerate(font_tests), ids=range(len(font_tests)))
@pytest.mark.parametrize(
'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif'])
@pytest.mark.parametrize('baseline_images', ['mathfont'], indirect=True)
@image_comparison(baseline_images=None, extensions=['png'],
tol=0.011 if platform.machine() in ('ppc64le', 's390x') else 0)
def test_mathfont_rendering(baseline_images, fontset, index, text):
mpl.rcParams['mathtext.fontset'] = fontset
fig = plt.figure(figsize=(5.25, 0.75))
fig.text(0.5, 0.5, text,
horizontalalignment='center', verticalalignment='center')
@check_figures_equal(extensions=["png"])
def test_short_long_accents(fig_test, fig_ref):
acc_map = _mathtext.Parser._accent_map
short_accs = [s for s in acc_map if len(s) == 1]
corresponding_long_accs = []
for s in short_accs:
l, = (l for l in acc_map if len(l) > 1 and acc_map[l] == acc_map[s])
corresponding_long_accs.append(l)
fig_test.text(0, .5, "$" + "".join(rf"\{s}a" for s in short_accs) + "$")
fig_ref.text(
0, .5, "$" + "".join(fr"\{l} a" for l in corresponding_long_accs) + "$")
def test_fontinfo():
fontpath = mpl.font_manager.findfont("DejaVu Sans")
font = mpl.ft2font.FT2Font(fontpath)
table = font.get_sfnt_table("head")
assert table is not None
assert table['version'] == (1, 0)
# See gh-26152 for more context on this xfail
@pytest.mark.xfail(pyparsing_version.release == (3, 1, 0),
reason="Error messages are incorrect for this version")
@pytest.mark.parametrize(
'math, msg',
[
(r'$\hspace{}$', r'Expected \hspace{space}'),
(r'$\hspace{foo}$', r'Expected \hspace{space}'),
(r'$\sinx$', r'Unknown symbol: \sinx'),
(r'$\dotx$', r'Unknown symbol: \dotx'),
(r'$\frac$', r'Expected \frac{num}{den}'),
(r'$\frac{}{}$', r'Expected \frac{num}{den}'),
(r'$\binom$', r'Expected \binom{num}{den}'),
(r'$\binom{}{}$', r'Expected \binom{num}{den}'),
(r'$\genfrac$',
r'Expected \genfrac{ldelim}{rdelim}{rulesize}{style}{num}{den}'),
(r'$\genfrac{}{}{}{}{}{}$',
r'Expected \genfrac{ldelim}{rdelim}{rulesize}{style}{num}{den}'),
(r'$\sqrt$', r'Expected \sqrt{value}'),
(r'$\sqrt f$', r'Expected \sqrt{value}'),
(r'$\overline$', r'Expected \overline{body}'),
(r'$\overline{}$', r'Expected \overline{body}'),
(r'$\leftF$', r'Expected a delimiter'),
(r'$\rightF$', r'Unknown symbol: \rightF'),
(r'$\left(\right$', r'Expected a delimiter'),
# PyParsing 2 uses double quotes, PyParsing 3 uses single quotes and an
# extra backslash.
(r'$\left($', re.compile(r'Expected ("|\'\\)\\right["\']')),
(r'$\dfrac$', r'Expected \dfrac{num}{den}'),
(r'$\dfrac{}{}$', r'Expected \dfrac{num}{den}'),
(r'$\overset$', r'Expected \overset{annotation}{body}'),
(r'$\underset$', r'Expected \underset{annotation}{body}'),
(r'$\foo$', r'Unknown symbol: \foo'),
(r'$a^2^2$', r'Double superscript'),
(r'$a_2_2$', r'Double subscript'),
(r'$a^2_a^2$', r'Double superscript'),
(r'$a = {b$', r"Expected '}'"),
],
ids=[
'hspace without value',
'hspace with invalid value',
'function without space',
'accent without space',
'frac without parameters',
'frac with empty parameters',
'binom without parameters',
'binom with empty parameters',
'genfrac without parameters',
'genfrac with empty parameters',
'sqrt without parameters',
'sqrt with invalid value',
'overline without parameters',
'overline with empty parameter',
'left with invalid delimiter',
'right with invalid delimiter',
'unclosed parentheses with sizing',
'unclosed parentheses without sizing',
'dfrac without parameters',
'dfrac with empty parameters',
'overset without parameters',
'underset without parameters',
'unknown symbol',
'double superscript',
'double subscript',
'super on sub without braces',
'unclosed group',
]
)
def test_mathtext_exceptions(math, msg):
parser = mathtext.MathTextParser('agg')
match = re.escape(msg) if isinstance(msg, str) else msg
with pytest.raises(ValueError, match=match):
parser.parse(math)
def test_get_unicode_index_exception():
with pytest.raises(ValueError):
_mathtext.get_unicode_index(r'\foo')
def test_single_minus_sign():
fig = plt.figure()
fig.text(0.5, 0.5, '$-$')
fig.canvas.draw()
t = np.asarray(fig.canvas.renderer.buffer_rgba())
assert (t != 0xff).any() # assert that canvas is not all white.
@check_figures_equal(extensions=["png"])
def test_spaces(fig_test, fig_ref):
fig_test.text(.5, .5, r"$1\,2\>3\ 4$")
fig_ref.text(.5, .5, r"$1\/2\:3~4$")
@check_figures_equal(extensions=["png"])
def test_operator_space(fig_test, fig_ref):
fig_test.text(0.1, 0.1, r"$\log 6$")
fig_test.text(0.1, 0.2, r"$\log(6)$")
fig_test.text(0.1, 0.3, r"$\arcsin 6$")
fig_test.text(0.1, 0.4, r"$\arcsin|6|$")
fig_test.text(0.1, 0.5, r"$\operatorname{op} 6$") # GitHub issue #553
fig_test.text(0.1, 0.6, r"$\operatorname{op}[6]$")
fig_test.text(0.1, 0.7, r"$\cos^2$")
fig_test.text(0.1, 0.8, r"$\log_2$")
fig_test.text(0.1, 0.9, r"$\sin^2 \cos$") # GitHub issue #17852
fig_ref.text(0.1, 0.1, r"$\mathrm{log\,}6$")
fig_ref.text(0.1, 0.2, r"$\mathrm{log}(6)$")
fig_ref.text(0.1, 0.3, r"$\mathrm{arcsin\,}6$")
fig_ref.text(0.1, 0.4, r"$\mathrm{arcsin}|6|$")
fig_ref.text(0.1, 0.5, r"$\mathrm{op\,}6$")
fig_ref.text(0.1, 0.6, r"$\mathrm{op}[6]$")
fig_ref.text(0.1, 0.7, r"$\mathrm{cos}^2$")
fig_ref.text(0.1, 0.8, r"$\mathrm{log}_2$")
fig_ref.text(0.1, 0.9, r"$\mathrm{sin}^2 \mathrm{\,cos}$")
@check_figures_equal(extensions=["png"])
def test_inverted_delimiters(fig_test, fig_ref):
fig_test.text(.5, .5, r"$\left)\right($", math_fontfamily="dejavusans")
fig_ref.text(.5, .5, r"$)($", math_fontfamily="dejavusans")
@check_figures_equal(extensions=["png"])
def test_genfrac_displaystyle(fig_test, fig_ref):
fig_test.text(0.1, 0.1, r"$\dfrac{2x}{3y}$")
thickness = _mathtext.TruetypeFonts.get_underline_thickness(
None, None, fontsize=mpl.rcParams["font.size"],
dpi=mpl.rcParams["savefig.dpi"])
fig_ref.text(0.1, 0.1, r"$\genfrac{}{}{%f}{0}{2x}{3y}$" % thickness)
def test_mathtext_fallback_valid():
for fallback in ['cm', 'stix', 'stixsans', 'None']:
mpl.rcParams['mathtext.fallback'] = fallback
def test_mathtext_fallback_invalid():
for fallback in ['abc', '']:
with pytest.raises(ValueError, match="not a valid fallback font name"):
mpl.rcParams['mathtext.fallback'] = fallback
@pytest.mark.parametrize(
"fallback,fontlist",
[("cm", ['DejaVu Sans', 'mpltest', 'STIXGeneral', 'cmr10', 'STIXGeneral']),
("stix", ['DejaVu Sans', 'mpltest', 'STIXGeneral', 'STIXGeneral', 'STIXGeneral'])])
def test_mathtext_fallback(fallback, fontlist):
mpl.font_manager.fontManager.addfont(
str(Path(__file__).resolve().parent / 'mpltest.ttf'))
mpl.rcParams["svg.fonttype"] = 'none'
mpl.rcParams['mathtext.fontset'] = 'custom'
mpl.rcParams['mathtext.rm'] = 'mpltest'
mpl.rcParams['mathtext.it'] = 'mpltest:italic'
mpl.rcParams['mathtext.bf'] = 'mpltest:bold'
mpl.rcParams['mathtext.bfit'] = 'mpltest:italic:bold'
mpl.rcParams['mathtext.fallback'] = fallback
test_str = r'a$A\AA\breve\gimel$'
buff = io.BytesIO()
fig, ax = plt.subplots()
fig.text(.5, .5, test_str, fontsize=40, ha='center')
fig.savefig(buff, format="svg")
tspans = (ET.fromstring(buff.getvalue())
.findall(".//{http://www.w3.org/2000/svg}tspan[@style]"))
char_fonts = [
re.search(r"font-family: '([\w ]+)'", tspan.attrib["style"]).group(1)
for tspan in tspans]
assert char_fonts == fontlist, f'Expected {fontlist}, got {char_fonts}'
mpl.font_manager.fontManager.ttflist.pop()
def test_math_to_image(tmp_path):
mathtext.math_to_image('$x^2$', tmp_path / 'example.png')
mathtext.math_to_image('$x^2$', io.BytesIO())
mathtext.math_to_image('$x^2$', io.BytesIO(), color='Maroon')
@image_comparison(baseline_images=['math_fontfamily_image.png'],
savefig_kwarg={'dpi': 40})
def test_math_fontfamily():
fig = plt.figure(figsize=(10, 3))
fig.text(0.2, 0.7, r"$This\ text\ should\ have\ one\ font$",
size=24, math_fontfamily='dejavusans')
fig.text(0.2, 0.3, r"$This\ text\ should\ have\ another$",
size=24, math_fontfamily='stix')
def test_default_math_fontfamily():
mpl.rcParams['mathtext.fontset'] = 'cm'
test_str = r'abc$abc\alpha$'
fig, ax = plt.subplots()
text1 = fig.text(0.1, 0.1, test_str, font='Arial')
prop1 = text1.get_fontproperties()
assert prop1.get_math_fontfamily() == 'cm'
text2 = fig.text(0.2, 0.2, test_str, fontproperties='Arial')
prop2 = text2.get_fontproperties()
assert prop2.get_math_fontfamily() == 'cm'
fig.draw_without_rendering()
def test_argument_order():
mpl.rcParams['mathtext.fontset'] = 'cm'
test_str = r'abc$abc\alpha$'
fig, ax = plt.subplots()
text1 = fig.text(0.1, 0.1, test_str,
math_fontfamily='dejavusans', font='Arial')
prop1 = text1.get_fontproperties()
assert prop1.get_math_fontfamily() == 'dejavusans'
text2 = fig.text(0.2, 0.2, test_str,
math_fontfamily='dejavusans', fontproperties='Arial')
prop2 = text2.get_fontproperties()
assert prop2.get_math_fontfamily() == 'dejavusans'
text3 = fig.text(0.3, 0.3, test_str,
font='Arial', math_fontfamily='dejavusans')
prop3 = text3.get_fontproperties()
assert prop3.get_math_fontfamily() == 'dejavusans'
text4 = fig.text(0.4, 0.4, test_str,
fontproperties='Arial', math_fontfamily='dejavusans')
prop4 = text4.get_fontproperties()
assert prop4.get_math_fontfamily() == 'dejavusans'
fig.draw_without_rendering()
def test_mathtext_cmr10_minus_sign():
# cmr10 does not contain a minus sign and used to issue a warning
# RuntimeWarning: Glyph 8722 missing from current font.
mpl.rcParams['font.family'] = 'cmr10'
mpl.rcParams['axes.formatter.use_mathtext'] = True
fig, ax = plt.subplots()
ax.plot(range(-1, 1), range(-1, 1))
# draw to make sure we have no warnings
fig.canvas.draw()
def test_mathtext_operators():
test_str = r'''
\increment \smallin \notsmallowns
\smallowns \QED \rightangle
\smallintclockwise \smallvarointclockwise
\smallointctrcclockwise
\ratio \minuscolon \dotsminusdots
\sinewave \simneqq \nlesssim
\ngtrsim \nlessgtr \ngtrless
\cupleftarrow \oequal \rightassert
\rightModels \hermitmatrix \barvee
\measuredrightangle \varlrtriangle
\equalparallel \npreccurlyeq \nsucccurlyeq
\nsqsubseteq \nsqsupseteq \sqsubsetneq
\sqsupsetneq \disin \varisins
\isins \isindot \varisinobar
\isinobar \isinvb \isinE
\nisd \varnis \nis
\varniobar \niobar \bagmember
\triangle'''.split()
fig = plt.figure()
for x, i in enumerate(test_str):
fig.text(0.5, (x + 0.5)/len(test_str), r'${%s}$' % i)
fig.draw_without_rendering()
@check_figures_equal(extensions=["png"])
def test_boldsymbol(fig_test, fig_ref):
fig_test.text(0.1, 0.2, r"$\boldsymbol{\mathrm{abc0123\alpha}}$")
fig_ref.text(0.1, 0.2, r"$\mathrm{abc0123\alpha}$")
@@ -0,0 +1,96 @@
import os
import subprocess
import sys
from unittest.mock import patch
import pytest
import matplotlib
from matplotlib.testing import subprocess_run_for_testing
@pytest.mark.parametrize('version_str, version_tuple', [
('3.5.0', (3, 5, 0, 'final', 0)),
('3.5.0rc2', (3, 5, 0, 'candidate', 2)),
('3.5.0.dev820+g6768ef8c4c', (3, 5, 0, 'alpha', 820)),
('3.5.0.post820+g6768ef8c4c', (3, 5, 1, 'alpha', 820)),
])
def test_parse_to_version_info(version_str, version_tuple):
assert matplotlib._parse_to_version_info(version_str) == version_tuple
@pytest.mark.skipif(sys.platform == "win32",
reason="chmod() doesn't work as is on Windows")
@pytest.mark.skipif(sys.platform != "win32" and os.geteuid() == 0,
reason="chmod() doesn't work as root")
def test_tmpconfigdir_warning(tmp_path):
"""Test that a warning is emitted if a temporary configdir must be used."""
mode = os.stat(tmp_path).st_mode
try:
os.chmod(tmp_path, 0)
proc = subprocess_run_for_testing(
[sys.executable, "-c", "import matplotlib"],
env={**os.environ, "MPLCONFIGDIR": str(tmp_path)},
stderr=subprocess.PIPE, text=True, check=True)
assert "set the MPLCONFIGDIR" in proc.stderr
finally:
os.chmod(tmp_path, mode)
def test_importable_with_no_home(tmp_path):
subprocess_run_for_testing(
[sys.executable, "-c",
"import pathlib; pathlib.Path.home = lambda *args: 1/0; "
"import matplotlib.pyplot"],
env={**os.environ, "MPLCONFIGDIR": str(tmp_path)}, check=True)
def test_use_doc_standard_backends():
"""
Test that the standard backends mentioned in the docstring of
matplotlib.use() are the same as in matplotlib.rcsetup.
"""
def parse(key):
backends = []
for line in matplotlib.use.__doc__.split(key)[1].split('\n'):
if not line.strip():
break
backends += [e.strip().lower() for e in line.split(',') if e]
return backends
from matplotlib.backends import BackendFilter, backend_registry
assert (set(parse('- interactive backends:\n')) ==
set(backend_registry.list_builtin(BackendFilter.INTERACTIVE)))
assert (set(parse('- non-interactive backends:\n')) ==
set(backend_registry.list_builtin(BackendFilter.NON_INTERACTIVE)))
def test_importable_with__OO():
"""
When using -OO or export PYTHONOPTIMIZE=2, docstrings are discarded,
this simple test may prevent something like issue #17970.
"""
program = (
"import matplotlib as mpl; "
"import matplotlib.pyplot as plt; "
"import matplotlib.cbook as cbook; "
"import matplotlib.patches as mpatches"
)
subprocess_run_for_testing(
[sys.executable, "-OO", "-c", program],
env={**os.environ, "MPLBACKEND": ""}, check=True
)
@patch('matplotlib.subprocess.check_output')
def test_get_executable_info_timeout(mock_check_output):
"""
Test that _get_executable_info raises ExecutableNotFoundError if the
command times out.
"""
mock_check_output.side_effect = subprocess.TimeoutExpired(cmd=['mock'], timeout=30)
with pytest.raises(matplotlib.ExecutableNotFoundError, match='Timed out'):
matplotlib._get_executable_info.__wrapped__('inkscape')
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,564 @@
import numpy as np
from numpy.testing import assert_array_equal, assert_allclose
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import (image_comparison,
remove_ticks_and_titles)
import matplotlib as mpl
import pytest
from pathlib import Path
from io import BytesIO
from PIL import Image
import base64
@image_comparison(["bivariate_cmap_shapes.png"])
def test_bivariate_cmap_shapes():
x_0 = np.repeat(np.linspace(-0.1, 1.1, 10, dtype='float32')[None, :], 10, axis=0)
x_1 = x_0.T
fig, axes = plt.subplots(1, 4, figsize=(10, 2))
# shape = 'square'
cmap = mpl.bivar_colormaps['BiPeak']
axes[0].imshow(cmap((x_0, x_1)), interpolation='nearest')
# shape = 'circle'
cmap = mpl.bivar_colormaps['BiCone']
axes[1].imshow(cmap((x_0, x_1)), interpolation='nearest')
# shape = 'ignore'
cmap = mpl.bivar_colormaps['BiPeak']
cmap = cmap.with_extremes(shape='ignore')
axes[2].imshow(cmap((x_0, x_1)), interpolation='nearest')
# shape = circleignore
cmap = mpl.bivar_colormaps['BiCone']
cmap = cmap.with_extremes(shape='circleignore')
axes[3].imshow(cmap((x_0, x_1)), interpolation='nearest')
remove_ticks_and_titles(fig)
def test_multivar_creation():
# test creation of a custom multivariate colorbar
blues = mpl.colormaps['Blues']
cmap = mpl.colors.MultivarColormap((blues, 'Oranges'), 'sRGB_sub')
y, x = np.mgrid[0:3, 0:3]/2
im = cmap((y, x))
res = np.array([[[0.96862745, 0.94509804, 0.92156863, 1],
[0.96004614, 0.53504037, 0.23277201, 1],
[0.46666667, 0.1372549, 0.01568627, 1]],
[[0.41708574, 0.64141484, 0.75980008, 1],
[0.40850442, 0.23135717, 0.07100346, 1],
[0, 0, 0, 1]],
[[0.03137255, 0.14901961, 0.34117647, 1],
[0.02279123, 0, 0, 1],
[0, 0, 0, 1]]])
assert_allclose(im, res, atol=0.01)
with pytest.raises(ValueError, match="colormaps must be a list of"):
cmap = mpl.colors.MultivarColormap((blues, [blues]), 'sRGB_sub')
with pytest.raises(ValueError, match="A MultivarColormap must"):
cmap = mpl.colors.MultivarColormap('blues', 'sRGB_sub')
with pytest.raises(ValueError, match="A MultivarColormap must"):
cmap = mpl.colors.MultivarColormap((blues), 'sRGB_sub')
@image_comparison(["multivar_alpha_mixing.png"])
def test_multivar_alpha_mixing():
# test creation of a custom colormap using 'rainbow'
# and a colormap that goes from alpha = 1 to alpha = 0
rainbow = mpl.colormaps['rainbow']
alpha = np.zeros((256, 4))
alpha[:, 3] = np.linspace(1, 0, 256)
alpha_cmap = mpl.colors.LinearSegmentedColormap.from_list('from_list', alpha)
cmap = mpl.colors.MultivarColormap((rainbow, alpha_cmap), 'sRGB_add')
y, x = np.mgrid[0:10, 0:10]/9
im = cmap((y, x))
fig, ax = plt.subplots()
ax.imshow(im, interpolation='nearest')
remove_ticks_and_titles(fig)
def test_multivar_cmap_call():
cmap = mpl.multivar_colormaps['2VarAddA']
assert_array_equal(cmap((0.0, 0.0)), (0, 0, 0, 1))
assert_array_equal(cmap((1.0, 1.0)), (1, 1, 1, 1))
assert_allclose(cmap((0.0, 0.0), alpha=0.1), (0, 0, 0, 0.1), atol=0.1)
cmap = mpl.multivar_colormaps['2VarSubA']
assert_array_equal(cmap((0.0, 0.0)), (1, 1, 1, 1))
assert_allclose(cmap((1.0, 1.0)), (0, 0, 0, 1), atol=0.1)
# check outside and bad
cs = cmap([(0., 0., 0., 1.2, np.nan), (0., 1.2, np.nan, 0., 0., )])
assert_allclose(cs, [[1., 1., 1., 1.],
[0.801, 0.426, 0.119, 1.],
[0., 0., 0., 0.],
[0.199, 0.574, 0.881, 1.],
[0., 0., 0., 0.]])
assert_array_equal(cmap((0.0, 0.0), bytes=True), (255, 255, 255, 255))
with pytest.raises(ValueError, match="alpha is array-like but its shape"):
cs = cmap([(0, 5, 9), (0, 0, 0)], alpha=(0.5, 0.3))
with pytest.raises(ValueError, match="For the selected colormap the data"):
cs = cmap([(0, 5, 9), (0, 0, 0), (0, 0, 0)])
with pytest.raises(ValueError, match="clip cannot be false"):
cs = cmap([(0, 5, 9), (0, 0, 0)], bytes=True, clip=False)
# Tests calling a multivariate colormap with integer values
cmap = mpl.multivar_colormaps['2VarSubA']
# call only integers
cs = cmap([(0, 50, 100, 0, 0, 300), (0, 0, 0, 50, 100, 300)])
res = np.array([[1, 1, 1, 1],
[0.85176471, 0.91029412, 0.96023529, 1],
[0.70452941, 0.82764706, 0.93358824, 1],
[0.94358824, 0.88505882, 0.83511765, 1],
[0.89729412, 0.77417647, 0.66823529, 1],
[0, 0, 0, 1]])
assert_allclose(cs, res, atol=0.01)
# call only integers, wrong byte order
swapped_dt = np.dtype(int).newbyteorder()
cs = cmap([np.array([0, 50, 100, 0, 0, 300], dtype=swapped_dt),
np.array([0, 0, 0, 50, 100, 300], dtype=swapped_dt)])
assert_allclose(cs, res, atol=0.01)
# call mix floats integers
# check calling with bytes = True
cs = cmap([(0, 50, 100, 0, 0, 300), (0, 0, 0, 50, 100, 300)], bytes=True)
res = np.array([[255, 255, 255, 255],
[217, 232, 244, 255],
[179, 211, 238, 255],
[240, 225, 212, 255],
[228, 197, 170, 255],
[0, 0, 0, 255]])
assert_allclose(cs, res, atol=0.01)
cs = cmap([(0, 50, 100, 0, 0, 300), (0, 0, 0, 50, 100, 300)], alpha=0.5)
res = np.array([[1, 1, 1, 0.5],
[0.85176471, 0.91029412, 0.96023529, 0.5],
[0.70452941, 0.82764706, 0.93358824, 0.5],
[0.94358824, 0.88505882, 0.83511765, 0.5],
[0.89729412, 0.77417647, 0.66823529, 0.5],
[0, 0, 0, 0.5]])
assert_allclose(cs, res, atol=0.01)
# call with tuple
assert_allclose(cmap((100, 120), bytes=True, alpha=0.5),
[149, 142, 136, 127], atol=0.01)
# alpha and bytes
cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)], bytes=True, alpha=0.5)
res = np.array([[0, 0, 255, 127],
[141, 0, 255, 127],
[255, 0, 255, 127],
[0, 115, 255, 127],
[0, 255, 255, 127],
[255, 255, 255, 127]])
# bad alpha shape
with pytest.raises(ValueError, match="alpha is array-like but its shape"):
cs = cmap([(0, 5, 9), (0, 0, 0)], bytes=True, alpha=(0.5, 0.3))
cmap = cmap.with_extremes(bad=(1, 1, 1, 1))
cs = cmap([(0., 1.1, np.nan), (0., 1.2, 1.)])
res = np.array([[1., 1., 1., 1.],
[0., 0., 0., 1.],
[1., 1., 1., 1.]])
assert_allclose(cs, res, atol=0.01)
# call outside with tuple
assert_allclose(cmap((300, 300), bytes=True, alpha=0.5),
[0, 0, 0, 127], atol=0.01)
with pytest.raises(ValueError,
match="For the selected colormap the data must have"):
cs = cmap((0, 5, 9))
# test over/under
cmap = mpl.multivar_colormaps['2VarAddA']
with pytest.raises(ValueError, match='i.e. be of length 2'):
cmap.with_extremes(over=0)
with pytest.raises(ValueError, match='i.e. be of length 2'):
cmap.with_extremes(under=0)
cmap = cmap.with_extremes(under=[(0, 0, 0, 0)]*2)
assert_allclose((0, 0, 0, 0), cmap((-1., 0)), atol=1e-2)
cmap = cmap.with_extremes(over=[(0, 0, 0, 0)]*2)
assert_allclose((0, 0, 0, 0), cmap((2., 0)), atol=1e-2)
def test_multivar_bad_mode():
cmap = mpl.multivar_colormaps['2VarSubA']
with pytest.raises(ValueError, match="is not a valid value for"):
cmap = mpl.colors.MultivarColormap(cmap[:], 'bad')
def test_multivar_resample():
cmap = mpl.multivar_colormaps['3VarAddA']
cmap_resampled = cmap.resampled((None, 10, 3))
assert_allclose(cmap_resampled[1](0.25), (0.093, 0.116, 0.059, 1.0))
assert_allclose(cmap_resampled((0, 0.25, 0)), (0.093, 0.116, 0.059, 1.0))
assert_allclose(cmap_resampled((1, 0.25, 1)), (0.417271, 0.264624, 0.274976, 1.),
atol=0.01)
with pytest.raises(ValueError, match="lutshape must be of length"):
cmap = cmap.resampled(4)
def test_bivar_cmap_call_tuple():
cmap = mpl.bivar_colormaps['BiOrangeBlue']
assert_allclose(cmap((1.0, 1.0)), (1, 1, 1, 1), atol=0.01)
assert_allclose(cmap((0.0, 0.0)), (0, 0, 0, 1), atol=0.1)
assert_allclose(cmap((0.0, 0.0), alpha=0.1), (0, 0, 0, 0.1), atol=0.1)
def test_bivar_cmap_call():
"""
Tests calling a bivariate colormap with integer values
"""
im = np.ones((10, 12, 4))
im[:, :, 0] = np.linspace(0, 1, 10)[:, np.newaxis]
im[:, :, 1] = np.linspace(0, 1, 12)[np.newaxis, :]
cmap = mpl.colors.BivarColormapFromImage(im)
# call only integers
cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)])
res = np.array([[0, 0, 1, 1],
[0.556, 0, 1, 1],
[1, 0, 1, 1],
[0, 0.454, 1, 1],
[0, 1, 1, 1],
[1, 1, 1, 1]])
assert_allclose(cs, res, atol=0.01)
# call only integers, wrong byte order
swapped_dt = np.dtype(int).newbyteorder()
cs = cmap([np.array([0, 5, 9, 0, 0, 10], dtype=swapped_dt),
np.array([0, 0, 0, 5, 11, 12], dtype=swapped_dt)])
assert_allclose(cs, res, atol=0.01)
# call mix floats integers
cmap = cmap.with_extremes(outside=(1, 0, 0, 0))
cs = cmap([(0.5, 0), (0, 3)])
res = np.array([[0.555, 0, 1, 1],
[0, 0.2727, 1, 1]])
assert_allclose(cs, res, atol=0.01)
# check calling with bytes = True
cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)], bytes=True)
res = np.array([[0, 0, 255, 255],
[141, 0, 255, 255],
[255, 0, 255, 255],
[0, 115, 255, 255],
[0, 255, 255, 255],
[255, 255, 255, 255]])
assert_allclose(cs, res, atol=0.01)
# test alpha
cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)], alpha=0.5)
res = np.array([[0, 0, 1, 0.5],
[0.556, 0, 1, 0.5],
[1, 0, 1, 0.5],
[0, 0.454, 1, 0.5],
[0, 1, 1, 0.5],
[1, 1, 1, 0.5]])
assert_allclose(cs, res, atol=0.01)
# call with tuple
assert_allclose(cmap((10, 12), bytes=True, alpha=0.5),
[255, 255, 255, 127], atol=0.01)
# alpha and bytes
cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)], bytes=True, alpha=0.5)
res = np.array([[0, 0, 255, 127],
[141, 0, 255, 127],
[255, 0, 255, 127],
[0, 115, 255, 127],
[0, 255, 255, 127],
[255, 255, 255, 127]])
# bad alpha shape
with pytest.raises(ValueError, match="alpha is array-like but its shape"):
cs = cmap([(0, 5, 9), (0, 0, 0)], bytes=True, alpha=(0.5, 0.3))
# set shape to 'ignore'.
# final point is outside colormap and should then receive
# the 'outside' (in this case [1,0,0,0])
# also test 'bad' (in this case [1,1,1,0])
cmap = cmap.with_extremes(outside=(1, 0, 0, 0), bad=(1, 1, 1, 0), shape='ignore')
cs = cmap([(0., 1.1, np.nan), (0., 1.2, 1.)])
res = np.array([[0, 0, 1, 1],
[1, 0, 0, 0],
[1, 1, 1, 0]])
assert_allclose(cs, res, atol=0.01)
# call outside with tuple
assert_allclose(cmap((10, 12), bytes=True, alpha=0.5),
[255, 0, 0, 127], atol=0.01)
# with integers
cs = cmap([(0, 10), (0, 12)])
res = np.array([[0, 0, 1, 1],
[1, 0, 0, 0]])
assert_allclose(cs, res, atol=0.01)
with pytest.raises(ValueError,
match="For a `BivarColormap` the data must have"):
cs = cmap((0, 5, 9))
cmap = cmap.with_extremes(shape='circle')
with pytest.raises(NotImplementedError,
match="only implemented for use with with floats"):
cs = cmap([(0, 5, 9, 0, 0, 9), (0, 0, 0, 5, 11, 11)])
# test origin
cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(0.5, 0.5))
assert_allclose(cmap[0](0.5),
(0.50244140625, 0.5024222412109375, 0.50244140625, 1))
assert_allclose(cmap[1](0.5),
(0.50244140625, 0.5024222412109375, 0.50244140625, 1))
cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(1, 1))
assert_allclose(cmap[0](1.),
(0.99853515625, 0.9985467529296875, 0.99853515625, 1.0))
assert_allclose(cmap[1](1.),
(0.99853515625, 0.9985467529296875, 0.99853515625, 1.0))
with pytest.raises(KeyError,
match="only 0 or 1 are valid keys"):
cs = cmap[2]
def test_bivar_getitem():
"""Test __getitem__ on BivarColormap"""
xA = ([.0, .25, .5, .75, 1., -1, 2], [.5]*7)
xB = ([.5]*7, [.0, .25, .5, .75, 1., -1, 2])
cmaps = mpl.bivar_colormaps['BiPeak']
assert_array_equal(cmaps(xA), cmaps[0](xA[0]))
assert_array_equal(cmaps(xB), cmaps[1](xB[1]))
cmaps = cmaps.with_extremes(shape='ignore')
assert_array_equal(cmaps(xA), cmaps[0](xA[0]))
assert_array_equal(cmaps(xB), cmaps[1](xB[1]))
xA = ([.0, .25, .5, .75, 1., -1, 2], [.0]*7)
xB = ([.0]*7, [.0, .25, .5, .75, 1., -1, 2])
cmaps = mpl.bivar_colormaps['BiOrangeBlue']
assert_array_equal(cmaps(xA), cmaps[0](xA[0]))
assert_array_equal(cmaps(xB), cmaps[1](xB[1]))
cmaps = cmaps.with_extremes(shape='ignore')
assert_array_equal(cmaps(xA), cmaps[0](xA[0]))
assert_array_equal(cmaps(xB), cmaps[1](xB[1]))
def test_bivar_cmap_bad_shape():
"""
Tests calling a bivariate colormap with integer values
"""
cmap = mpl.bivar_colormaps['BiCone']
_ = cmap.lut
with pytest.raises(ValueError,
match="is not a valid value for shape"):
cmap.with_extremes(shape='bad_shape')
with pytest.raises(ValueError,
match="is not a valid value for shape"):
mpl.colors.BivarColormapFromImage(np.ones((3, 3, 4)),
shape='bad_shape')
def test_bivar_cmap_bad_lut():
"""
Tests calling a bivariate colormap with integer values
"""
with pytest.raises(ValueError,
match="The lut must be an array of shape"):
cmap = mpl.colors.BivarColormapFromImage(np.ones((3, 3, 5)))
def test_bivar_cmap_from_image():
"""
This tests the creation and use of a bivariate colormap
generated from an image
"""
data_0 = np.arange(6).reshape((2, 3))/5
data_1 = np.arange(6).reshape((3, 2)).T/5
# bivariate colormap from array
cim = np.ones((10, 12, 3))
cim[:, :, 0] = np.arange(10)[:, np.newaxis]/10
cim[:, :, 1] = np.arange(12)[np.newaxis, :]/12
cmap = mpl.colors.BivarColormapFromImage(cim)
im = cmap((data_0, data_1))
res = np.array([[[0, 0, 1, 1],
[0.2, 0.33333333, 1, 1],
[0.4, 0.75, 1, 1]],
[[0.6, 0.16666667, 1, 1],
[0.8, 0.58333333, 1, 1],
[0.9, 0.91666667, 1, 1]]])
assert_allclose(im, res, atol=0.01)
# input as unit8
cim = np.ones((10, 12, 3))*255
cim[:, :, 0] = np.arange(10)[:, np.newaxis]/10*255
cim[:, :, 1] = np.arange(12)[np.newaxis, :]/12*255
cmap = mpl.colors.BivarColormapFromImage(cim.astype(np.uint8))
im = cmap((data_0, data_1))
res = np.array([[[0, 0, 1, 1],
[0.2, 0.33333333, 1, 1],
[0.4, 0.75, 1, 1]],
[[0.6, 0.16666667, 1, 1],
[0.8, 0.58333333, 1, 1],
[0.9, 0.91666667, 1, 1]]])
assert_allclose(im, res, atol=0.01)
# bivariate colormap from array
png_path = Path(__file__).parent / "baseline_images/pngsuite/basn2c16.png"
cim = Image.open(png_path)
cim = np.asarray(cim.convert('RGBA'))
cmap = mpl.colors.BivarColormapFromImage(cim)
im = cmap((data_0, data_1), bytes=True)
res = np.array([[[255, 255, 0, 255],
[156, 206, 0, 255],
[49, 156, 49, 255]],
[[206, 99, 0, 255],
[99, 49, 107, 255],
[0, 0, 255, 255]]])
assert_allclose(im, res, atol=0.01)
def test_bivar_resample():
cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((2, 2))
assert_allclose(cmap((0.25, 0.25)), (0, 0, 0, 1), atol=1e-2)
cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, 2))
assert_allclose(cmap((0.25, 0.25)), (1., 0.5, 0., 1.), atol=1e-2)
cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((2, -2))
assert_allclose(cmap((0.25, 0.25)), (0., 0.5, 1., 1.), atol=1e-2)
cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, -2))
assert_allclose(cmap((0.25, 0.25)), (1, 1, 1, 1), atol=1e-2)
cmap = mpl.bivar_colormaps['BiOrangeBlue'].reversed()
assert_allclose(cmap((0.25, 0.25)), (0.748535, 0.748547, 0.748535, 1.), atol=1e-2)
cmap = mpl.bivar_colormaps['BiOrangeBlue'].transposed()
assert_allclose(cmap((0.25, 0.25)), (0.252441, 0.252422, 0.252441, 1.), atol=1e-2)
with pytest.raises(ValueError, match="lutshape must be of length"):
cmap = cmap.resampled(4)
def test_bivariate_repr_png():
cmap = mpl.bivar_colormaps['BiCone']
png = cmap._repr_png_()
assert len(png) > 0
img = Image.open(BytesIO(png))
assert img.width > 0
assert img.height > 0
assert 'Title' in img.text
assert 'Description' in img.text
assert 'Author' in img.text
assert 'Software' in img.text
def test_bivariate_repr_html():
cmap = mpl.bivar_colormaps['BiCone']
html = cmap._repr_html_()
assert len(html) > 0
png = cmap._repr_png_()
assert base64.b64encode(png).decode('ascii') in html
assert cmap.name in html
assert html.startswith('<div')
assert html.endswith('</div>')
def test_multivariate_repr_png():
cmap = mpl.multivar_colormaps['3VarAddA']
png = cmap._repr_png_()
assert len(png) > 0
img = Image.open(BytesIO(png))
assert img.width > 0
assert img.height > 0
assert 'Title' in img.text
assert 'Description' in img.text
assert 'Author' in img.text
assert 'Software' in img.text
def test_multivariate_repr_html():
cmap = mpl.multivar_colormaps['3VarAddA']
html = cmap._repr_html_()
assert len(html) > 0
for c in cmap:
png = c._repr_png_()
assert base64.b64encode(png).decode('ascii') in html
assert cmap.name in html
assert html.startswith('<div')
assert html.endswith('</div>')
def test_bivar_eq():
"""
Tests equality between multivariate colormaps
"""
cmap_0 = mpl.bivar_colormaps['BiPeak']
cmap_1 = mpl.bivar_colormaps['BiPeak']
assert (cmap_0 == cmap_1) is True
cmap_1 = mpl.multivar_colormaps['2VarAddA']
assert (cmap_0 == cmap_1) is False
cmap_1 = mpl.bivar_colormaps['BiCone']
assert (cmap_0 == cmap_1) is False
cmap_1 = mpl.bivar_colormaps['BiPeak']
cmap_1 = cmap_1.with_extremes(bad='k')
assert (cmap_0 == cmap_1) is False
cmap_1 = mpl.bivar_colormaps['BiPeak']
cmap_1 = cmap_1.with_extremes(outside='k')
assert (cmap_0 == cmap_1) is False
cmap_1 = mpl.bivar_colormaps['BiPeak']
cmap_1._init()
cmap_1._lut *= 0.5
assert (cmap_0 == cmap_1) is False
cmap_1 = mpl.bivar_colormaps['BiPeak']
cmap_1 = cmap_1.with_extremes(shape='ignore')
assert (cmap_0 == cmap_1) is False
def test_multivar_eq():
"""
Tests equality between multivariate colormaps
"""
cmap_0 = mpl.multivar_colormaps['2VarAddA']
cmap_1 = mpl.multivar_colormaps['2VarAddA']
assert (cmap_0 == cmap_1) is True
cmap_1 = mpl.bivar_colormaps['BiPeak']
assert (cmap_0 == cmap_1) is False
cmap_1 = mpl.colors.MultivarColormap([cmap_0[0]]*2,
'sRGB_add')
assert (cmap_0 == cmap_1) is False
cmap_1 = mpl.multivar_colormaps['3VarAddA']
assert (cmap_0 == cmap_1) is False
cmap_1 = mpl.multivar_colormaps['2VarAddA']
cmap_1 = cmap_1.with_extremes(bad='k')
assert (cmap_0 == cmap_1) is False
cmap_1 = mpl.multivar_colormaps['2VarAddA']
cmap_1 = mpl.colors.MultivarColormap(cmap_1[:], 'sRGB_sub')
assert (cmap_0 == cmap_1) is False
@@ -0,0 +1,472 @@
from collections import namedtuple
import io
import numpy as np
from numpy.testing import assert_allclose
import pytest
from matplotlib.testing.decorators import check_figures_equal, image_comparison
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.lines as mlines
from matplotlib.backend_bases import MouseButton, MouseEvent
from matplotlib.offsetbox import (
AnchoredOffsetbox, AnnotationBbox, AnchoredText, DrawingArea, HPacker,
OffsetBox, OffsetImage, PaddedBox, TextArea, VPacker, _get_packed_offsets)
@image_comparison(['offsetbox_clipping'], remove_text=True)
def test_offsetbox_clipping():
# - create a plot
# - put an AnchoredOffsetbox with a child DrawingArea
# at the center of the axes
# - give the DrawingArea a gray background
# - put a black line across the bounds of the DrawingArea
# - see that the black line is clipped to the edges of
# the DrawingArea.
fig, ax = plt.subplots()
size = 100
da = DrawingArea(size, size, clip=True)
assert da.clip_children
bg = mpatches.Rectangle((0, 0), size, size,
facecolor='#CCCCCC',
edgecolor='None',
linewidth=0)
line = mlines.Line2D([-size*.5, size*1.5], [size/2, size/2],
color='black',
linewidth=10)
anchored_box = AnchoredOffsetbox(
loc='center',
child=da,
pad=0.,
frameon=False,
bbox_to_anchor=(.5, .5),
bbox_transform=ax.transAxes,
borderpad=0.)
da.add_artist(bg)
da.add_artist(line)
ax.add_artist(anchored_box)
ax.set_xlim((0, 1))
ax.set_ylim((0, 1))
def test_offsetbox_clip_children():
# - create a plot
# - put an AnchoredOffsetbox with a child DrawingArea
# at the center of the axes
# - give the DrawingArea a gray background
# - put a black line across the bounds of the DrawingArea
# - see that the black line is clipped to the edges of
# the DrawingArea.
fig, ax = plt.subplots()
size = 100
da = DrawingArea(size, size, clip=True)
bg = mpatches.Rectangle((0, 0), size, size,
facecolor='#CCCCCC',
edgecolor='None',
linewidth=0)
line = mlines.Line2D([-size*.5, size*1.5], [size/2, size/2],
color='black',
linewidth=10)
anchored_box = AnchoredOffsetbox(
loc='center',
child=da,
pad=0.,
frameon=False,
bbox_to_anchor=(.5, .5),
bbox_transform=ax.transAxes,
borderpad=0.)
da.add_artist(bg)
da.add_artist(line)
ax.add_artist(anchored_box)
fig.canvas.draw()
assert not fig.stale
da.clip_children = True
assert fig.stale
def test_offsetbox_loc_codes():
# Check that valid string location codes all work with an AnchoredOffsetbox
codes = {'upper right': 1,
'upper left': 2,
'lower left': 3,
'lower right': 4,
'right': 5,
'center left': 6,
'center right': 7,
'lower center': 8,
'upper center': 9,
'center': 10,
}
fig, ax = plt.subplots()
da = DrawingArea(100, 100)
for code in codes:
anchored_box = AnchoredOffsetbox(loc=code, child=da)
ax.add_artist(anchored_box)
fig.canvas.draw()
def test_expand_with_tight_layout():
# Check issue reported in #10476, and updated due to #10784
fig, ax = plt.subplots()
d1 = [1, 2]
d2 = [2, 1]
ax.plot(d1, label='series 1')
ax.plot(d2, label='series 2')
ax.legend(ncols=2, mode='expand')
fig.tight_layout() # where the crash used to happen
@pytest.mark.parametrize('widths',
([150], [150, 150, 150], [0.1], [0.1, 0.1]))
@pytest.mark.parametrize('total', (250, 100, 0, -1, None))
@pytest.mark.parametrize('sep', (250, 1, 0, -1))
@pytest.mark.parametrize('mode', ("expand", "fixed", "equal"))
def test_get_packed_offsets(widths, total, sep, mode):
# Check a (rather arbitrary) set of parameters due to successive similar
# issue tickets (at least #10476 and #10784) related to corner cases
# triggered inside this function when calling higher-level functions
# (e.g. `Axes.legend`).
# These are just some additional smoke tests. The output is untested.
_get_packed_offsets(widths, total, sep, mode=mode)
_Params = namedtuple('_Params', 'wd_list, total, sep, expected')
@pytest.mark.parametrize('widths, total, sep, expected', [
_Params( # total=None
[3, 1, 2], total=None, sep=1, expected=(8, [0, 4, 6])),
_Params( # total larger than required
[3, 1, 2], total=10, sep=1, expected=(10, [0, 4, 6])),
_Params( # total smaller than required
[3, 1, 2], total=5, sep=1, expected=(5, [0, 4, 6])),
])
def test_get_packed_offsets_fixed(widths, total, sep, expected):
result = _get_packed_offsets(widths, total, sep, mode='fixed')
assert result[0] == expected[0]
assert_allclose(result[1], expected[1])
@pytest.mark.parametrize('widths, total, sep, expected', [
_Params( # total=None (implicit 1)
[.1, .1, .1], total=None, sep=None, expected=(1, [0, .45, .9])),
_Params( # total larger than sum of widths
[3, 1, 2], total=10, sep=1, expected=(10, [0, 5, 8])),
_Params( # total smaller sum of widths: overlapping boxes
[3, 1, 2], total=5, sep=1, expected=(5, [0, 2.5, 3])),
])
def test_get_packed_offsets_expand(widths, total, sep, expected):
result = _get_packed_offsets(widths, total, sep, mode='expand')
assert result[0] == expected[0]
assert_allclose(result[1], expected[1])
@pytest.mark.parametrize('widths, total, sep, expected', [
_Params( # total larger than required
[3, 2, 1], total=6, sep=None, expected=(6, [0, 2, 4])),
_Params( # total smaller sum of widths: overlapping boxes
[3, 2, 1, .5], total=2, sep=None, expected=(2, [0, 0.5, 1, 1.5])),
_Params( # total larger than required
[.5, 1, .2], total=None, sep=1, expected=(6, [0, 2, 4])),
# the case total=None, sep=None is tested separately below
])
def test_get_packed_offsets_equal(widths, total, sep, expected):
result = _get_packed_offsets(widths, total, sep, mode='equal')
assert result[0] == expected[0]
assert_allclose(result[1], expected[1])
def test_get_packed_offsets_equal_total_none_sep_none():
with pytest.raises(ValueError):
_get_packed_offsets([1, 1, 1], total=None, sep=None, mode='equal')
@pytest.mark.parametrize('child_type', ['draw', 'image', 'text'])
@pytest.mark.parametrize('boxcoords',
['axes fraction', 'axes pixels', 'axes points',
'data'])
def test_picking(child_type, boxcoords):
# These all take up approximately the same area.
if child_type == 'draw':
picking_child = DrawingArea(5, 5)
picking_child.add_artist(mpatches.Rectangle((0, 0), 5, 5, linewidth=0))
elif child_type == 'image':
im = np.ones((5, 5))
im[2, 2] = 0
picking_child = OffsetImage(im)
elif child_type == 'text':
picking_child = TextArea('\N{Black Square}', textprops={'fontsize': 5})
else:
assert False, f'Unknown picking child type {child_type}'
fig, ax = plt.subplots()
ab = AnnotationBbox(picking_child, (0.5, 0.5), boxcoords=boxcoords)
ab.set_picker(True)
ax.add_artist(ab)
calls = []
fig.canvas.mpl_connect('pick_event', lambda event: calls.append(event))
# Annotation should be picked by an event occurring at its center.
if boxcoords == 'axes points':
x, y = ax.transAxes.transform_point((0, 0))
x += 0.5 * fig.dpi / 72
y += 0.5 * fig.dpi / 72
elif boxcoords == 'axes pixels':
x, y = ax.transAxes.transform_point((0, 0))
x += 0.5
y += 0.5
else:
x, y = ax.transAxes.transform_point((0.5, 0.5))
fig.canvas.draw()
calls.clear()
MouseEvent(
"button_press_event", fig.canvas, x, y, MouseButton.LEFT)._process()
assert len(calls) == 1 and calls[0].artist == ab
# Annotation should *not* be picked by an event at its original center
# point when the limits have changed enough to hide the *xy* point.
ax.set_xlim(-1, 0)
ax.set_ylim(-1, 0)
fig.canvas.draw()
calls.clear()
MouseEvent(
"button_press_event", fig.canvas, x, y, MouseButton.LEFT)._process()
assert len(calls) == 0
@image_comparison(['anchoredtext_align.png'], remove_text=True, style='mpl20')
def test_anchoredtext_horizontal_alignment():
fig, ax = plt.subplots()
text0 = AnchoredText("test\ntest long text", loc="center left",
pad=0.2, prop={"ha": "left"})
ax.add_artist(text0)
text1 = AnchoredText("test\ntest long text", loc="center",
pad=0.2, prop={"ha": "center"})
ax.add_artist(text1)
text2 = AnchoredText("test\ntest long text", loc="center right",
pad=0.2, prop={"ha": "right"})
ax.add_artist(text2)
@pytest.mark.parametrize("extent_kind", ["window_extent", "tightbbox"])
def test_annotationbbox_extents(extent_kind):
plt.rcParams.update(plt.rcParamsDefault)
fig, ax = plt.subplots(figsize=(4, 3), dpi=100)
ax.axis([0, 1, 0, 1])
an1 = ax.annotate("Annotation", xy=(.9, .9), xytext=(1.1, 1.1),
arrowprops=dict(arrowstyle="->"), clip_on=False,
va="baseline", ha="left")
da = DrawingArea(20, 20, 0, 0, clip=True)
p = mpatches.Circle((-10, 30), 32)
da.add_artist(p)
ab3 = AnnotationBbox(da, [.5, .5], xybox=(-0.2, 0.5), xycoords='data',
boxcoords="axes fraction", box_alignment=(0., .5),
arrowprops=dict(arrowstyle="->"))
ax.add_artist(ab3)
im = OffsetImage(np.random.rand(10, 10), zoom=3)
im.image.axes = ax
ab6 = AnnotationBbox(im, (0.5, -.3), xybox=(0, 75),
xycoords='axes fraction',
boxcoords="offset points", pad=0.3,
arrowprops=dict(arrowstyle="->"))
ax.add_artist(ab6)
# Test Annotation
bb1 = getattr(an1, f"get_{extent_kind}")()
target1 = [332.9, 242.8, 467.0, 298.9]
assert_allclose(bb1.extents, target1, atol=2)
# Test AnnotationBbox
bb3 = getattr(ab3, f"get_{extent_kind}")()
target3 = [-17.6, 129.0, 200.7, 167.9]
assert_allclose(bb3.extents, target3, atol=2)
bb6 = getattr(ab6, f"get_{extent_kind}")()
target6 = [180.0, -32.0, 230.0, 92.9]
assert_allclose(bb6.extents, target6, atol=2)
# Test bbox_inches='tight'
buf = io.BytesIO()
fig.savefig(buf, bbox_inches='tight')
buf.seek(0)
shape = plt.imread(buf).shape
targetshape = (350, 504, 4)
assert_allclose(shape, targetshape, atol=2)
# Simple smoke test for tight_layout, to make sure it does not error out.
fig.canvas.draw()
fig.tight_layout()
fig.canvas.draw()
def test_zorder():
assert OffsetBox(zorder=42).zorder == 42
def test_arrowprops_copied():
da = DrawingArea(20, 20, 0, 0, clip=True)
arrowprops = {"arrowstyle": "->", "relpos": (.3, .7)}
ab = AnnotationBbox(da, [.5, .5], xybox=(-0.2, 0.5), xycoords='data',
boxcoords="axes fraction", box_alignment=(0., .5),
arrowprops=arrowprops)
assert ab.arrowprops is not ab
assert arrowprops["relpos"] == (.3, .7)
@pytest.mark.parametrize("align", ["baseline", "bottom", "top",
"left", "right", "center"])
def test_packers(align):
# set the DPI to match points to make the math easier below
fig = plt.figure(dpi=72)
renderer = fig.canvas.get_renderer()
x1, y1 = 10, 30
x2, y2 = 20, 60
r1 = DrawingArea(x1, y1)
r2 = DrawingArea(x2, y2)
# HPacker
hpacker = HPacker(children=[r1, r2], align=align)
hpacker.draw(renderer)
bbox = hpacker.get_bbox(renderer)
px, py = hpacker.get_offset(bbox, renderer)
# width, height, xdescent, ydescent
assert_allclose(bbox.bounds, (0, 0, x1 + x2, max(y1, y2)))
# internal element placement
if align in ("baseline", "left", "bottom"):
y_height = 0
elif align in ("right", "top"):
y_height = y2 - y1
elif align == "center":
y_height = (y2 - y1) / 2
# x-offsets, y-offsets
assert_allclose([child.get_offset() for child in hpacker.get_children()],
[(px, py + y_height), (px + x1, py)])
# VPacker
vpacker = VPacker(children=[r1, r2], align=align)
vpacker.draw(renderer)
bbox = vpacker.get_bbox(renderer)
px, py = vpacker.get_offset(bbox, renderer)
# width, height, xdescent, ydescent
assert_allclose(bbox.bounds, (0, -max(y1, y2), max(x1, x2), y1 + y2))
# internal element placement
if align in ("baseline", "left", "bottom"):
x_height = 0
elif align in ("right", "top"):
x_height = x2 - x1
elif align == "center":
x_height = (x2 - x1) / 2
# x-offsets, y-offsets
assert_allclose([child.get_offset() for child in vpacker.get_children()],
[(px + x_height, py), (px, py - y2)])
def test_paddedbox_default_values():
# smoke test paddedbox for correct default value
fig, ax = plt.subplots()
at = AnchoredText("foo", 'upper left')
pb = PaddedBox(at, patch_attrs={'facecolor': 'r'}, draw_frame=True)
ax.add_artist(pb)
fig.draw_without_rendering()
def test_annotationbbox_properties():
ab = AnnotationBbox(DrawingArea(20, 20, 0, 0, clip=True), (0.5, 0.5),
xycoords='data')
assert ab.xyann == (0.5, 0.5) # xy if xybox not given
assert ab.anncoords == 'data' # xycoords if boxcoords not given
ab = AnnotationBbox(DrawingArea(20, 20, 0, 0, clip=True), (0.5, 0.5),
xybox=(-0.2, 0.4), xycoords='data',
boxcoords='axes fraction')
assert ab.xyann == (-0.2, 0.4) # xybox if given
assert ab.anncoords == 'axes fraction' # boxcoords if given
def test_textarea_properties():
ta = TextArea('Foo')
assert ta.get_text() == 'Foo'
assert not ta.get_multilinebaseline()
ta.set_text('Bar')
ta.set_multilinebaseline(True)
assert ta.get_text() == 'Bar'
assert ta.get_multilinebaseline()
@check_figures_equal(extensions=['png'])
def test_textarea_set_text(fig_test, fig_ref):
ax_ref = fig_ref.add_subplot()
text0 = AnchoredText("Foo", "upper left")
ax_ref.add_artist(text0)
ax_test = fig_test.add_subplot()
text1 = AnchoredText("Bar", "upper left")
ax_test.add_artist(text1)
text1.txt.set_text("Foo")
@image_comparison(['paddedbox.png'], remove_text=True, style='mpl20')
def test_paddedbox():
fig, ax = plt.subplots()
ta = TextArea("foo")
pb = PaddedBox(ta, pad=5, patch_attrs={'facecolor': 'r'}, draw_frame=True)
ab = AnchoredOffsetbox('upper left', child=pb)
ax.add_artist(ab)
ta = TextArea("bar")
pb = PaddedBox(ta, pad=10, patch_attrs={'facecolor': 'b'})
ab = AnchoredOffsetbox('upper right', child=pb)
ax.add_artist(ab)
ta = TextArea("foobar")
pb = PaddedBox(ta, pad=15, draw_frame=True)
ab = AnchoredOffsetbox('lower right', child=pb)
ax.add_artist(ab)
def test_remove_draggable():
fig, ax = plt.subplots()
an = ax.annotate("foo", (.5, .5))
an.draggable(True)
an.remove()
MouseEvent("button_release_event", fig.canvas, 1, 1)._process()
def test_draggable_in_subfigure():
fig = plt.figure()
# Put annotation at lower left corner to make it easily pickable below.
ann = fig.subfigures().add_axes([0, 0, 1, 1]).annotate("foo", (0, 0))
ann.draggable(True)
fig.canvas.draw() # Texts are non-pickable until the first draw.
MouseEvent("button_press_event", fig.canvas, 1, 1)._process()
assert ann._draggable.got_artist
# Stop dragging the annotation.
MouseEvent("button_release_event", fig.canvas, 1, 1)._process()
assert not ann._draggable.got_artist
# A scroll event should not initiate a drag.
MouseEvent("scroll_event", fig.canvas, 1, 1)._process()
assert not ann._draggable.got_artist
# An event outside the annotation should not initiate a drag.
bbox = ann.get_window_extent()
MouseEvent("button_press_event", fig.canvas, bbox.x1+2, bbox.y1+2)._process()
assert not ann._draggable.got_artist
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,658 @@
import platform
import re
import numpy as np
from numpy.testing import assert_array_equal
import pytest
from matplotlib import patches
from matplotlib.path import Path
from matplotlib.patches import Polygon
from matplotlib.testing.decorators import image_comparison
import matplotlib.pyplot as plt
from matplotlib import transforms
from matplotlib.backend_bases import MouseEvent
def test_empty_closed_path():
path = Path(np.zeros((0, 2)), closed=True)
assert path.vertices.shape == (0, 2)
assert path.codes is None
assert_array_equal(path.get_extents().extents,
transforms.Bbox.null().extents)
def test_readonly_path():
path = Path.unit_circle()
def modify_vertices():
path.vertices = path.vertices * 2.0
with pytest.raises(AttributeError):
modify_vertices()
def test_path_exceptions():
bad_verts1 = np.arange(12).reshape(4, 3)
with pytest.raises(ValueError,
match=re.escape(f'has shape {bad_verts1.shape}')):
Path(bad_verts1)
bad_verts2 = np.arange(12).reshape(2, 3, 2)
with pytest.raises(ValueError,
match=re.escape(f'has shape {bad_verts2.shape}')):
Path(bad_verts2)
good_verts = np.arange(12).reshape(6, 2)
bad_codes = np.arange(2)
msg = re.escape(f"Your vertices have shape {good_verts.shape} "
f"but your codes have shape {bad_codes.shape}")
with pytest.raises(ValueError, match=msg):
Path(good_verts, bad_codes)
def test_point_in_path():
# Test #1787
path = Path._create_closed([(0, 0), (0, 1), (1, 1), (1, 0)])
points = [(0.5, 0.5), (1.5, 0.5)]
ret = path.contains_points(points)
assert ret.dtype == 'bool'
np.testing.assert_equal(ret, [True, False])
@pytest.mark.parametrize(
"other_path, inside, inverted_inside",
[(Path([(0.25, 0.25), (0.25, 0.75), (0.75, 0.75), (0.75, 0.25), (0.25, 0.25)],
closed=True), True, False),
(Path([(-0.25, -0.25), (-0.25, 1.75), (1.75, 1.75), (1.75, -0.25), (-0.25, -0.25)],
closed=True), False, True),
(Path([(-0.25, -0.25), (-0.25, 1.75), (0.5, 0.5),
(1.75, 1.75), (1.75, -0.25), (-0.25, -0.25)],
closed=True), False, False),
(Path([(0.25, 0.25), (0.25, 1.25), (1.25, 1.25), (1.25, 0.25), (0.25, 0.25)],
closed=True), False, False),
(Path([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)], closed=True), False, False),
(Path([(2, 2), (2, 3), (3, 3), (3, 2), (2, 2)], closed=True), False, False)])
def test_contains_path(other_path, inside, inverted_inside):
path = Path([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)], closed=True)
assert path.contains_path(other_path) is inside
assert other_path.contains_path(path) is inverted_inside
def test_contains_points_negative_radius():
path = Path.unit_circle()
points = [(0.0, 0.0), (1.25, 0.0), (0.9, 0.9)]
result = path.contains_points(points, radius=-0.5)
np.testing.assert_equal(result, [True, False, False])
_test_paths = [
# interior extrema determine extents and degenerate derivative
Path([[0, 0], [1, 0], [1, 1], [0, 1]],
[Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]),
# a quadratic curve
Path([[0, 0], [0, 1], [1, 0]], [Path.MOVETO, Path.CURVE3, Path.CURVE3]),
# a linear curve, degenerate vertically
Path([[0, 1], [1, 1]], [Path.MOVETO, Path.LINETO]),
# a point
Path([[1, 2]], [Path.MOVETO]),
]
_test_path_extents = [(0., 0., 0.75, 1.), (0., 0., 1., 0.5), (0., 1., 1., 1.),
(1., 2., 1., 2.)]
@pytest.mark.parametrize('path, extents', zip(_test_paths, _test_path_extents))
def test_exact_extents(path, extents):
# notice that if we just looked at the control points to get the bounding
# box of each curve, we would get the wrong answers. For example, for
# hard_curve = Path([[0, 0], [1, 0], [1, 1], [0, 1]],
# [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4])
# we would get that the extents area (0, 0, 1, 1). This code takes into
# account the curved part of the path, which does not typically extend all
# the way out to the control points.
# Note that counterintuitively, path.get_extents() returns a Bbox, so we
# have to get that Bbox's `.extents`.
assert np.all(path.get_extents().extents == extents)
@pytest.mark.parametrize('ignored_code', [Path.CLOSEPOLY, Path.STOP])
def test_extents_with_ignored_codes(ignored_code):
# Check that STOP and CLOSEPOLY points are ignored when calculating extents
# of a path with only straight lines
path = Path([[0, 0],
[1, 1],
[2, 2]], [Path.MOVETO, Path.MOVETO, ignored_code])
assert np.all(path.get_extents().extents == (0., 0., 1., 1.))
def test_point_in_path_nan():
box = np.array([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]])
p = Path(box)
test = np.array([[np.nan, 0.5]])
contains = p.contains_points(test)
assert len(contains) == 1
assert not contains[0]
def test_nonlinear_containment():
fig, ax = plt.subplots()
ax.set(xscale="log", ylim=(0, 1))
polygon = ax.axvspan(1, 10)
assert polygon.get_path().contains_point(
ax.transData.transform((5, .5)), polygon.get_transform())
assert not polygon.get_path().contains_point(
ax.transData.transform((.5, .5)), polygon.get_transform())
assert not polygon.get_path().contains_point(
ax.transData.transform((50, .5)), polygon.get_transform())
@image_comparison(['arrow_contains_point.png'], remove_text=True, style='mpl20',
tol=0 if platform.machine() == 'x86_64' else 0.027)
def test_arrow_contains_point():
# fix bug (#8384)
fig, ax = plt.subplots()
ax.set_xlim((0, 2))
ax.set_ylim((0, 2))
# create an arrow with Curve style
arrow = patches.FancyArrowPatch((0.5, 0.25), (1.5, 0.75),
arrowstyle='->',
mutation_scale=40)
ax.add_patch(arrow)
# create an arrow with Bracket style
arrow1 = patches.FancyArrowPatch((0.5, 1), (1.5, 1.25),
arrowstyle=']-[',
mutation_scale=40)
ax.add_patch(arrow1)
# create an arrow with other arrow style
arrow2 = patches.FancyArrowPatch((0.5, 1.5), (1.5, 1.75),
arrowstyle='fancy',
fill=False,
mutation_scale=40)
ax.add_patch(arrow2)
patches_list = [arrow, arrow1, arrow2]
# generate some points
X, Y = np.meshgrid(np.arange(0, 2, 0.1),
np.arange(0, 2, 0.1))
for k, (x, y) in enumerate(zip(X.ravel(), Y.ravel())):
xdisp, ydisp = ax.transData.transform([x, y])
event = MouseEvent('button_press_event', fig.canvas, xdisp, ydisp)
for m, patch in enumerate(patches_list):
# set the points to red only if the arrow contains the point
inside, res = patch.contains(event)
if inside:
ax.scatter(x, y, s=5, c="r")
@image_comparison(['path_clipping.svg'], remove_text=True)
def test_path_clipping():
fig = plt.figure(figsize=(6.0, 6.2))
for i, xy in enumerate([
[(200, 200), (200, 350), (400, 350), (400, 200)],
[(200, 200), (200, 350), (400, 350), (400, 100)],
[(200, 100), (200, 350), (400, 350), (400, 100)],
[(200, 100), (200, 415), (400, 350), (400, 100)],
[(200, 100), (200, 415), (400, 415), (400, 100)],
[(200, 415), (400, 415), (400, 100), (200, 100)],
[(400, 415), (400, 100), (200, 100), (200, 415)]]):
ax = fig.add_subplot(4, 2, i+1)
bbox = [0, 140, 640, 260]
ax.set_xlim(bbox[0], bbox[0] + bbox[2])
ax.set_ylim(bbox[1], bbox[1] + bbox[3])
ax.add_patch(Polygon(
xy, facecolor='none', edgecolor='red', closed=True))
@image_comparison(['semi_log_with_zero.png'], style='mpl20')
def test_log_transform_with_zero():
x = np.arange(-10, 10)
y = (1.0 - 1.0/(x**2+1))**20
fig, ax = plt.subplots()
ax.semilogy(x, y, "-o", lw=15, markeredgecolor='k')
ax.set_ylim(1e-7, 1)
ax.grid(True)
def test_make_compound_path_empty():
# We should be able to make a compound path with no arguments.
# This makes it easier to write generic path based code.
empty = Path.make_compound_path()
assert empty.vertices.shape == (0, 2)
r2 = Path.make_compound_path(empty, empty)
assert r2.vertices.shape == (0, 2)
assert r2.codes.shape == (0,)
r3 = Path.make_compound_path(Path([(0, 0)]), empty)
assert r3.vertices.shape == (1, 2)
assert r3.codes.shape == (1,)
def test_make_compound_path_stops():
zero = [0, 0]
paths = 3*[Path([zero, zero], [Path.MOVETO, Path.STOP])]
compound_path = Path.make_compound_path(*paths)
# the choice to not preserve the terminal STOP is arbitrary, but
# documented, so we test that it is in fact respected here
assert np.sum(compound_path.codes == Path.STOP) == 0
@image_comparison(['xkcd.png'], remove_text=True)
def test_xkcd():
np.random.seed(0)
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
with plt.xkcd():
fig, ax = plt.subplots()
ax.plot(x, y)
@image_comparison(['xkcd_marker.png'], remove_text=True)
def test_xkcd_marker():
np.random.seed(0)
x = np.linspace(0, 5, 8)
y1 = x
y2 = 5 - x
y3 = 2.5 * np.ones(8)
with plt.xkcd():
fig, ax = plt.subplots()
ax.plot(x, y1, '+', ms=10)
ax.plot(x, y2, 'o', ms=10)
ax.plot(x, y3, '^', ms=10)
@image_comparison(['marker_paths.pdf'], remove_text=True)
def test_marker_paths_pdf():
N = 7
plt.errorbar(np.arange(N),
np.ones(N) + 4,
np.ones(N))
plt.xlim(-1, N)
plt.ylim(-1, 7)
@image_comparison(['nan_path'], style='default', remove_text=True,
extensions=['pdf', 'svg', 'eps', 'png'],
tol=0 if platform.machine() == 'x86_64' else 0.009)
def test_nan_isolated_points():
y0 = [0, np.nan, 2, np.nan, 4, 5, 6]
y1 = [np.nan, 7, np.nan, 9, 10, np.nan, 12]
fig, ax = plt.subplots()
ax.plot(y0, '-o')
ax.plot(y1, '-o')
def test_path_no_doubled_point_in_to_polygon():
hand = np.array(
[[1.64516129, 1.16145833],
[1.64516129, 1.59375],
[1.35080645, 1.921875],
[1.375, 2.18229167],
[1.68548387, 1.9375],
[1.60887097, 2.55208333],
[1.68548387, 2.69791667],
[1.76209677, 2.56770833],
[1.83064516, 1.97395833],
[1.89516129, 2.75],
[1.9516129, 2.84895833],
[2.01209677, 2.76041667],
[1.99193548, 1.99479167],
[2.11290323, 2.63020833],
[2.2016129, 2.734375],
[2.25403226, 2.60416667],
[2.14919355, 1.953125],
[2.30645161, 2.36979167],
[2.39112903, 2.36979167],
[2.41532258, 2.1875],
[2.1733871, 1.703125],
[2.07782258, 1.16666667]])
(r0, c0, r1, c1) = (1.0, 1.5, 2.1, 2.5)
poly = Path(np.vstack((hand[:, 1], hand[:, 0])).T, closed=True)
clip_rect = transforms.Bbox([[r0, c0], [r1, c1]])
poly_clipped = poly.clip_to_bbox(clip_rect).to_polygons()[0]
assert np.all(poly_clipped[-2] != poly_clipped[-1])
assert np.all(poly_clipped[-1] == poly_clipped[0])
def test_path_to_polygons():
data = [[10, 10], [20, 20]]
p = Path(data)
assert_array_equal(p.to_polygons(width=40, height=40), [])
assert_array_equal(p.to_polygons(width=40, height=40, closed_only=False),
[data])
assert_array_equal(p.to_polygons(), [])
assert_array_equal(p.to_polygons(closed_only=False), [data])
data = [[10, 10], [20, 20], [30, 30]]
closed_data = [[10, 10], [20, 20], [30, 30], [10, 10]]
p = Path(data)
assert_array_equal(p.to_polygons(width=40, height=40), [closed_data])
assert_array_equal(p.to_polygons(width=40, height=40, closed_only=False),
[data])
assert_array_equal(p.to_polygons(), [closed_data])
assert_array_equal(p.to_polygons(closed_only=False), [data])
def test_path_deepcopy():
# Should not raise any error
verts = [[0, 0], [1, 1]]
codes = [Path.MOVETO, Path.LINETO]
path1 = Path(verts, readonly=True)
path2 = Path(verts, codes, readonly=True)
path1_copy = path1.deepcopy()
path2_copy = path2.deepcopy()
assert path1 is not path1_copy
assert path1.vertices is not path1_copy.vertices
assert_array_equal(path1.vertices, path1_copy.vertices)
assert path1.readonly
assert not path1_copy.readonly
assert path2 is not path2_copy
assert path2.vertices is not path2_copy.vertices
assert_array_equal(path2.vertices, path2_copy.vertices)
assert path2.codes is not path2_copy.codes
assert_array_equal(path2.codes, path2_copy.codes)
assert path2.readonly
assert not path2_copy.readonly
def test_path_deepcopy_cycle():
class PathWithCycle(Path):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.x = self
p = PathWithCycle([[0, 0], [1, 1]], readonly=True)
p_copy = p.deepcopy()
assert p_copy is not p
assert p.readonly
assert not p_copy.readonly
assert p_copy.x is p_copy
class PathWithCycle2(Path):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.x = [self] * 2
p2 = PathWithCycle2([[0, 0], [1, 1]], readonly=True)
p2_copy = p2.deepcopy()
assert p2_copy is not p2
assert p2.readonly
assert not p2_copy.readonly
assert p2_copy.x[0] is p2_copy
assert p2_copy.x[1] is p2_copy
def test_path_shallowcopy():
# Should not raise any error
verts = [[0, 0], [1, 1]]
codes = [Path.MOVETO, Path.LINETO]
path1 = Path(verts)
path2 = Path(verts, codes)
path1_copy = path1.copy()
path2_copy = path2.copy()
assert path1 is not path1_copy
assert path1.vertices is path1_copy.vertices
assert path2 is not path2_copy
assert path2.vertices is path2_copy.vertices
assert path2.codes is path2_copy.codes
@pytest.mark.parametrize('phi', np.concatenate([
np.array([0, 15, 30, 45, 60, 75, 90, 105, 120, 135]) + delta
for delta in [-1, 0, 1]]))
def test_path_intersect_path(phi):
# test for the range of intersection angles
eps_array = [1e-5, 1e-8, 1e-10, 1e-12]
transform = transforms.Affine2D().rotate(np.deg2rad(phi))
# a and b intersect at angle phi
a = Path([(-2, 0), (2, 0)])
b = transform.transform_path(a)
assert a.intersects_path(b) and b.intersects_path(a)
# a and b touch at angle phi at (0, 0)
a = Path([(0, 0), (2, 0)])
b = transform.transform_path(a)
assert a.intersects_path(b) and b.intersects_path(a)
# a and b are orthogonal and intersect at (0, 3)
a = transform.transform_path(Path([(0, 1), (0, 3)]))
b = transform.transform_path(Path([(1, 3), (0, 3)]))
assert a.intersects_path(b) and b.intersects_path(a)
# a and b are collinear and intersect at (0, 3)
a = transform.transform_path(Path([(0, 1), (0, 3)]))
b = transform.transform_path(Path([(0, 5), (0, 3)]))
assert a.intersects_path(b) and b.intersects_path(a)
# self-intersect
assert a.intersects_path(a)
# a contains b
a = transform.transform_path(Path([(0, 0), (5, 5)]))
b = transform.transform_path(Path([(1, 1), (3, 3)]))
assert a.intersects_path(b) and b.intersects_path(a)
# a and b are collinear but do not intersect
a = transform.transform_path(Path([(0, 1), (0, 5)]))
b = transform.transform_path(Path([(3, 0), (3, 3)]))
assert not a.intersects_path(b) and not b.intersects_path(a)
# a and b are on the same line but do not intersect
a = transform.transform_path(Path([(0, 1), (0, 5)]))
b = transform.transform_path(Path([(0, 6), (0, 7)]))
assert not a.intersects_path(b) and not b.intersects_path(a)
# Note: 1e-13 is the absolute tolerance error used for
# `isclose` function from src/_path.h
# a and b are parallel but do not touch
for eps in eps_array:
a = transform.transform_path(Path([(0, 1), (0, 5)]))
b = transform.transform_path(Path([(0 + eps, 1), (0 + eps, 5)]))
assert not a.intersects_path(b) and not b.intersects_path(a)
# a and b are on the same line but do not intersect (really close)
for eps in eps_array:
a = transform.transform_path(Path([(0, 1), (0, 5)]))
b = transform.transform_path(Path([(0, 5 + eps), (0, 7)]))
assert not a.intersects_path(b) and not b.intersects_path(a)
# a and b are on the same line and intersect (really close)
for eps in eps_array:
a = transform.transform_path(Path([(0, 1), (0, 5)]))
b = transform.transform_path(Path([(0, 5 - eps), (0, 7)]))
assert a.intersects_path(b) and b.intersects_path(a)
# b is the same as a but with an extra point
a = transform.transform_path(Path([(0, 1), (0, 5)]))
b = transform.transform_path(Path([(0, 1), (0, 2), (0, 5)]))
assert a.intersects_path(b) and b.intersects_path(a)
# a and b are collinear but do not intersect
a = transform.transform_path(Path([(1, -1), (0, -1)]))
b = transform.transform_path(Path([(0, 1), (0.9, 1)]))
assert not a.intersects_path(b) and not b.intersects_path(a)
# a and b are collinear but do not intersect
a = transform.transform_path(Path([(0., -5.), (1., -5.)]))
b = transform.transform_path(Path([(1., 5.), (0., 5.)]))
assert not a.intersects_path(b) and not b.intersects_path(a)
@pytest.mark.parametrize('offset', range(-720, 361, 45))
def test_full_arc(offset):
low = offset
high = 360 + offset
path = Path.arc(low, high)
mins = np.min(path.vertices, axis=0)
maxs = np.max(path.vertices, axis=0)
np.testing.assert_allclose(mins, -1)
np.testing.assert_allclose(maxs, 1)
def test_disjoint_zero_length_segment():
this_path = Path(
np.array([
[824.85064295, 2056.26489203],
[861.69033931, 2041.00539016],
[868.57864109, 2057.63522175],
[831.73894473, 2072.89472361],
[824.85064295, 2056.26489203]]),
np.array([1, 2, 2, 2, 79], dtype=Path.code_type))
outline_path = Path(
np.array([
[859.91051028, 2165.38461538],
[859.06772495, 2149.30331334],
[859.06772495, 2181.46591743],
[859.91051028, 2165.38461538],
[859.91051028, 2165.38461538]]),
np.array([1, 2, 2, 2, 2],
dtype=Path.code_type))
assert not outline_path.intersects_path(this_path)
assert not this_path.intersects_path(outline_path)
def test_intersect_zero_length_segment():
this_path = Path(
np.array([
[0, 0],
[1, 1],
]))
outline_path = Path(
np.array([
[1, 0],
[.5, .5],
[.5, .5],
[0, 1],
]))
assert outline_path.intersects_path(this_path)
assert this_path.intersects_path(outline_path)
def test_cleanup_closepoly():
# if the first connected component of a Path ends in a CLOSEPOLY, but that
# component contains a NaN, then Path.cleaned should ignore not just the
# control points but also the CLOSEPOLY, since it has nowhere valid to
# point.
paths = [
Path([[np.nan, np.nan], [np.nan, np.nan]],
[Path.MOVETO, Path.CLOSEPOLY]),
# we trigger a different path in the C++ code if we don't pass any
# codes explicitly, so we must also make sure that this works
Path([[np.nan, np.nan], [np.nan, np.nan]]),
# we should also make sure that this cleanup works if there's some
# multi-vertex curves
Path([[np.nan, np.nan], [np.nan, np.nan], [np.nan, np.nan],
[np.nan, np.nan]],
[Path.MOVETO, Path.CURVE3, Path.CURVE3, Path.CLOSEPOLY])
]
for p in paths:
cleaned = p.cleaned(remove_nans=True)
assert len(cleaned) == 1
assert cleaned.codes[0] == Path.STOP
def test_interpolated_moveto():
# Initial path has two subpaths with two LINETOs each
vertices = np.array([[0, 0],
[0, 1],
[1, 2],
[4, 4],
[4, 5],
[5, 5]])
codes = [Path.MOVETO, Path.LINETO, Path.LINETO] * 2
path = Path(vertices, codes)
result = path.interpolated(3)
# Result should have two subpaths with six LINETOs each
expected_subpath_codes = [Path.MOVETO] + [Path.LINETO] * 6
np.testing.assert_array_equal(result.codes, expected_subpath_codes * 2)
def test_interpolated_closepoly():
codes = [Path.MOVETO] + [Path.LINETO]*2 + [Path.CLOSEPOLY]
vertices = [(4, 3), (5, 4), (5, 3), (0, 0)]
path = Path(vertices, codes)
result = path.interpolated(2)
expected_vertices = np.array([[4, 3],
[4.5, 3.5],
[5, 4],
[5, 3.5],
[5, 3],
[4.5, 3],
[4, 3]])
expected_codes = [Path.MOVETO] + [Path.LINETO]*5 + [Path.CLOSEPOLY]
np.testing.assert_allclose(result.vertices, expected_vertices)
np.testing.assert_array_equal(result.codes, expected_codes)
# Usually closepoly is the last vertex but does not have to be.
codes += [Path.LINETO]
vertices += [(2, 1)]
path = Path(vertices, codes)
result = path.interpolated(2)
extra_expected_vertices = np.array([[3, 2],
[2, 1]])
expected_vertices = np.concatenate([expected_vertices, extra_expected_vertices])
expected_codes += [Path.LINETO] * 2
np.testing.assert_allclose(result.vertices, expected_vertices)
np.testing.assert_array_equal(result.codes, expected_codes)
def test_interpolated_moveto_closepoly():
# Initial path has two closed subpaths
codes = ([Path.MOVETO] + [Path.LINETO]*2 + [Path.CLOSEPOLY]) * 2
vertices = [(4, 3), (5, 4), (5, 3), (0, 0), (8, 6), (10, 8), (10, 6), (0, 0)]
path = Path(vertices, codes)
result = path.interpolated(2)
expected_vertices1 = np.array([[4, 3],
[4.5, 3.5],
[5, 4],
[5, 3.5],
[5, 3],
[4.5, 3],
[4, 3]])
expected_vertices = np.concatenate([expected_vertices1, expected_vertices1 * 2])
expected_codes = ([Path.MOVETO] + [Path.LINETO]*5 + [Path.CLOSEPOLY]) * 2
np.testing.assert_allclose(result.vertices, expected_vertices)
np.testing.assert_array_equal(result.codes, expected_codes)
def test_interpolated_empty_path():
path = Path(np.zeros((0, 2)))
assert path.interpolated(42) is path
@@ -0,0 +1,217 @@
import platform
import numpy as np
from matplotlib.testing.decorators import image_comparison
import matplotlib.pyplot as plt
import matplotlib.patheffects as path_effects
from matplotlib.path import Path
import matplotlib.patches as patches
from matplotlib.backend_bases import RendererBase
from matplotlib.patheffects import PathEffectRenderer
@image_comparison(['patheffect1'], remove_text=True)
def test_patheffect1():
ax1 = plt.subplot()
ax1.imshow([[1, 2], [2, 3]])
txt = ax1.annotate("test", (1., 1.), (0., 0),
arrowprops=dict(arrowstyle="->",
connectionstyle="angle3", lw=2),
size=20, ha="center",
path_effects=[path_effects.withStroke(linewidth=3,
foreground="w")])
txt.arrow_patch.set_path_effects([path_effects.Stroke(linewidth=5,
foreground="w"),
path_effects.Normal()])
pe = [path_effects.withStroke(linewidth=3, foreground="w")]
ax1.grid(True, linestyle="-", path_effects=pe)
@image_comparison(['patheffect2'], remove_text=True, style='mpl20',
tol=0 if platform.machine() == 'x86_64' else 0.06)
def test_patheffect2():
ax2 = plt.subplot()
arr = np.arange(25).reshape((5, 5))
ax2.imshow(arr, interpolation='nearest')
cntr = ax2.contour(arr, colors="k")
cntr.set(path_effects=[path_effects.withStroke(linewidth=3, foreground="w")])
clbls = ax2.clabel(cntr, fmt="%2.0f", use_clabeltext=True)
plt.setp(clbls,
path_effects=[path_effects.withStroke(linewidth=3,
foreground="w")])
@image_comparison(['patheffect3'],
tol=0 if platform.machine() == 'x86_64' else 0.019)
def test_patheffect3():
p1, = plt.plot([1, 3, 5, 4, 3], 'o-b', lw=4)
p1.set_path_effects([path_effects.SimpleLineShadow(),
path_effects.Normal()])
plt.title(
r'testing$^{123}$',
path_effects=[path_effects.withStroke(linewidth=1, foreground="r")])
leg = plt.legend([p1], [r'Line 1$^2$'], fancybox=True, loc='upper left')
leg.legendPatch.set_path_effects([path_effects.withSimplePatchShadow()])
text = plt.text(2, 3, 'Drop test', color='white',
bbox={'boxstyle': 'circle,pad=0.1', 'color': 'red'})
pe = [path_effects.Stroke(linewidth=3.75, foreground='k'),
path_effects.withSimplePatchShadow((6, -3), shadow_rgbFace='blue')]
text.set_path_effects(pe)
text.get_bbox_patch().set_path_effects(pe)
pe = [path_effects.PathPatchEffect(offset=(4, -4), hatch='xxxx',
facecolor='gray'),
path_effects.PathPatchEffect(edgecolor='white', facecolor='black',
lw=1.1)]
t = plt.gcf().text(0.02, 0.1, 'Hatch shadow', fontsize=75, weight=1000,
va='center')
t.set_path_effects(pe)
@image_comparison(['stroked_text.png'])
def test_patheffects_stroked_text():
text_chunks = [
'A B C D E F G H I J K L',
'M N O P Q R S T U V W',
'X Y Z a b c d e f g h i j',
'k l m n o p q r s t u v',
'w x y z 0123456789',
r"!@#$%^&*()-=_+[]\;'",
',./{}|:"<>?'
]
font_size = 50
ax = plt.axes((0, 0, 1, 1))
for i, chunk in enumerate(text_chunks):
text = ax.text(x=0.01, y=(0.9 - i * 0.13), s=chunk,
fontdict={'ha': 'left', 'va': 'center',
'size': font_size, 'color': 'white'})
text.set_path_effects([path_effects.Stroke(linewidth=font_size / 10,
foreground='black'),
path_effects.Normal()])
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis('off')
def test_PathEffect_points_to_pixels():
fig = plt.figure(dpi=150)
p1, = plt.plot(range(10))
p1.set_path_effects([path_effects.SimpleLineShadow(),
path_effects.Normal()])
renderer = fig.canvas.get_renderer()
pe_renderer = path_effects.PathEffectRenderer(
p1.get_path_effects(), renderer)
# Confirm that using a path effects renderer maintains point sizes
# appropriately. Otherwise rendered font would be the wrong size.
assert renderer.points_to_pixels(15) == pe_renderer.points_to_pixels(15)
def test_SimplePatchShadow_offset():
pe = path_effects.SimplePatchShadow(offset=(4, 5))
assert pe._offset == (4, 5)
@image_comparison(['collection'], tol=0.03, style='mpl20')
def test_collection():
x, y = np.meshgrid(np.linspace(0, 10, 150), np.linspace(-5, 5, 100))
data = np.sin(x) + np.cos(y)
cs = plt.contour(data)
cs.set(path_effects=[
path_effects.PathPatchEffect(edgecolor='black', facecolor='none', linewidth=12),
path_effects.Stroke(linewidth=5)])
for text in plt.clabel(cs, colors='white'):
text.set_path_effects([path_effects.withStroke(foreground='k',
linewidth=3)])
text.set_bbox({'boxstyle': 'sawtooth', 'facecolor': 'none',
'edgecolor': 'blue'})
@image_comparison(['tickedstroke'], remove_text=True, extensions=['png'],
tol=0.22) # Increased tolerance due to fixed clipping.
def test_tickedstroke():
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(12, 4))
path = Path.unit_circle()
patch = patches.PathPatch(path, facecolor='none', lw=2, path_effects=[
path_effects.withTickedStroke(angle=-90, spacing=10,
length=1)])
ax1.add_patch(patch)
ax1.axis('equal')
ax1.set_xlim(-2, 2)
ax1.set_ylim(-2, 2)
ax2.plot([0, 1], [0, 1], label=' ',
path_effects=[path_effects.withTickedStroke(spacing=7,
angle=135)])
nx = 101
x = np.linspace(0.0, 1.0, nx)
y = 0.3 * np.sin(x * 8) + 0.4
ax2.plot(x, y, label=' ', path_effects=[path_effects.withTickedStroke()])
ax2.legend()
nx = 101
ny = 105
# Set up survey vectors
xvec = np.linspace(0.001, 4.0, nx)
yvec = np.linspace(0.001, 4.0, ny)
# Set up survey matrices. Design disk loading and gear ratio.
x1, x2 = np.meshgrid(xvec, yvec)
# Evaluate some stuff to plot
g1 = -(3 * x1 + x2 - 5.5)
g2 = -(x1 + 2 * x2 - 4)
g3 = .8 + x1 ** -3 - x2
cg1 = ax3.contour(x1, x2, g1, [0], colors=('k',))
cg1.set(path_effects=[path_effects.withTickedStroke(angle=135)])
cg2 = ax3.contour(x1, x2, g2, [0], colors=('r',))
cg2.set(path_effects=[path_effects.withTickedStroke(angle=60, length=2)])
cg3 = ax3.contour(x1, x2, g3, [0], colors=('b',))
cg3.set(path_effects=[path_effects.withTickedStroke(spacing=7)])
ax3.set_xlim(0, 4)
ax3.set_ylim(0, 4)
@image_comparison(['spaces_and_newlines.png'], remove_text=True)
def test_patheffects_spaces_and_newlines():
ax = plt.subplot()
s1 = " "
s2 = "\nNewline also causes problems"
text1 = ax.text(0.5, 0.75, s1, ha='center', va='center', size=20,
bbox={'color': 'salmon'})
text2 = ax.text(0.5, 0.25, s2, ha='center', va='center', size=20,
bbox={'color': 'thistle'})
text1.set_path_effects([path_effects.Normal()])
text2.set_path_effects([path_effects.Normal()])
def test_patheffects_overridden_methods_open_close_group():
class CustomRenderer(RendererBase):
def __init__(self):
super().__init__()
def open_group(self, s, gid=None):
return "open_group overridden"
def close_group(self, s):
return "close_group overridden"
renderer = PathEffectRenderer([path_effects.Normal()], CustomRenderer())
assert renderer.open_group('s') == "open_group overridden"
assert renderer.close_group('s') == "close_group overridden"
@@ -0,0 +1,339 @@
from io import BytesIO
import ast
import os
import sys
import pickle
import pickletools
import numpy as np
import pytest
import matplotlib as mpl
from matplotlib import cm
from matplotlib.testing import subprocess_run_helper, is_ci_environment
from matplotlib.testing.decorators import check_figures_equal
from matplotlib.dates import rrulewrapper
from matplotlib.lines import VertexSelector
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import matplotlib.figure as mfigure
from mpl_toolkits.axes_grid1 import axes_divider, parasite_axes # type: ignore[import]
def test_simple():
fig = plt.figure()
pickle.dump(fig, BytesIO(), pickle.HIGHEST_PROTOCOL)
ax = plt.subplot(121)
pickle.dump(ax, BytesIO(), pickle.HIGHEST_PROTOCOL)
ax = plt.axes(projection='polar')
plt.plot(np.arange(10), label='foobar')
plt.legend()
pickle.dump(ax, BytesIO(), pickle.HIGHEST_PROTOCOL)
# ax = plt.subplot(121, projection='hammer')
# pickle.dump(ax, BytesIO(), pickle.HIGHEST_PROTOCOL)
plt.figure()
plt.bar(x=np.arange(10), height=np.arange(10))
pickle.dump(plt.gca(), BytesIO(), pickle.HIGHEST_PROTOCOL)
fig = plt.figure()
ax = plt.axes()
plt.plot(np.arange(10))
ax.set_yscale('log')
pickle.dump(fig, BytesIO(), pickle.HIGHEST_PROTOCOL)
def _generate_complete_test_figure(fig_ref):
fig_ref.set_size_inches((10, 6))
plt.figure(fig_ref)
plt.suptitle('Can you fit any more in a figure?')
# make some arbitrary data
x, y = np.arange(8), np.arange(10)
data = u = v = np.linspace(0, 10, 80).reshape(10, 8)
v = np.sin(v * -0.6)
# Ensure lists also pickle correctly.
plt.subplot(3, 3, 1)
plt.plot(list(range(10)))
plt.ylabel("hello")
plt.subplot(3, 3, 2)
plt.contourf(data, hatches=['//', 'ooo'])
plt.colorbar()
plt.subplot(3, 3, 3)
plt.pcolormesh(data)
plt.subplot(3, 3, 4)
plt.imshow(data)
plt.ylabel("hello\nworld!")
plt.subplot(3, 3, 5)
plt.pcolor(data)
ax = plt.subplot(3, 3, 6)
ax.set_xlim(0, 7)
ax.set_ylim(0, 9)
plt.streamplot(x, y, u, v)
ax = plt.subplot(3, 3, 7)
ax.set_xlim(0, 7)
ax.set_ylim(0, 9)
plt.quiver(x, y, u, v)
plt.subplot(3, 3, 8)
plt.scatter(x, x ** 2, label='$x^2$')
plt.legend(loc='upper left')
plt.subplot(3, 3, 9)
plt.errorbar(x, x * -0.5, xerr=0.2, yerr=0.4, label='$-.5 x$')
plt.legend(draggable=True)
# Ensure subfigure parenting works.
subfigs = fig_ref.subfigures(2)
subfigs[0].subplots(1, 2)
subfigs[1].subplots(1, 2)
fig_ref.align_ylabels() # Test handling of _align_label_groups Groupers.
@mpl.style.context("default")
@check_figures_equal(extensions=["png"])
def test_complete(fig_test, fig_ref):
_generate_complete_test_figure(fig_ref)
# plotting is done, now test its pickle-ability
pkl = pickle.dumps(fig_ref, pickle.HIGHEST_PROTOCOL)
# FigureCanvasAgg is picklable and GUI canvases are generally not, but there should
# be no reference to the canvas in the pickle stream in either case. In order to
# keep the test independent of GUI toolkits, run it with Agg and check that there's
# no reference to FigureCanvasAgg in the pickle stream.
assert "FigureCanvasAgg" not in [arg for op, arg, pos in pickletools.genops(pkl)]
loaded = pickle.loads(pkl)
loaded.canvas.draw()
fig_test.set_size_inches(loaded.get_size_inches())
fig_test.figimage(loaded.canvas.renderer.buffer_rgba())
plt.close(loaded)
def _pickle_load_subprocess():
import os
import pickle
path = os.environ['PICKLE_FILE_PATH']
with open(path, 'rb') as blob:
fig = pickle.load(blob)
print(str(pickle.dumps(fig)))
@mpl.style.context("default")
@check_figures_equal(extensions=['png'])
def test_pickle_load_from_subprocess(fig_test, fig_ref, tmp_path):
_generate_complete_test_figure(fig_ref)
fp = tmp_path / 'sinus.pickle'
assert not fp.exists()
with fp.open('wb') as file:
pickle.dump(fig_ref, file, pickle.HIGHEST_PROTOCOL)
assert fp.exists()
proc = subprocess_run_helper(
_pickle_load_subprocess,
timeout=60,
extra_env={
"PICKLE_FILE_PATH": str(fp),
"MPLBACKEND": "Agg",
# subprocess_run_helper will set SOURCE_DATE_EPOCH=0, so for a dirty tree,
# the version will have the date 19700101. As we aren't trying to test the
# version compatibility warning, force setuptools-scm to use the same
# version as us.
"SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MATPLOTLIB": mpl.__version__,
},
)
loaded_fig = pickle.loads(ast.literal_eval(proc.stdout))
loaded_fig.canvas.draw()
fig_test.set_size_inches(loaded_fig.get_size_inches())
fig_test.figimage(loaded_fig.canvas.renderer.buffer_rgba())
plt.close(loaded_fig)
def test_gcf():
fig = plt.figure("a label")
buf = BytesIO()
pickle.dump(fig, buf, pickle.HIGHEST_PROTOCOL)
plt.close("all")
assert plt._pylab_helpers.Gcf.figs == {} # No figures must be left.
fig = pickle.loads(buf.getbuffer())
assert plt._pylab_helpers.Gcf.figs != {} # A manager is there again.
assert fig.get_label() == "a label"
def test_no_pyplot():
# tests pickle-ability of a figure not created with pyplot
from matplotlib.backends.backend_pdf import FigureCanvasPdf
fig = mfigure.Figure()
_ = FigureCanvasPdf(fig)
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3], [1, 2, 3])
pickle.dump(fig, BytesIO(), pickle.HIGHEST_PROTOCOL)
def test_renderer():
from matplotlib.backends.backend_agg import RendererAgg
renderer = RendererAgg(10, 20, 30)
pickle.dump(renderer, BytesIO())
def test_image():
# Prior to v1.4.0 the Image would cache data which was not picklable
# once it had been drawn.
from matplotlib.backends.backend_agg import new_figure_manager
manager = new_figure_manager(1000)
fig = manager.canvas.figure
ax = fig.add_subplot(1, 1, 1)
ax.imshow(np.arange(12).reshape(3, 4))
manager.canvas.draw()
pickle.dump(fig, BytesIO())
def test_polar():
plt.subplot(polar=True)
fig = plt.gcf()
pf = pickle.dumps(fig)
pickle.loads(pf)
plt.draw()
class TransformBlob:
def __init__(self):
self.identity = mtransforms.IdentityTransform()
self.identity2 = mtransforms.IdentityTransform()
# Force use of the more complex composition.
self.composite = mtransforms.CompositeGenericTransform(
self.identity,
self.identity2)
# Check parent -> child links of TransformWrapper.
self.wrapper = mtransforms.TransformWrapper(self.composite)
# Check child -> parent links of TransformWrapper.
self.composite2 = mtransforms.CompositeGenericTransform(
self.wrapper,
self.identity)
def test_transform():
obj = TransformBlob()
pf = pickle.dumps(obj)
del obj
obj = pickle.loads(pf)
# Check parent -> child links of TransformWrapper.
assert obj.wrapper._child == obj.composite
# Check child -> parent links of TransformWrapper.
assert [v() for v in obj.wrapper._parents.values()] == [obj.composite2]
# Check input and output dimensions are set as expected.
assert obj.wrapper.input_dims == obj.composite.input_dims
assert obj.wrapper.output_dims == obj.composite.output_dims
def test_rrulewrapper():
r = rrulewrapper(2)
try:
pickle.loads(pickle.dumps(r))
except RecursionError:
print('rrulewrapper pickling test failed')
raise
def test_shared():
fig, axs = plt.subplots(2, sharex=True)
fig = pickle.loads(pickle.dumps(fig))
fig.axes[0].set_xlim(10, 20)
assert fig.axes[1].get_xlim() == (10, 20)
def test_inset_and_secondary():
fig, ax = plt.subplots()
ax.inset_axes([.1, .1, .3, .3])
ax.secondary_xaxis("top", functions=(np.square, np.sqrt))
pickle.loads(pickle.dumps(fig))
@pytest.mark.parametrize("cmap", cm._colormaps.values())
def test_cmap(cmap):
pickle.dumps(cmap)
def test_unpickle_canvas():
fig = mfigure.Figure()
assert fig.canvas is not None
out = BytesIO()
pickle.dump(fig, out)
out.seek(0)
fig2 = pickle.load(out)
assert fig2.canvas is not None
def test_mpl_toolkits():
ax = parasite_axes.host_axes([0, 0, 1, 1])
axes_divider.make_axes_area_auto_adjustable(ax)
assert type(pickle.loads(pickle.dumps(ax))) == parasite_axes.HostAxes
def test_standard_norm():
assert type(pickle.loads(pickle.dumps(mpl.colors.LogNorm()))) \
== mpl.colors.LogNorm
def test_dynamic_norm():
logit_norm_instance = mpl.colors.make_norm_from_scale(
mpl.scale.LogitScale, mpl.colors.Normalize)()
assert type(pickle.loads(pickle.dumps(logit_norm_instance))) \
== type(logit_norm_instance)
def test_vertexselector():
line, = plt.plot([0, 1], picker=True)
pickle.loads(pickle.dumps(VertexSelector(line)))
def test_cycler():
ax = plt.figure().add_subplot()
ax.set_prop_cycle(c=["c", "m", "y", "k"])
ax.plot([1, 2])
ax = pickle.loads(pickle.dumps(ax))
l, = ax.plot([3, 4])
assert l.get_color() == "m"
# Run under an interactive backend to test that we don't try to pickle the
# (interactive and non-picklable) canvas.
def _test_axeswidget_interactive():
ax = plt.figure().add_subplot()
pickle.dumps(mpl.widgets.Button(ax, "button"))
@pytest.mark.xfail( # https://github.com/actions/setup-python/issues/649
('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
sys.platform == 'darwin' and sys.version_info[:2] < (3, 11),
reason='Tk version mismatch on Azure macOS CI'
)
def test_axeswidget_interactive():
subprocess_run_helper(
_test_axeswidget_interactive,
timeout=120 if is_ci_environment() else 20,
extra_env={'MPLBACKEND': 'tkagg'}
)
@@ -0,0 +1,53 @@
from io import BytesIO
from pathlib import Path
import pytest
from matplotlib.testing.decorators import image_comparison
from matplotlib import cm, pyplot as plt
@image_comparison(['pngsuite.png'], tol=0.04)
def test_pngsuite():
files = sorted(
(Path(__file__).parent / "baseline_images/pngsuite").glob("basn*.png"))
plt.figure(figsize=(len(files), 2))
for i, fname in enumerate(files):
data = plt.imread(fname)
cmap = None # use default colormap
if data.ndim == 2:
# keep grayscale images gray
cmap = cm.gray
# Using the old default data interpolation stage lets us
# continue to use the existing reference image
plt.imshow(data, extent=(i, i + 1, 0, 1), cmap=cmap,
interpolation_stage='data')
plt.gca().patch.set_facecolor("#ddffff")
plt.gca().set_xlim(0, len(files))
def test_truncated_file(tmp_path):
path = tmp_path / 'test.png'
path_t = tmp_path / 'test_truncated.png'
plt.savefig(path)
with open(path, 'rb') as fin:
buf = fin.read()
with open(path_t, 'wb') as fout:
fout.write(buf[:20])
with pytest.raises(Exception):
plt.imread(path_t)
def test_truncated_buffer():
b = BytesIO()
plt.savefig(b)
b.seek(0)
b2 = BytesIO(b.read(20))
b2.seek(0)
with pytest.raises(Exception):
plt.imread(b2)
@@ -0,0 +1,528 @@
import numpy as np
from numpy.testing import assert_allclose
import pytest
import matplotlib as mpl
from matplotlib import pyplot as plt
from matplotlib.testing.decorators import image_comparison, check_figures_equal
@image_comparison(['polar_axes.png'], style='default', tol=0.012)
def test_polar_annotations():
# You can specify the xypoint and the xytext in different positions and
# coordinate systems, and optionally turn on a connecting line and mark the
# point with a marker. Annotations work on polar axes too. In the example
# below, the xy point is in native coordinates (xycoords defaults to
# 'data'). For a polar axes, this is in (theta, radius) space. The text
# in this example is placed in the fractional figure coordinate system.
# Text keyword args like horizontal and vertical alignment are respected.
# Setup some data
r = np.arange(0.0, 1.0, 0.001)
theta = 2.0 * 2.0 * np.pi * r
fig = plt.figure()
ax = fig.add_subplot(polar=True)
line, = ax.plot(theta, r, color='#ee8d18', lw=3)
line, = ax.plot((0, 0), (0, 1), color="#0000ff", lw=1)
ind = 800
thisr, thistheta = r[ind], theta[ind]
ax.plot([thistheta], [thisr], 'o')
ax.annotate('a polar annotation',
xy=(thistheta, thisr), # theta, radius
xytext=(0.05, 0.05), # fraction, fraction
textcoords='figure fraction',
arrowprops=dict(facecolor='black', shrink=0.05),
horizontalalignment='left',
verticalalignment='baseline',
)
ax.tick_params(axis='x', tick1On=True, tick2On=True, direction='out')
@image_comparison(['polar_coords.png'], style='default', remove_text=True,
tol=0.014)
def test_polar_coord_annotations():
# You can also use polar notation on a cartesian axes. Here the native
# coordinate system ('data') is cartesian, so you need to specify the
# xycoords and textcoords as 'polar' if you want to use (theta, radius).
el = mpl.patches.Ellipse((0, 0), 10, 20, facecolor='r', alpha=0.5)
fig = plt.figure()
ax = fig.add_subplot(aspect='equal')
ax.add_artist(el)
el.set_clip_box(ax.bbox)
ax.annotate('the top',
xy=(np.pi/2., 10.), # theta, radius
xytext=(np.pi/3, 20.), # theta, radius
xycoords='polar',
textcoords='polar',
arrowprops=dict(facecolor='black', shrink=0.05),
horizontalalignment='left',
verticalalignment='baseline',
clip_on=True, # clip to the axes bounding box
)
ax.set_xlim(-20, 20)
ax.set_ylim(-20, 20)
@image_comparison(['polar_alignment.png'])
def test_polar_alignment():
# Test changing the vertical/horizontal alignment of a polar graph.
angles = np.arange(0, 360, 90)
grid_values = [0, 0.2, 0.4, 0.6, 0.8, 1]
fig = plt.figure()
rect = [0.1, 0.1, 0.8, 0.8]
horizontal = fig.add_axes(rect, polar=True, label='horizontal')
horizontal.set_thetagrids(angles)
vertical = fig.add_axes(rect, polar=True, label='vertical')
vertical.patch.set_visible(False)
for i in range(2):
fig.axes[i].set_rgrids(
grid_values, angle=angles[i],
horizontalalignment='left', verticalalignment='top')
def test_polar_twice():
fig = plt.figure()
plt.polar([1, 2], [.1, .2])
plt.polar([3, 4], [.3, .4])
assert len(fig.axes) == 1, 'More than one polar Axes created.'
@check_figures_equal(extensions=['png'])
def test_polar_wrap(fig_test, fig_ref):
ax = fig_test.add_subplot(projection="polar")
ax.plot(np.deg2rad([179, -179]), [0.2, 0.1])
ax.plot(np.deg2rad([2, -2]), [0.2, 0.1])
ax = fig_ref.add_subplot(projection="polar")
ax.plot(np.deg2rad([179, 181]), [0.2, 0.1])
ax.plot(np.deg2rad([2, 358]), [0.2, 0.1])
@check_figures_equal(extensions=['png'])
def test_polar_units_1(fig_test, fig_ref):
import matplotlib.testing.jpl_units as units
units.register()
xs = [30.0, 45.0, 60.0, 90.0]
ys = [1.0, 2.0, 3.0, 4.0]
plt.figure(fig_test.number)
plt.polar([x * units.deg for x in xs], ys)
ax = fig_ref.add_subplot(projection="polar")
ax.plot(np.deg2rad(xs), ys)
ax.set(xlabel="deg")
@check_figures_equal(extensions=['png'])
def test_polar_units_2(fig_test, fig_ref):
import matplotlib.testing.jpl_units as units
units.register()
xs = [30.0, 45.0, 60.0, 90.0]
xs_deg = [x * units.deg for x in xs]
ys = [1.0, 2.0, 3.0, 4.0]
ys_km = [y * units.km for y in ys]
plt.figure(fig_test.number)
# test {theta,r}units.
plt.polar(xs_deg, ys_km, thetaunits="rad", runits="km")
assert isinstance(plt.gca().xaxis.get_major_formatter(),
units.UnitDblFormatter)
ax = fig_ref.add_subplot(projection="polar")
ax.plot(np.deg2rad(xs), ys)
ax.xaxis.set_major_formatter(mpl.ticker.FuncFormatter("{:.12}".format))
ax.set(xlabel="rad", ylabel="km")
@image_comparison(['polar_rmin.png'], style='default')
def test_polar_rmin():
r = np.arange(0, 3.0, 0.01)
theta = 2*np.pi*r
fig = plt.figure()
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
ax.plot(theta, r)
ax.set_rmax(2.0)
ax.set_rmin(0.5)
@image_comparison(['polar_negative_rmin.png'], style='default')
def test_polar_negative_rmin():
r = np.arange(-3.0, 0.0, 0.01)
theta = 2*np.pi*r
fig = plt.figure()
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
ax.plot(theta, r)
ax.set_rmax(0.0)
ax.set_rmin(-3.0)
@image_comparison(['polar_rorigin.png'], style='default')
def test_polar_rorigin():
r = np.arange(0, 3.0, 0.01)
theta = 2*np.pi*r
fig = plt.figure()
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
ax.plot(theta, r)
ax.set_rmax(2.0)
ax.set_rmin(0.5)
ax.set_rorigin(0.0)
@image_comparison(['polar_invertedylim.png'], style='default')
def test_polar_invertedylim():
fig = plt.figure()
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
ax.set_ylim(2, 0)
@image_comparison(['polar_invertedylim_rorigin.png'], style='default')
def test_polar_invertedylim_rorigin():
fig = plt.figure()
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
ax.yaxis.set_inverted(True)
# Set the rlims to inverted (2, 0) without calling set_rlim, to check that
# viewlims are correctly unstaled before draw()ing.
ax.plot([0, 0], [0, 2], c="none")
ax.margins(0)
ax.set_rorigin(3)
@image_comparison(['polar_theta_position.png'], style='default')
def test_polar_theta_position():
r = np.arange(0, 3.0, 0.01)
theta = 2*np.pi*r
fig = plt.figure()
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
ax.plot(theta, r)
ax.set_theta_zero_location("NW", 30)
ax.set_theta_direction('clockwise')
@image_comparison(['polar_rlabel_position.png'], style='default')
def test_polar_rlabel_position():
fig = plt.figure()
ax = fig.add_subplot(projection='polar')
ax.set_rlabel_position(315)
ax.tick_params(rotation='auto')
@image_comparison(['polar_title_position.png'], style='mpl20')
def test_polar_title_position():
fig = plt.figure()
ax = fig.add_subplot(projection='polar')
ax.set_title('foo')
@image_comparison(['polar_theta_wedge.png'], style='default')
def test_polar_theta_limits():
r = np.arange(0, 3.0, 0.01)
theta = 2*np.pi*r
theta_mins = np.arange(15.0, 361.0, 90.0)
theta_maxs = np.arange(50.0, 361.0, 90.0)
DIRECTIONS = ('out', 'in', 'inout')
fig, axs = plt.subplots(len(theta_mins), len(theta_maxs),
subplot_kw={'polar': True},
figsize=(8, 6))
for i, start in enumerate(theta_mins):
for j, end in enumerate(theta_maxs):
ax = axs[i, j]
ax.plot(theta, r)
if start < end:
ax.set_thetamin(start)
ax.set_thetamax(end)
else:
# Plot with clockwise orientation instead.
ax.set_thetamin(end)
ax.set_thetamax(start)
ax.set_theta_direction('clockwise')
ax.tick_params(tick1On=True, tick2On=True,
direction=DIRECTIONS[i % len(DIRECTIONS)],
rotation='auto')
ax.yaxis.set_tick_params(label2On=True, rotation='auto')
ax.xaxis.get_major_locator().base.set_params( # backcompat
steps=[1, 2, 2.5, 5, 10])
@check_figures_equal(extensions=["png"])
def test_polar_rlim(fig_test, fig_ref):
ax = fig_test.subplots(subplot_kw={'polar': True})
ax.set_rlim(top=10)
ax.set_rlim(bottom=.5)
ax = fig_ref.subplots(subplot_kw={'polar': True})
ax.set_rmax(10.)
ax.set_rmin(.5)
@check_figures_equal(extensions=["png"])
def test_polar_rlim_bottom(fig_test, fig_ref):
ax = fig_test.subplots(subplot_kw={'polar': True})
ax.set_rlim(bottom=[.5, 10])
ax = fig_ref.subplots(subplot_kw={'polar': True})
ax.set_rmax(10.)
ax.set_rmin(.5)
def test_polar_rlim_zero():
ax = plt.figure().add_subplot(projection='polar')
ax.plot(np.arange(10), np.arange(10) + .01)
assert ax.get_ylim()[0] == 0
def test_polar_no_data():
plt.subplot(projection="polar")
ax = plt.gca()
assert ax.get_rmin() == 0 and ax.get_rmax() == 1
plt.close("all")
# Used to behave differently (by triggering an autoscale with no data).
plt.polar()
ax = plt.gca()
assert ax.get_rmin() == 0 and ax.get_rmax() == 1
def test_polar_default_log_lims():
plt.subplot(projection='polar')
ax = plt.gca()
ax.set_rscale('log')
assert ax.get_rmin() > 0
def test_polar_not_datalim_adjustable():
ax = plt.figure().add_subplot(projection="polar")
with pytest.raises(ValueError):
ax.set_adjustable("datalim")
def test_polar_gridlines():
fig = plt.figure()
ax = fig.add_subplot(polar=True)
# make all major grid lines lighter, only x grid lines set in 2.1.0
ax.grid(alpha=0.2)
# hide y tick labels, no effect in 2.1.0
plt.setp(ax.yaxis.get_ticklabels(), visible=False)
fig.canvas.draw()
assert ax.xaxis.majorTicks[0].gridline.get_alpha() == .2
assert ax.yaxis.majorTicks[0].gridline.get_alpha() == .2
def test_get_tightbbox_polar():
fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
fig.canvas.draw()
bb = ax.get_tightbbox(fig.canvas.get_renderer())
assert_allclose(
bb.extents, [107.7778, 29.2778, 539.7847, 450.7222], rtol=1e-03)
@check_figures_equal(extensions=["png"])
def test_polar_interpolation_steps_constant_r(fig_test, fig_ref):
# Check that an extra half-turn doesn't make any difference -- modulo
# antialiasing, which we disable here.
p1 = (fig_test.add_subplot(121, projection="polar")
.bar([0], [1], 3*np.pi, edgecolor="none", antialiased=False))
p2 = (fig_test.add_subplot(122, projection="polar")
.bar([0], [1], -3*np.pi, edgecolor="none", antialiased=False))
p3 = (fig_ref.add_subplot(121, projection="polar")
.bar([0], [1], 2*np.pi, edgecolor="none", antialiased=False))
p4 = (fig_ref.add_subplot(122, projection="polar")
.bar([0], [1], -2*np.pi, edgecolor="none", antialiased=False))
@check_figures_equal(extensions=["png"])
def test_polar_interpolation_steps_variable_r(fig_test, fig_ref):
l, = fig_test.add_subplot(projection="polar").plot([0, np.pi/2], [1, 2])
l.get_path()._interpolation_steps = 100
fig_ref.add_subplot(projection="polar").plot(
np.linspace(0, np.pi/2, 101), np.linspace(1, 2, 101))
def test_thetalim_valid_invalid():
ax = plt.subplot(projection='polar')
ax.set_thetalim(0, 2 * np.pi) # doesn't raise.
ax.set_thetalim(thetamin=800, thetamax=440) # doesn't raise.
with pytest.raises(ValueError,
match='angle range must be less than a full circle'):
ax.set_thetalim(0, 3 * np.pi)
with pytest.raises(ValueError,
match='angle range must be less than a full circle'):
ax.set_thetalim(thetamin=800, thetamax=400)
def test_thetalim_args():
ax = plt.subplot(projection='polar')
ax.set_thetalim(0, 1)
assert tuple(np.radians((ax.get_thetamin(), ax.get_thetamax()))) == (0, 1)
ax.set_thetalim((2, 3))
assert tuple(np.radians((ax.get_thetamin(), ax.get_thetamax()))) == (2, 3)
def test_default_thetalocator():
# Ideally we would check AAAABBC, but the smallest axes currently puts a
# single tick at 150° because MaxNLocator doesn't have a way to accept 15°
# while rejecting 150°.
fig, axs = plt.subplot_mosaic(
"AAAABB.", subplot_kw={"projection": "polar"})
for ax in axs.values():
ax.set_thetalim(0, np.pi)
for ax in axs.values():
ticklocs = np.degrees(ax.xaxis.get_majorticklocs()).tolist()
assert pytest.approx(90) in ticklocs
assert pytest.approx(100) not in ticklocs
def test_axvspan():
ax = plt.subplot(projection="polar")
span = ax.axvspan(0, np.pi/4)
assert span.get_path()._interpolation_steps > 1
@check_figures_equal(extensions=["png"])
def test_remove_shared_polar(fig_ref, fig_test):
# Removing shared polar axes used to crash. Test removing them, keeping in
# both cases just the lower left axes of a grid to avoid running into a
# separate issue (now being fixed) of ticklabel visibility for shared axes.
axs = fig_ref.subplots(
2, 2, sharex=True, subplot_kw={"projection": "polar"})
for i in [0, 1, 3]:
axs.flat[i].remove()
axs = fig_test.subplots(
2, 2, sharey=True, subplot_kw={"projection": "polar"})
for i in [0, 1, 3]:
axs.flat[i].remove()
def test_shared_polar_keeps_ticklabels():
fig, axs = plt.subplots(
2, 2, subplot_kw={"projection": "polar"}, sharex=True, sharey=True)
fig.canvas.draw()
assert axs[0, 1].xaxis.majorTicks[0].get_visible()
assert axs[0, 1].yaxis.majorTicks[0].get_visible()
fig, axs = plt.subplot_mosaic(
"ab\ncd", subplot_kw={"projection": "polar"}, sharex=True, sharey=True)
fig.canvas.draw()
assert axs["b"].xaxis.majorTicks[0].get_visible()
assert axs["b"].yaxis.majorTicks[0].get_visible()
def test_axvline_axvspan_do_not_modify_rlims():
ax = plt.subplot(projection="polar")
ax.axvspan(0, 1)
ax.axvline(.5)
ax.plot([.1, .2])
assert ax.get_ylim() == (0, .2)
def test_cursor_precision():
ax = plt.subplot(projection="polar")
# Higher radii correspond to higher theta-precisions.
assert ax.format_coord(0, 0.005) == "θ=0.0π (0°), r=0.005"
assert ax.format_coord(0, .1) == "θ=0.00π (0°), r=0.100"
assert ax.format_coord(0, 1) == "θ=0.000π (0.0°), r=1.000"
assert ax.format_coord(1, 0.005) == "θ=0.3π (57°), r=0.005"
assert ax.format_coord(1, .1) == "θ=0.32π (57°), r=0.100"
assert ax.format_coord(1, 1) == "θ=0.318π (57.3°), r=1.000"
assert ax.format_coord(2, 0.005) == "θ=0.6π (115°), r=0.005"
assert ax.format_coord(2, .1) == "θ=0.64π (115°), r=0.100"
assert ax.format_coord(2, 1) == "θ=0.637π (114.6°), r=1.000"
def test_custom_fmt_data():
ax = plt.subplot(projection="polar")
def millions(x):
return '$%1.1fM' % (x*1e-6)
# Test only x formatter
ax.fmt_xdata = None
ax.fmt_ydata = millions
assert ax.format_coord(12, 2e7) == "θ=3.8197186342π (687.54935416°), r=$20.0M"
assert ax.format_coord(1234, 2e6) == "θ=392.794399551π (70702.9919191°), r=$2.0M"
assert ax.format_coord(3, 100) == "θ=0.95493π (171.887°), r=$0.0M"
# Test only y formatter
ax.fmt_xdata = millions
ax.fmt_ydata = None
assert ax.format_coord(2e5, 1) == "θ=$0.2M, r=1.000"
assert ax.format_coord(1, .1) == "θ=$0.0M, r=0.100"
assert ax.format_coord(1e6, 0.005) == "θ=$1.0M, r=0.005"
# Test both x and y formatters
ax.fmt_xdata = millions
ax.fmt_ydata = millions
assert ax.format_coord(2e6, 2e4*3e5) == "θ=$2.0M, r=$6000.0M"
assert ax.format_coord(1e18, 12891328123) == "θ=$1000000000000.0M, r=$12891.3M"
assert ax.format_coord(63**7, 1081968*1024) == "θ=$3938980.6M, r=$1107.9M"
@image_comparison(['polar_log.png'], style='default')
def test_polar_log():
fig = plt.figure()
ax = fig.add_subplot(polar=True)
ax.set_rscale('log')
ax.set_rlim(1, 1000)
n = 100
ax.plot(np.linspace(0, 2 * np.pi, n), np.logspace(0, 2, n))
@check_figures_equal()
def test_polar_log_rorigin(fig_ref, fig_test):
# Test that equivalent linear and log radial settings give the same axes patch
# and spines.
ax_ref = fig_ref.add_subplot(projection='polar', facecolor='red')
ax_ref.set_rlim(0, 2)
ax_ref.set_rorigin(-3)
ax_ref.set_rticks(np.linspace(0, 2, 5))
ax_test = fig_test.add_subplot(projection='polar', facecolor='red')
ax_test.set_rscale('log')
ax_test.set_rlim(1, 100)
ax_test.set_rorigin(10**-3)
ax_test.set_rticks(np.logspace(0, 2, 5))
for ax in ax_ref, ax_test:
# Radial tick labels should be the only difference, so turn them off.
ax.tick_params(labelleft=False)
def test_polar_neg_theta_lims():
fig = plt.figure()
ax = fig.add_subplot(projection='polar')
ax.set_thetalim(-np.pi, np.pi)
labels = [l.get_text() for l in ax.xaxis.get_ticklabels()]
assert labels == ['-180°', '-135°', '-90°', '-45°', '0°', '45°', '90°', '135°']
@pytest.mark.parametrize("order", ["before", "after"])
@image_comparison(baseline_images=['polar_errorbar.png'], remove_text=True,
style='mpl20')
def test_polar_errorbar(order):
theta = np.arange(0, 2 * np.pi, np.pi / 8)
r = theta / np.pi / 2 + 0.5
fig = plt.figure(figsize=(5, 5))
ax = fig.add_subplot(projection='polar')
if order == "before":
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)
ax.errorbar(theta, r, xerr=0.1, yerr=0.1, capsize=7, fmt="o", c="seagreen")
else:
ax.errorbar(theta, r, xerr=0.1, yerr=0.1, capsize=7, fmt="o", c="seagreen")
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)
@@ -0,0 +1,288 @@
import re
import sys
import numpy as np
import pytest
from matplotlib import _preprocess_data
from matplotlib.axes import Axes
from matplotlib.testing import subprocess_run_for_testing
from matplotlib.testing.decorators import check_figures_equal
# Notes on testing the plotting functions itself
# * the individual decorated plotting functions are tested in 'test_axes.py'
# * that pyplot functions accept a data kwarg is only tested in
# test_axes.test_pie_linewidth_0
# this gets used in multiple tests, so define it here
@_preprocess_data(replace_names=["x", "y"], label_namer="y")
def plot_func(ax, x, y, ls="x", label=None, w="xyz"):
return f"x: {list(x)}, y: {list(y)}, ls: {ls}, w: {w}, label: {label}"
all_funcs = [plot_func]
all_func_ids = ['plot_func']
def test_compiletime_checks():
"""Test decorator invocations -> no replacements."""
def func(ax, x, y): pass
def func_args(ax, x, y, *args): pass
def func_kwargs(ax, x, y, **kwargs): pass
def func_no_ax_args(*args, **kwargs): pass
# this is ok
_preprocess_data(replace_names=["x", "y"])(func)
_preprocess_data(replace_names=["x", "y"])(func_kwargs)
# this has "enough" information to do all the replaces
_preprocess_data(replace_names=["x", "y"])(func_args)
# no positional_parameter_names but needed due to replaces
with pytest.raises(AssertionError):
# z is unknown
_preprocess_data(replace_names=["x", "y", "z"])(func_args)
# no replacements at all -> all ok...
_preprocess_data(replace_names=[], label_namer=None)(func)
_preprocess_data(replace_names=[], label_namer=None)(func_args)
_preprocess_data(replace_names=[], label_namer=None)(func_kwargs)
_preprocess_data(replace_names=[], label_namer=None)(func_no_ax_args)
# label namer is unknown
with pytest.raises(AssertionError):
_preprocess_data(label_namer="z")(func)
with pytest.raises(AssertionError):
_preprocess_data(label_namer="z")(func_args)
@pytest.mark.parametrize('func', all_funcs, ids=all_func_ids)
def test_function_call_without_data(func):
"""Test without data -> no replacements."""
assert (func(None, "x", "y") ==
"x: ['x'], y: ['y'], ls: x, w: xyz, label: None")
assert (func(None, x="x", y="y") ==
"x: ['x'], y: ['y'], ls: x, w: xyz, label: None")
assert (func(None, "x", "y", label="") ==
"x: ['x'], y: ['y'], ls: x, w: xyz, label: ")
assert (func(None, "x", "y", label="text") ==
"x: ['x'], y: ['y'], ls: x, w: xyz, label: text")
assert (func(None, x="x", y="y", label="") ==
"x: ['x'], y: ['y'], ls: x, w: xyz, label: ")
assert (func(None, x="x", y="y", label="text") ==
"x: ['x'], y: ['y'], ls: x, w: xyz, label: text")
@pytest.mark.parametrize('func', all_funcs, ids=all_func_ids)
def test_function_call_with_dict_input(func):
"""Tests with dict input, unpacking via preprocess_pipeline"""
data = {'a': 1, 'b': 2}
assert (func(None, data.keys(), data.values()) ==
"x: ['a', 'b'], y: [1, 2], ls: x, w: xyz, label: None")
@pytest.mark.parametrize('func', all_funcs, ids=all_func_ids)
def test_function_call_with_dict_data(func):
"""Test with dict data -> label comes from the value of 'x' parameter."""
data = {"a": [1, 2], "b": [8, 9], "w": "NOT"}
assert (func(None, "a", "b", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b")
assert (func(None, x="a", y="b", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b")
assert (func(None, "a", "b", label="", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
assert (func(None, "a", "b", label="text", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
assert (func(None, x="a", y="b", label="", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
assert (func(None, x="a", y="b", label="text", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
@pytest.mark.parametrize('func', all_funcs, ids=all_func_ids)
def test_function_call_with_dict_data_not_in_data(func):
"""Test the case that one var is not in data -> half replaces, half kept"""
data = {"a": [1, 2], "w": "NOT"}
assert (func(None, "a", "b", data=data) ==
"x: [1, 2], y: ['b'], ls: x, w: xyz, label: b")
assert (func(None, x="a", y="b", data=data) ==
"x: [1, 2], y: ['b'], ls: x, w: xyz, label: b")
assert (func(None, "a", "b", label="", data=data) ==
"x: [1, 2], y: ['b'], ls: x, w: xyz, label: ")
assert (func(None, "a", "b", label="text", data=data) ==
"x: [1, 2], y: ['b'], ls: x, w: xyz, label: text")
assert (func(None, x="a", y="b", label="", data=data) ==
"x: [1, 2], y: ['b'], ls: x, w: xyz, label: ")
assert (func(None, x="a", y="b", label="text", data=data) ==
"x: [1, 2], y: ['b'], ls: x, w: xyz, label: text")
@pytest.mark.parametrize('func', all_funcs, ids=all_func_ids)
def test_function_call_with_pandas_data(func, pd):
"""Test with pandas dataframe -> label comes from ``data["col"].name``."""
data = pd.DataFrame({"a": np.array([1, 2], dtype=np.int32),
"b": np.array([8, 9], dtype=np.int32),
"w": ["NOT", "NOT"]})
assert (func(None, "a", "b", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b")
assert (func(None, x="a", y="b", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b")
assert (func(None, "a", "b", label="", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
assert (func(None, "a", "b", label="text", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
assert (func(None, x="a", y="b", label="", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
assert (func(None, x="a", y="b", label="text", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
def test_function_call_replace_all():
"""Test without a "replace_names" argument, all vars should be replaced."""
data = {"a": [1, 2], "b": [8, 9], "x": "xyz"}
@_preprocess_data(label_namer="y")
def func_replace_all(ax, x, y, ls="x", label=None, w="NOT"):
return f"x: {list(x)}, y: {list(y)}, ls: {ls}, w: {w}, label: {label}"
assert (func_replace_all(None, "a", "b", w="x", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b")
assert (func_replace_all(None, x="a", y="b", w="x", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b")
assert (func_replace_all(None, "a", "b", w="x", label="", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
assert (
func_replace_all(None, "a", "b", w="x", label="text", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
assert (
func_replace_all(None, x="a", y="b", w="x", label="", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
assert (
func_replace_all(None, x="a", y="b", w="x", label="text", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
def test_no_label_replacements():
"""Test with "label_namer=None" -> no label replacement at all."""
@_preprocess_data(replace_names=["x", "y"], label_namer=None)
def func_no_label(ax, x, y, ls="x", label=None, w="xyz"):
return f"x: {list(x)}, y: {list(y)}, ls: {ls}, w: {w}, label: {label}"
data = {"a": [1, 2], "b": [8, 9], "w": "NOT"}
assert (func_no_label(None, "a", "b", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: None")
assert (func_no_label(None, x="a", y="b", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: None")
assert (func_no_label(None, "a", "b", label="", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
assert (func_no_label(None, "a", "b", label="text", data=data) ==
"x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
def test_more_args_than_pos_parameter():
@_preprocess_data(replace_names=["x", "y"], label_namer="y")
def func(ax, x, y, z=1):
pass
data = {"a": [1, 2], "b": [8, 9], "w": "NOT"}
with pytest.raises(TypeError):
func(None, "a", "b", "z", "z", data=data)
def test_docstring_addition():
@_preprocess_data()
def funcy(ax, *args, **kwargs):
"""
Parameters
----------
data : indexable object, optional
DATA_PARAMETER_PLACEHOLDER
"""
assert re.search(r"all parameters also accept a string", funcy.__doc__)
assert not re.search(r"the following parameters", funcy.__doc__)
@_preprocess_data(replace_names=[])
def funcy(ax, x, y, z, bar=None):
"""
Parameters
----------
data : indexable object, optional
DATA_PARAMETER_PLACEHOLDER
"""
assert not re.search(r"all parameters also accept a string", funcy.__doc__)
assert not re.search(r"the following parameters", funcy.__doc__)
@_preprocess_data(replace_names=["bar"])
def funcy(ax, x, y, z, bar=None):
"""
Parameters
----------
data : indexable object, optional
DATA_PARAMETER_PLACEHOLDER
"""
assert not re.search(r"all parameters also accept a string", funcy.__doc__)
assert not re.search(r"the following parameters .*: \*bar\*\.",
funcy.__doc__)
@_preprocess_data(replace_names=["x", "t"])
def funcy(ax, x, y, z, t=None):
"""
Parameters
----------
data : indexable object, optional
DATA_PARAMETER_PLACEHOLDER
"""
assert not re.search(r"all parameters also accept a string", funcy.__doc__)
assert not re.search(r"the following parameters .*: \*x\*, \*t\*\.",
funcy.__doc__)
def test_data_parameter_replacement():
"""
Test that the docstring contains the correct *data* parameter stub
for all methods that we run _preprocess_data() on.
"""
program = (
"import logging; "
"logging.basicConfig(level=logging.DEBUG); "
"import matplotlib.pyplot as plt"
)
cmd = [sys.executable, "-c", program]
completed_proc = subprocess_run_for_testing(
cmd, text=True, capture_output=True
)
assert 'data parameter docstring error' not in completed_proc.stderr
class TestPlotTypes:
plotters = [Axes.scatter, Axes.bar, Axes.plot]
@pytest.mark.parametrize('plotter', plotters)
@check_figures_equal(extensions=['png'])
def test_dict_unpack(self, plotter, fig_test, fig_ref):
x = [1, 2, 3]
y = [4, 5, 6]
ddict = dict(zip(x, y))
plotter(fig_test.subplots(),
ddict.keys(), ddict.values())
plotter(fig_ref.subplots(), x, y)
@pytest.mark.parametrize('plotter', plotters)
@check_figures_equal(extensions=['png'])
def test_data_kwarg(self, plotter, fig_test, fig_ref):
x = [1, 2, 3]
y = [4, 5, 6]
plotter(fig_test.subplots(), 'xval', 'yval',
data={'xval': x, 'yval': y})
plotter(fig_ref.subplots(), x, y)
@@ -0,0 +1,485 @@
import difflib
import numpy as np
import sys
from pathlib import Path
import pytest
import matplotlib as mpl
from matplotlib.testing import subprocess_run_for_testing
from matplotlib import pyplot as plt
def test_pyplot_up_to_date(tmp_path):
pytest.importorskip("black", minversion="24.1")
gen_script = Path(mpl.__file__).parents[2] / "tools/boilerplate.py"
if not gen_script.exists():
pytest.skip("boilerplate.py not found")
orig_contents = Path(plt.__file__).read_text()
plt_file = tmp_path / 'pyplot.py'
plt_file.write_text(orig_contents, 'utf-8')
subprocess_run_for_testing(
[sys.executable, str(gen_script), str(plt_file)],
check=True)
new_contents = plt_file.read_text('utf-8')
if orig_contents != new_contents:
diff_msg = '\n'.join(
difflib.unified_diff(
orig_contents.split('\n'), new_contents.split('\n'),
fromfile='found pyplot.py',
tofile='expected pyplot.py',
n=0, lineterm=''))
pytest.fail(
"pyplot.py is not up-to-date. Please run "
"'python tools/boilerplate.py' to update pyplot.py. "
"This needs to be done from an environment where your "
"current working copy is installed (e.g. 'pip install -e'd). "
"Here is a diff of unexpected differences:\n%s" % diff_msg
)
def test_copy_docstring_and_deprecators(recwarn):
@mpl._api.rename_parameter(mpl.__version__, "old", "new")
@mpl._api.make_keyword_only(mpl.__version__, "kwo")
def func(new, kwo=None):
pass
@plt._copy_docstring_and_deprecators(func)
def wrapper_func(new, kwo=None):
pass
wrapper_func(None)
wrapper_func(new=None)
wrapper_func(None, kwo=None)
wrapper_func(new=None, kwo=None)
assert not recwarn
with pytest.warns(mpl.MatplotlibDeprecationWarning):
wrapper_func(old=None)
with pytest.warns(mpl.MatplotlibDeprecationWarning):
wrapper_func(None, None)
def test_pyplot_box():
fig, ax = plt.subplots()
plt.box(False)
assert not ax.get_frame_on()
plt.box(True)
assert ax.get_frame_on()
plt.box()
assert not ax.get_frame_on()
plt.box()
assert ax.get_frame_on()
def test_stackplot_smoke():
# Small smoke test for stackplot (see #12405)
plt.stackplot([1, 2, 3], [1, 2, 3])
def test_nrows_error():
with pytest.raises(TypeError):
plt.subplot(nrows=1)
with pytest.raises(TypeError):
plt.subplot(ncols=1)
def test_ioff():
plt.ion()
assert mpl.is_interactive()
with plt.ioff():
assert not mpl.is_interactive()
assert mpl.is_interactive()
plt.ioff()
assert not mpl.is_interactive()
with plt.ioff():
assert not mpl.is_interactive()
assert not mpl.is_interactive()
def test_ion():
plt.ioff()
assert not mpl.is_interactive()
with plt.ion():
assert mpl.is_interactive()
assert not mpl.is_interactive()
plt.ion()
assert mpl.is_interactive()
with plt.ion():
assert mpl.is_interactive()
assert mpl.is_interactive()
def test_nested_ion_ioff():
# initial state is interactive
plt.ion()
# mixed ioff/ion
with plt.ioff():
assert not mpl.is_interactive()
with plt.ion():
assert mpl.is_interactive()
assert not mpl.is_interactive()
assert mpl.is_interactive()
# redundant contexts
with plt.ioff():
with plt.ioff():
assert not mpl.is_interactive()
assert mpl.is_interactive()
with plt.ion():
plt.ioff()
assert mpl.is_interactive()
# initial state is not interactive
plt.ioff()
# mixed ioff/ion
with plt.ion():
assert mpl.is_interactive()
with plt.ioff():
assert not mpl.is_interactive()
assert mpl.is_interactive()
assert not mpl.is_interactive()
# redundant contexts
with plt.ion():
with plt.ion():
assert mpl.is_interactive()
assert not mpl.is_interactive()
with plt.ioff():
plt.ion()
assert not mpl.is_interactive()
def test_close():
try:
plt.close(1.1)
except TypeError as e:
assert str(e) == "close() argument must be a Figure, an int, " \
"a string, or None, not <class 'float'>"
def test_subplot_reuse():
ax1 = plt.subplot(121)
assert ax1 is plt.gca()
ax2 = plt.subplot(122)
assert ax2 is plt.gca()
ax3 = plt.subplot(121)
assert ax1 is plt.gca()
assert ax1 is ax3
def test_axes_kwargs():
# plt.axes() always creates new axes, even if axes kwargs differ.
plt.figure()
ax = plt.axes()
ax1 = plt.axes()
assert ax is not None
assert ax1 is not ax
plt.close()
plt.figure()
ax = plt.axes(projection='polar')
ax1 = plt.axes(projection='polar')
assert ax is not None
assert ax1 is not ax
plt.close()
plt.figure()
ax = plt.axes(projection='polar')
ax1 = plt.axes()
assert ax is not None
assert ax1.name == 'rectilinear'
assert ax1 is not ax
plt.close()
def test_subplot_replace_projection():
# plt.subplot() searches for axes with the same subplot spec, and if one
# exists, and the kwargs match returns it, create a new one if they do not
fig = plt.figure()
ax = plt.subplot(1, 2, 1)
ax1 = plt.subplot(1, 2, 1)
ax2 = plt.subplot(1, 2, 2)
ax3 = plt.subplot(1, 2, 1, projection='polar')
ax4 = plt.subplot(1, 2, 1, projection='polar')
assert ax is not None
assert ax1 is ax
assert ax2 is not ax
assert ax3 is not ax
assert ax3 is ax4
assert ax in fig.axes
assert ax2 in fig.axes
assert ax3 in fig.axes
assert ax.name == 'rectilinear'
assert ax2.name == 'rectilinear'
assert ax3.name == 'polar'
def test_subplot_kwarg_collision():
ax1 = plt.subplot(projection='polar', theta_offset=0)
ax2 = plt.subplot(projection='polar', theta_offset=0)
assert ax1 is ax2
ax1.remove()
ax3 = plt.subplot(projection='polar', theta_offset=1)
assert ax1 is not ax3
assert ax1 not in plt.gcf().axes
def test_gca():
# plt.gca() returns an existing axes, unless there were no axes.
plt.figure()
ax = plt.gca()
ax1 = plt.gca()
assert ax is not None
assert ax1 is ax
plt.close()
def test_subplot_projection_reuse():
# create an Axes
ax1 = plt.subplot(111)
# check that it is current
assert ax1 is plt.gca()
# make sure we get it back if we ask again
assert ax1 is plt.subplot(111)
# remove it
ax1.remove()
# create a polar plot
ax2 = plt.subplot(111, projection='polar')
assert ax2 is plt.gca()
# this should have deleted the first axes
assert ax1 not in plt.gcf().axes
# assert we get it back if no extra parameters passed
assert ax2 is plt.subplot(111)
ax2.remove()
# now check explicitly setting the projection to rectilinear
# makes a new axes
ax3 = plt.subplot(111, projection='rectilinear')
assert ax3 is plt.gca()
assert ax3 is not ax2
assert ax2 not in plt.gcf().axes
def test_subplot_polar_normalization():
ax1 = plt.subplot(111, projection='polar')
ax2 = plt.subplot(111, polar=True)
ax3 = plt.subplot(111, polar=True, projection='polar')
assert ax1 is ax2
assert ax1 is ax3
with pytest.raises(ValueError,
match="polar=True, yet projection='3d'"):
ax2 = plt.subplot(111, polar=True, projection='3d')
def test_subplot_change_projection():
created_axes = set()
ax = plt.subplot()
created_axes.add(ax)
projections = ('aitoff', 'hammer', 'lambert', 'mollweide',
'polar', 'rectilinear', '3d')
for proj in projections:
ax.remove()
ax = plt.subplot(projection=proj)
assert ax is plt.subplot()
assert ax.name == proj
created_axes.add(ax)
# Check that each call created a new Axes.
assert len(created_axes) == 1 + len(projections)
def test_polar_second_call():
# the first call creates the axes with polar projection
ln1, = plt.polar(0., 1., 'ro')
assert isinstance(ln1, mpl.lines.Line2D)
# the second call should reuse the existing axes
ln2, = plt.polar(1.57, .5, 'bo')
assert isinstance(ln2, mpl.lines.Line2D)
assert ln1.axes is ln2.axes
def test_fallback_position():
# check that position kwarg works if rect not supplied
axref = plt.axes([0.2, 0.2, 0.5, 0.5])
axtest = plt.axes(position=[0.2, 0.2, 0.5, 0.5])
np.testing.assert_allclose(axtest.bbox.get_points(),
axref.bbox.get_points())
# check that position kwarg ignored if rect is supplied
axref = plt.axes([0.2, 0.2, 0.5, 0.5])
axtest = plt.axes([0.2, 0.2, 0.5, 0.5], position=[0.1, 0.1, 0.8, 0.8])
np.testing.assert_allclose(axtest.bbox.get_points(),
axref.bbox.get_points())
def test_set_current_figure_via_subfigure():
fig1 = plt.figure()
subfigs = fig1.subfigures(2)
plt.figure()
assert plt.gcf() != fig1
current = plt.figure(subfigs[1])
assert plt.gcf() == fig1
assert current == fig1
def test_set_current_axes_on_subfigure():
fig = plt.figure()
subfigs = fig.subfigures(2)
ax = subfigs[0].subplots(1, squeeze=True)
subfigs[1].subplots(1, squeeze=True)
assert plt.gca() != ax
plt.sca(ax)
assert plt.gca() == ax
def test_pylab_integration():
IPython = pytest.importorskip("IPython")
mpl.testing.subprocess_run_helper(
IPython.start_ipython,
"--pylab",
"-c",
";".join((
"import matplotlib.pyplot as plt",
"assert plt._REPL_DISPLAYHOOK == plt._ReplDisplayHook.IPYTHON",
)),
timeout=60,
)
def test_doc_pyplot_summary():
"""Test that pyplot_summary lists all the plot functions."""
pyplot_docs = Path(__file__).parent / '../../../doc/api/pyplot_summary.rst'
if not pyplot_docs.exists():
pytest.skip("Documentation sources not available")
def extract_documented_functions(lines):
"""
Return a list of all the functions that are mentioned in the
autosummary blocks contained in *lines*.
An autosummary block looks like this::
.. autosummary::
:toctree: _as_gen
:template: autosummary.rst
:nosignatures:
plot
plot_date
"""
functions = []
in_autosummary = False
for line in lines:
if not in_autosummary:
if line.startswith(".. autosummary::"):
in_autosummary = True
else:
if not line or line.startswith(" :"):
# empty line or autosummary parameter
continue
if not line[0].isspace():
# no more indentation: end of autosummary block
in_autosummary = False
continue
functions.append(line.strip())
return functions
lines = pyplot_docs.read_text().split("\n")
doc_functions = set(extract_documented_functions(lines))
plot_commands = set(plt._get_pyplot_commands())
missing = plot_commands.difference(doc_functions)
if missing:
raise AssertionError(
f"The following pyplot functions are not listed in the "
f"documentation. Please add them to doc/api/pyplot_summary.rst: "
f"{missing!r}")
extra = doc_functions.difference(plot_commands)
if extra:
raise AssertionError(
f"The following functions are listed in the pyplot documentation, "
f"but they do not exist in pyplot. "
f"Please remove them from doc/api/pyplot_summary.rst: {extra!r}")
def test_minor_ticks():
plt.figure()
plt.plot(np.arange(1, 10))
tick_pos, tick_labels = plt.xticks(minor=True)
assert np.all(tick_labels == np.array([], dtype=np.float64))
assert tick_labels == []
plt.yticks(ticks=[3.5, 6.5], labels=["a", "b"], minor=True)
ax = plt.gca()
tick_pos = ax.get_yticks(minor=True)
tick_labels = ax.get_yticklabels(minor=True)
assert np.all(tick_pos == np.array([3.5, 6.5]))
assert [l.get_text() for l in tick_labels] == ['a', 'b']
def test_switch_backend_no_close():
plt.switch_backend('agg')
fig = plt.figure()
fig = plt.figure()
assert len(plt.get_fignums()) == 2
plt.switch_backend('agg')
assert len(plt.get_fignums()) == 2
plt.switch_backend('svg')
assert len(plt.get_fignums()) == 2
def figure_hook_example(figure):
figure._test_was_here = True
def test_figure_hook():
test_rc = {
'figure.hooks': ['matplotlib.tests.test_pyplot:figure_hook_example']
}
with mpl.rc_context(test_rc):
fig = plt.figure()
assert fig._test_was_here
def test_multiple_same_figure_calls():
fig = plt.figure(1, figsize=(1, 2))
with pytest.warns(UserWarning, match="Ignoring specified arguments in this call"):
fig2 = plt.figure(1, figsize=np.array([3, 4]))
with pytest.warns(UserWarning, match="Ignoring specified arguments in this call"):
plt.figure(fig, figsize=np.array([5, 6]))
assert fig is fig2
fig3 = plt.figure(1) # Checks for false warnings
assert fig is fig3
def test_close_all_warning():
fig1 = plt.figure()
# Check that the warning is issued when 'all' is passed to plt.figure
with pytest.warns(UserWarning, match="closes all existing figures"):
fig2 = plt.figure("all")
def test_matshow():
fig = plt.figure()
arr = [[0, 1], [1, 2]]
# Smoke test that matshow does not ask for a new figsize on the existing figure
plt.matshow(arr, fignum=fig.number)
@@ -0,0 +1,387 @@
import platform
import sys
import numpy as np
import pytest
from matplotlib import pyplot as plt
from matplotlib.testing.decorators import image_comparison
from matplotlib.testing.decorators import check_figures_equal
def draw_quiver(ax, **kwargs):
X, Y = np.meshgrid(np.arange(0, 2 * np.pi, 1),
np.arange(0, 2 * np.pi, 1))
U = np.cos(X)
V = np.sin(Y)
Q = ax.quiver(U, V, **kwargs)
return Q
@pytest.mark.skipif(platform.python_implementation() != 'CPython',
reason='Requires CPython')
def test_quiver_memory_leak():
fig, ax = plt.subplots()
Q = draw_quiver(ax)
ttX = Q.X
orig_refcount = sys.getrefcount(ttX)
Q.remove()
del Q
assert sys.getrefcount(ttX) < orig_refcount
@pytest.mark.skipif(platform.python_implementation() != 'CPython',
reason='Requires CPython')
def test_quiver_key_memory_leak():
fig, ax = plt.subplots()
Q = draw_quiver(ax)
qk = ax.quiverkey(Q, 0.5, 0.92, 2, r'$2 \frac{m}{s}$',
labelpos='W',
fontproperties={'weight': 'bold'})
orig_refcount = sys.getrefcount(qk)
qk.remove()
assert sys.getrefcount(qk) < orig_refcount
def test_quiver_number_of_args():
X = [1, 2]
with pytest.raises(
TypeError,
match='takes from 2 to 5 positional arguments but 1 were given'):
plt.quiver(X)
with pytest.raises(
TypeError,
match='takes from 2 to 5 positional arguments but 6 were given'):
plt.quiver(X, X, X, X, X, X)
def test_quiver_arg_sizes():
X2 = [1, 2]
X3 = [1, 2, 3]
with pytest.raises(
ValueError, match=('X and Y must be the same size, but '
'X.size is 2 and Y.size is 3.')):
plt.quiver(X2, X3, X2, X2)
with pytest.raises(
ValueError, match=('Argument U has a size 3 which does not match '
'2, the number of arrow positions')):
plt.quiver(X2, X2, X3, X2)
with pytest.raises(
ValueError, match=('Argument V has a size 3 which does not match '
'2, the number of arrow positions')):
plt.quiver(X2, X2, X2, X3)
with pytest.raises(
ValueError, match=('Argument C has a size 3 which does not match '
'2, the number of arrow positions')):
plt.quiver(X2, X2, X2, X2, X3)
def test_no_warnings():
fig, ax = plt.subplots()
X, Y = np.meshgrid(np.arange(15), np.arange(10))
U = V = np.ones_like(X)
phi = (np.random.rand(15, 10) - .5) * 150
ax.quiver(X, Y, U, V, angles=phi)
fig.canvas.draw() # Check that no warning is emitted.
def test_zero_headlength():
# Based on report by Doug McNeil:
# https://discourse.matplotlib.org/t/quiver-warnings/16722
fig, ax = plt.subplots()
X, Y = np.meshgrid(np.arange(10), np.arange(10))
U, V = np.cos(X), np.sin(Y)
ax.quiver(U, V, headlength=0, headaxislength=0)
fig.canvas.draw() # Check that no warning is emitted.
@image_comparison(['quiver_animated_test_image.png'])
def test_quiver_animate():
# Tests fix for #2616
fig, ax = plt.subplots()
Q = draw_quiver(ax, animated=True)
ax.quiverkey(Q, 0.5, 0.92, 2, r'$2 \frac{m}{s}$',
labelpos='W', fontproperties={'weight': 'bold'})
@image_comparison(['quiver_with_key_test_image.png'])
def test_quiver_with_key():
fig, ax = plt.subplots()
ax.margins(0.1)
Q = draw_quiver(ax)
ax.quiverkey(Q, 0.5, 0.95, 2,
r'$2\, \mathrm{m}\, \mathrm{s}^{-1}$',
angle=-10,
coordinates='figure',
labelpos='W',
fontproperties={'weight': 'bold', 'size': 'large'})
@image_comparison(['quiver_single_test_image.png'], remove_text=True)
def test_quiver_single():
fig, ax = plt.subplots()
ax.margins(0.1)
ax.quiver([1], [1], [2], [2])
def test_quiver_copy():
fig, ax = plt.subplots()
uv = dict(u=np.array([1.1]), v=np.array([2.0]))
q0 = ax.quiver([1], [1], uv['u'], uv['v'])
uv['v'][0] = 0
assert q0.V[0] == 2.0
@image_comparison(['quiver_key_pivot.png'], remove_text=True)
def test_quiver_key_pivot():
fig, ax = plt.subplots()
u, v = np.mgrid[0:2*np.pi:10j, 0:2*np.pi:10j]
q = ax.quiver(np.sin(u), np.cos(v))
ax.set_xlim(-2, 11)
ax.set_ylim(-2, 11)
ax.quiverkey(q, 0.5, 1, 1, 'N', labelpos='N')
ax.quiverkey(q, 1, 0.5, 1, 'E', labelpos='E')
ax.quiverkey(q, 0.5, 0, 1, 'S', labelpos='S')
ax.quiverkey(q, 0, 0.5, 1, 'W', labelpos='W')
@image_comparison(['quiver_key_xy.png'], remove_text=True)
def test_quiver_key_xy():
# With scale_units='xy', ensure quiverkey still matches its quiver.
# Note that the quiver and quiverkey lengths depend on the axes aspect
# ratio, and that with angles='xy' their angles also depend on the axes
# aspect ratio.
X = np.arange(8)
Y = np.zeros(8)
angles = X * (np.pi / 4)
uv = np.exp(1j * angles)
U = uv.real
V = uv.imag
fig, axs = plt.subplots(2)
for ax, angle_str in zip(axs, ('uv', 'xy')):
ax.set_xlim(-1, 8)
ax.set_ylim(-0.2, 0.2)
q = ax.quiver(X, Y, U, V, pivot='middle',
units='xy', width=0.05,
scale=2, scale_units='xy',
angles=angle_str)
for x, angle in zip((0.2, 0.5, 0.8), (0, 45, 90)):
ax.quiverkey(q, X=x, Y=0.8, U=1, angle=angle, label='', color='b')
@image_comparison(['barbs_test_image.png'], remove_text=True)
def test_barbs():
x = np.linspace(-5, 5, 5)
X, Y = np.meshgrid(x, x)
U, V = 12*X, 12*Y
fig, ax = plt.subplots()
ax.barbs(X, Y, U, V, np.hypot(U, V), fill_empty=True, rounding=False,
sizes=dict(emptybarb=0.25, spacing=0.2, height=0.3),
cmap='viridis')
@image_comparison(['barbs_pivot_test_image.png'], remove_text=True)
def test_barbs_pivot():
x = np.linspace(-5, 5, 5)
X, Y = np.meshgrid(x, x)
U, V = 12*X, 12*Y
fig, ax = plt.subplots()
ax.barbs(X, Y, U, V, fill_empty=True, rounding=False, pivot=1.7,
sizes=dict(emptybarb=0.25, spacing=0.2, height=0.3))
ax.scatter(X, Y, s=49, c='black')
@image_comparison(['barbs_test_flip.png'], remove_text=True)
def test_barbs_flip():
"""Test barbs with an array for flip_barb."""
x = np.linspace(-5, 5, 5)
X, Y = np.meshgrid(x, x)
U, V = 12*X, 12*Y
fig, ax = plt.subplots()
ax.barbs(X, Y, U, V, fill_empty=True, rounding=False, pivot=1.7,
sizes=dict(emptybarb=0.25, spacing=0.2, height=0.3),
flip_barb=Y < 0)
def test_barb_copy():
fig, ax = plt.subplots()
u = np.array([1.1])
v = np.array([2.2])
b0 = ax.barbs([1], [1], u, v)
u[0] = 0
assert b0.u[0] == 1.1
v[0] = 0
assert b0.v[0] == 2.2
def test_bad_masked_sizes():
"""Test error handling when given differing sized masked arrays."""
x = np.arange(3)
y = np.arange(3)
u = np.ma.array(15. * np.ones((4,)))
v = np.ma.array(15. * np.ones_like(u))
u[1] = np.ma.masked
v[1] = np.ma.masked
fig, ax = plt.subplots()
with pytest.raises(ValueError):
ax.barbs(x, y, u, v)
def test_angles_and_scale():
# angles array + scale_units kwarg
fig, ax = plt.subplots()
X, Y = np.meshgrid(np.arange(15), np.arange(10))
U = V = np.ones_like(X)
phi = (np.random.rand(15, 10) - .5) * 150
ax.quiver(X, Y, U, V, angles=phi, scale_units='xy')
@image_comparison(['quiver_xy.png'], remove_text=True)
def test_quiver_xy():
# simple arrow pointing from SW to NE
fig, ax = plt.subplots(subplot_kw=dict(aspect='equal'))
ax.quiver(0, 0, 1, 1, angles='xy', scale_units='xy', scale=1)
ax.set_xlim(0, 1.1)
ax.set_ylim(0, 1.1)
ax.grid()
def test_quiverkey_angles():
# Check that only a single arrow is plotted for a quiverkey when an array
# of angles is given to the original quiver plot
fig, ax = plt.subplots()
X, Y = np.meshgrid(np.arange(2), np.arange(2))
U = V = angles = np.ones_like(X)
q = ax.quiver(X, Y, U, V, angles=angles)
qk = ax.quiverkey(q, 1, 1, 2, 'Label')
# The arrows are only created when the key is drawn
fig.canvas.draw()
assert len(qk.verts) == 1
def test_quiverkey_angles_xy_aitoff():
# GH 26316 and GH 26748
# Test that only one arrow will be plotted with non-cartesian
# when angles='xy' and/or scale_units='xy'
# only for test purpose
# scale_units='xy' may not be a valid use case for non-cartesian
kwargs_list = [
{'angles': 'xy'},
{'angles': 'xy', 'scale_units': 'xy'},
{'scale_units': 'xy'}
]
for kwargs_dict in kwargs_list:
x = np.linspace(-np.pi, np.pi, 11)
y = np.ones_like(x) * np.pi / 6
vx = np.zeros_like(x)
vy = np.ones_like(x)
fig = plt.figure()
ax = fig.add_subplot(projection='aitoff')
q = ax.quiver(x, y, vx, vy, **kwargs_dict)
qk = ax.quiverkey(q, 0, 0, 1, '1 units')
fig.canvas.draw()
assert len(qk.verts) == 1
def test_quiverkey_angles_scale_units_cartesian():
# GH 26316
# Test that only one arrow will be plotted with normal cartesian
# when angles='xy' and/or scale_units='xy'
kwargs_list = [
{'angles': 'xy'},
{'angles': 'xy', 'scale_units': 'xy'},
{'scale_units': 'xy'}
]
for kwargs_dict in kwargs_list:
X = [0, -1, 0]
Y = [0, -1, 0]
U = [1, -1, 1]
V = [1, -1, 0]
fig, ax = plt.subplots()
q = ax.quiver(X, Y, U, V, **kwargs_dict)
ax.quiverkey(q, X=0.3, Y=1.1, U=1,
label='Quiver key, length = 1', labelpos='E')
qk = ax.quiverkey(q, 0, 0, 1, '1 units')
fig.canvas.draw()
assert len(qk.verts) == 1
def test_quiver_setuvc_numbers():
"""Check that it is possible to set all arrow UVC to the same numbers"""
fig, ax = plt.subplots()
X, Y = np.meshgrid(np.arange(2), np.arange(2))
U = V = np.ones_like(X)
q = ax.quiver(X, Y, U, V)
q.set_UVC(0, 1)
def draw_quiverkey_zorder_argument(fig, zorder=None):
"""Draw Quiver and QuiverKey using zorder argument"""
x = np.arange(1, 6, 1)
y = np.arange(1, 6, 1)
X, Y = np.meshgrid(x, y)
U, V = 2, 2
ax = fig.subplots()
q = ax.quiver(X, Y, U, V, pivot='middle')
ax.set_xlim(0.5, 5.5)
ax.set_ylim(0.5, 5.5)
if zorder is None:
ax.quiverkey(q, 4, 4, 25, coordinates='data',
label='U', color='blue')
ax.quiverkey(q, 5.5, 2, 20, coordinates='data',
label='V', color='blue', angle=90)
else:
ax.quiverkey(q, 4, 4, 25, coordinates='data',
label='U', color='blue', zorder=zorder)
ax.quiverkey(q, 5.5, 2, 20, coordinates='data',
label='V', color='blue', angle=90, zorder=zorder)
def draw_quiverkey_setzorder(fig, zorder=None):
"""Draw Quiver and QuiverKey using set_zorder"""
x = np.arange(1, 6, 1)
y = np.arange(1, 6, 1)
X, Y = np.meshgrid(x, y)
U, V = 2, 2
ax = fig.subplots()
q = ax.quiver(X, Y, U, V, pivot='middle')
ax.set_xlim(0.5, 5.5)
ax.set_ylim(0.5, 5.5)
qk1 = ax.quiverkey(q, 4, 4, 25, coordinates='data',
label='U', color='blue')
qk2 = ax.quiverkey(q, 5.5, 2, 20, coordinates='data',
label='V', color='blue', angle=90)
if zorder is not None:
qk1.set_zorder(zorder)
qk2.set_zorder(zorder)
@pytest.mark.parametrize('zorder', [0, 2, 5, None])
@check_figures_equal(extensions=['png'])
def test_quiverkey_zorder(fig_test, fig_ref, zorder):
draw_quiverkey_zorder_argument(fig_test, zorder=zorder)
draw_quiverkey_setzorder(fig_ref, zorder=zorder)
@@ -0,0 +1,682 @@
import copy
import os
import subprocess
import sys
from unittest import mock
from cycler import cycler, Cycler
from packaging.version import parse as parse_version
import pytest
import matplotlib as mpl
from matplotlib import _api, _c_internal_utils
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np
from matplotlib.rcsetup import (
validate_bool,
validate_color,
validate_colorlist,
_validate_color_or_linecolor,
validate_cycler,
validate_float,
validate_fontstretch,
validate_fontweight,
validate_hatch,
validate_hist_bins,
validate_int,
validate_markevery,
validate_stringlist,
validate_sketch,
_validate_linestyle,
_listify_validator)
from matplotlib.testing import subprocess_run_for_testing
def test_rcparams(tmp_path):
mpl.rc('text', usetex=False)
mpl.rc('lines', linewidth=22)
usetex = mpl.rcParams['text.usetex']
linewidth = mpl.rcParams['lines.linewidth']
rcpath = tmp_path / 'test_rcparams.rc'
rcpath.write_text('lines.linewidth: 33', encoding='utf-8')
# test context given dictionary
with mpl.rc_context(rc={'text.usetex': not usetex}):
assert mpl.rcParams['text.usetex'] == (not usetex)
assert mpl.rcParams['text.usetex'] == usetex
# test context given filename (mpl.rc sets linewidth to 33)
with mpl.rc_context(fname=rcpath):
assert mpl.rcParams['lines.linewidth'] == 33
assert mpl.rcParams['lines.linewidth'] == linewidth
# test context given filename and dictionary
with mpl.rc_context(fname=rcpath, rc={'lines.linewidth': 44}):
assert mpl.rcParams['lines.linewidth'] == 44
assert mpl.rcParams['lines.linewidth'] == linewidth
# test context as decorator (and test reusability, by calling func twice)
@mpl.rc_context({'lines.linewidth': 44})
def func():
assert mpl.rcParams['lines.linewidth'] == 44
func()
func()
# test rc_file
mpl.rc_file(rcpath)
assert mpl.rcParams['lines.linewidth'] == 33
def test_RcParams_class():
rc = mpl.RcParams({'font.cursive': ['Apple Chancery',
'Textile',
'Zapf Chancery',
'cursive'],
'font.family': 'sans-serif',
'font.weight': 'normal',
'font.size': 12})
expected_repr = """
RcParams({'font.cursive': ['Apple Chancery',
'Textile',
'Zapf Chancery',
'cursive'],
'font.family': ['sans-serif'],
'font.size': 12.0,
'font.weight': 'normal'})""".lstrip()
assert expected_repr == repr(rc)
expected_str = """
font.cursive: ['Apple Chancery', 'Textile', 'Zapf Chancery', 'cursive']
font.family: ['sans-serif']
font.size: 12.0
font.weight: normal""".lstrip()
assert expected_str == str(rc)
# test the find_all functionality
assert ['font.cursive', 'font.size'] == sorted(rc.find_all('i[vz]'))
assert ['font.family'] == list(rc.find_all('family'))
def test_rcparams_update():
rc = mpl.RcParams({'figure.figsize': (3.5, 42)})
bad_dict = {'figure.figsize': (3.5, 42, 1)}
# make sure validation happens on input
with pytest.raises(ValueError):
rc.update(bad_dict)
def test_rcparams_init():
with pytest.raises(ValueError):
mpl.RcParams({'figure.figsize': (3.5, 42, 1)})
def test_nargs_cycler():
from matplotlib.rcsetup import cycler as ccl
with pytest.raises(TypeError, match='3 were given'):
# cycler() takes 0-2 arguments.
ccl(ccl(color=list('rgb')), 2, 3)
def test_Bug_2543():
# Test that it possible to add all values to itself / deepcopy
# https://github.com/matplotlib/matplotlib/issues/2543
# We filter warnings at this stage since a number of them are raised
# for deprecated rcparams as they should. We don't want these in the
# printed in the test suite.
with _api.suppress_matplotlib_deprecation_warning():
with mpl.rc_context():
_copy = mpl.rcParams.copy()
for key in _copy:
mpl.rcParams[key] = _copy[key]
with mpl.rc_context():
copy.deepcopy(mpl.rcParams)
with pytest.raises(ValueError):
validate_bool(None)
with pytest.raises(ValueError):
with mpl.rc_context():
mpl.rcParams['svg.fonttype'] = True
legend_color_tests = [
('face', {'color': 'r'}, mcolors.to_rgba('r')),
('face', {'color': 'inherit', 'axes.facecolor': 'r'},
mcolors.to_rgba('r')),
('face', {'color': 'g', 'axes.facecolor': 'r'}, mcolors.to_rgba('g')),
('edge', {'color': 'r'}, mcolors.to_rgba('r')),
('edge', {'color': 'inherit', 'axes.edgecolor': 'r'},
mcolors.to_rgba('r')),
('edge', {'color': 'g', 'axes.facecolor': 'r'}, mcolors.to_rgba('g'))
]
legend_color_test_ids = [
'same facecolor',
'inherited facecolor',
'different facecolor',
'same edgecolor',
'inherited edgecolor',
'different facecolor',
]
@pytest.mark.parametrize('color_type, param_dict, target', legend_color_tests,
ids=legend_color_test_ids)
def test_legend_colors(color_type, param_dict, target):
param_dict[f'legend.{color_type}color'] = param_dict.pop('color')
get_func = f'get_{color_type}color'
with mpl.rc_context(param_dict):
_, ax = plt.subplots()
ax.plot(range(3), label='test')
leg = ax.legend()
assert getattr(leg.legendPatch, get_func)() == target
def test_mfc_rcparams():
mpl.rcParams['lines.markerfacecolor'] = 'r'
ln = mpl.lines.Line2D([1, 2], [1, 2])
assert ln.get_markerfacecolor() == 'r'
def test_mec_rcparams():
mpl.rcParams['lines.markeredgecolor'] = 'r'
ln = mpl.lines.Line2D([1, 2], [1, 2])
assert ln.get_markeredgecolor() == 'r'
def test_axes_titlecolor_rcparams():
mpl.rcParams['axes.titlecolor'] = 'r'
_, ax = plt.subplots()
title = ax.set_title("Title")
assert title.get_color() == 'r'
def test_Issue_1713(tmp_path):
rcpath = tmp_path / 'test_rcparams.rc'
rcpath.write_text('timezone: UTC', encoding='utf-8')
with mock.patch('locale.getpreferredencoding', return_value='UTF-32-BE'):
rc = mpl.rc_params_from_file(rcpath, True, False)
assert rc.get('timezone') == 'UTC'
def test_animation_frame_formats():
# Animation frame_format should allow any of the following
# if any of these are not allowed, an exception will be raised
# test for gh issue #17908
for fmt in ['png', 'jpeg', 'tiff', 'raw', 'rgba', 'ppm',
'sgi', 'bmp', 'pbm', 'svg']:
mpl.rcParams['animation.frame_format'] = fmt
def generate_validator_testcases(valid):
validation_tests = (
{'validator': validate_bool,
'success': (*((_, True) for _ in
('t', 'y', 'yes', 'on', 'true', '1', 1, True)),
*((_, False) for _ in
('f', 'n', 'no', 'off', 'false', '0', 0, False))),
'fail': ((_, ValueError)
for _ in ('aardvark', 2, -1, [], ))
},
{'validator': validate_stringlist,
'success': (('', []),
('a,b', ['a', 'b']),
('aardvark', ['aardvark']),
('aardvark, ', ['aardvark']),
('aardvark, ,', ['aardvark']),
(['a', 'b'], ['a', 'b']),
(('a', 'b'), ['a', 'b']),
(iter(['a', 'b']), ['a', 'b']),
(np.array(['a', 'b']), ['a', 'b']),
),
'fail': ((set(), ValueError),
(1, ValueError),
)
},
{'validator': _listify_validator(validate_int, n=2),
'success': ((_, [1, 2])
for _ in ('1, 2', [1.5, 2.5], [1, 2],
(1, 2), np.array((1, 2)))),
'fail': ((_, ValueError)
for _ in ('aardvark', ('a', 1),
(1, 2, 3)
))
},
{'validator': _listify_validator(validate_float, n=2),
'success': ((_, [1.5, 2.5])
for _ in ('1.5, 2.5', [1.5, 2.5], [1.5, 2.5],
(1.5, 2.5), np.array((1.5, 2.5)))),
'fail': ((_, ValueError)
for _ in ('aardvark', ('a', 1), (1, 2, 3), (None, ), None))
},
{'validator': validate_cycler,
'success': (('cycler("color", "rgb")',
cycler("color", 'rgb')),
(cycler('linestyle', ['-', '--']),
cycler('linestyle', ['-', '--'])),
("""(cycler("color", ["r", "g", "b"]) +
cycler("mew", [2, 3, 5]))""",
(cycler("color", 'rgb') +
cycler("markeredgewidth", [2, 3, 5]))),
("cycler(c='rgb', lw=[1, 2, 3])",
cycler('color', 'rgb') + cycler('linewidth', [1, 2, 3])),
("cycler('c', 'rgb') * cycler('linestyle', ['-', '--'])",
(cycler('color', 'rgb') *
cycler('linestyle', ['-', '--']))),
(cycler('ls', ['-', '--']),
cycler('linestyle', ['-', '--'])),
(cycler(mew=[2, 5]),
cycler('markeredgewidth', [2, 5])),
),
# This is *so* incredibly important: validate_cycler() eval's
# an arbitrary string! I think I have it locked down enough,
# and that is what this is testing.
# TODO: Note that these tests are actually insufficient, as it may
# be that they raised errors, but still did an action prior to
# raising the exception. We should devise some additional tests
# for that...
'fail': ((4, ValueError), # Gotta be a string or Cycler object
('cycler("bleh, [])', ValueError), # syntax error
('Cycler("linewidth", [1, 2, 3])',
ValueError), # only 'cycler()' function is allowed
# do not allow dunder in string literals
("cycler('c', [j.__class__(j) for j in ['r', 'b']])",
ValueError),
("cycler('c', [j. __class__(j) for j in ['r', 'b']])",
ValueError),
("cycler('c', [j.\t__class__(j) for j in ['r', 'b']])",
ValueError),
("cycler('c', [j.\u000c__class__(j) for j in ['r', 'b']])",
ValueError),
("cycler('c', [j.__class__(j).lower() for j in ['r', 'b']])",
ValueError),
('1 + 2', ValueError), # doesn't produce a Cycler object
('os.system("echo Gotcha")', ValueError), # os not available
('import os', ValueError), # should not be able to import
('def badjuju(a): return a; badjuju(cycler("color", "rgb"))',
ValueError), # Should not be able to define anything
# even if it does return a cycler
('cycler("waka", [1, 2, 3])', ValueError), # not a property
('cycler(c=[1, 2, 3])', ValueError), # invalid values
("cycler(lw=['a', 'b', 'c'])", ValueError), # invalid values
(cycler('waka', [1, 3, 5]), ValueError), # not a property
(cycler('color', ['C1', 'r', 'g']), ValueError) # no CN
)
},
{'validator': validate_hatch,
'success': (('--|', '--|'), ('\\oO', '\\oO'),
('/+*/.x', '/+*/.x'), ('', '')),
'fail': (('--_', ValueError),
(8, ValueError),
('X', ValueError)),
},
{'validator': validate_colorlist,
'success': (('r,g,b', ['r', 'g', 'b']),
(['r', 'g', 'b'], ['r', 'g', 'b']),
('r, ,', ['r']),
(['', 'g', 'blue'], ['g', 'blue']),
([np.array([1, 0, 0]), np.array([0, 1, 0])],
np.array([[1, 0, 0], [0, 1, 0]])),
(np.array([[1, 0, 0], [0, 1, 0]]),
np.array([[1, 0, 0], [0, 1, 0]])),
),
'fail': (('fish', ValueError),
),
},
{'validator': validate_color,
'success': (('None', 'none'),
('none', 'none'),
('AABBCC', '#AABBCC'), # RGB hex code
('AABBCC00', '#AABBCC00'), # RGBA hex code
('tab:blue', 'tab:blue'), # named color
('C12', 'C12'), # color from cycle
('(0, 1, 0)', (0.0, 1.0, 0.0)), # RGB tuple
((0, 1, 0), (0, 1, 0)), # non-string version
('(0, 1, 0, 1)', (0.0, 1.0, 0.0, 1.0)), # RGBA tuple
((0, 1, 0, 1), (0, 1, 0, 1)), # non-string version
),
'fail': (('tab:veryblue', ValueError), # invalid name
('(0, 1)', ValueError), # tuple with length < 3
('(0, 1, 0, 1, 0)', ValueError), # tuple with length > 4
('(0, 1, none)', ValueError), # cannot cast none to float
('(0, 1, "0.5")', ValueError), # last one not a float
),
},
{'validator': _validate_color_or_linecolor,
'success': (('linecolor', 'linecolor'),
('markerfacecolor', 'markerfacecolor'),
('mfc', 'markerfacecolor'),
('markeredgecolor', 'markeredgecolor'),
('mec', 'markeredgecolor')
),
'fail': (('line', ValueError),
('marker', ValueError)
)
},
{'validator': validate_hist_bins,
'success': (('auto', 'auto'),
('fd', 'fd'),
('10', 10),
('1, 2, 3', [1, 2, 3]),
([1, 2, 3], [1, 2, 3]),
(np.arange(15), np.arange(15))
),
'fail': (('aardvark', ValueError),
)
},
{'validator': validate_markevery,
'success': ((None, None),
(1, 1),
(0.1, 0.1),
((1, 1), (1, 1)),
((0.1, 0.1), (0.1, 0.1)),
([1, 2, 3], [1, 2, 3]),
(slice(2), slice(None, 2, None)),
(slice(1, 2, 3), slice(1, 2, 3))
),
'fail': (((1, 2, 3), TypeError),
([1, 2, 0.3], TypeError),
(['a', 2, 3], TypeError),
([1, 2, 'a'], TypeError),
((0.1, 0.2, 0.3), TypeError),
((0.1, 2, 3), TypeError),
((1, 0.2, 0.3), TypeError),
((1, 0.1), TypeError),
((0.1, 1), TypeError),
(('abc'), TypeError),
((1, 'a'), TypeError),
((0.1, 'b'), TypeError),
(('a', 1), TypeError),
(('a', 0.1), TypeError),
('abc', TypeError),
('a', TypeError),
(object(), TypeError)
)
},
{'validator': _validate_linestyle,
'success': (('-', '-'), ('solid', 'solid'),
('--', '--'), ('dashed', 'dashed'),
('-.', '-.'), ('dashdot', 'dashdot'),
(':', ':'), ('dotted', 'dotted'),
('', ''), (' ', ' '),
('None', 'none'), ('none', 'none'),
('DoTtEd', 'dotted'), # case-insensitive
('1, 3', (0, (1, 3))),
([1.23, 456], (0, [1.23, 456.0])),
([1, 2, 3, 4], (0, [1.0, 2.0, 3.0, 4.0])),
((0, [1, 2]), (0, [1, 2])),
((-1, [1, 2]), (-1, [1, 2])),
),
'fail': (('aardvark', ValueError), # not a valid string
(b'dotted', ValueError),
('dotted'.encode('utf-16'), ValueError),
([1, 2, 3], ValueError), # sequence with odd length
(1.23, ValueError), # not a sequence
(("a", [1, 2]), ValueError), # wrong explicit offset
((None, [1, 2]), ValueError), # wrong explicit offset
((1, [1, 2, 3]), ValueError), # odd length sequence
(([1, 2], 1), ValueError), # inverted offset/onoff
)
},
)
for validator_dict in validation_tests:
validator = validator_dict['validator']
if valid:
for arg, target in validator_dict['success']:
yield validator, arg, target
else:
for arg, error_type in validator_dict['fail']:
yield validator, arg, error_type
@pytest.mark.parametrize('validator, arg, target',
generate_validator_testcases(True))
def test_validator_valid(validator, arg, target):
res = validator(arg)
if isinstance(target, np.ndarray):
np.testing.assert_equal(res, target)
elif not isinstance(target, Cycler):
assert res == target
else:
# Cyclers can't simply be asserted equal. They don't implement __eq__
assert list(res) == list(target)
@pytest.mark.parametrize('validator, arg, exception_type',
generate_validator_testcases(False))
def test_validator_invalid(validator, arg, exception_type):
with pytest.raises(exception_type):
validator(arg)
@pytest.mark.parametrize('weight, parsed_weight', [
('bold', 'bold'),
('BOLD', ValueError), # weight is case-sensitive
(100, 100),
('100', 100),
(np.array(100), 100),
# fractional fontweights are not defined. This should actually raise a
# ValueError, but historically did not.
(20.6, 20),
('20.6', ValueError),
([100], ValueError),
])
def test_validate_fontweight(weight, parsed_weight):
if parsed_weight is ValueError:
with pytest.raises(ValueError):
validate_fontweight(weight)
else:
assert validate_fontweight(weight) == parsed_weight
@pytest.mark.parametrize('stretch, parsed_stretch', [
('expanded', 'expanded'),
('EXPANDED', ValueError), # stretch is case-sensitive
(100, 100),
('100', 100),
(np.array(100), 100),
# fractional fontweights are not defined. This should actually raise a
# ValueError, but historically did not.
(20.6, 20),
('20.6', ValueError),
([100], ValueError),
])
def test_validate_fontstretch(stretch, parsed_stretch):
if parsed_stretch is ValueError:
with pytest.raises(ValueError):
validate_fontstretch(stretch)
else:
assert validate_fontstretch(stretch) == parsed_stretch
def test_keymaps():
key_list = [k for k in mpl.rcParams if 'keymap' in k]
for k in key_list:
assert isinstance(mpl.rcParams[k], list)
def test_no_backend_reset_rccontext():
assert mpl.rcParams['backend'] != 'module://aardvark'
with mpl.rc_context():
mpl.rcParams['backend'] = 'module://aardvark'
assert mpl.rcParams['backend'] == 'module://aardvark'
def test_rcparams_reset_after_fail():
# There was previously a bug that meant that if rc_context failed and
# raised an exception due to issues in the supplied rc parameters, the
# global rc parameters were left in a modified state.
with mpl.rc_context(rc={'text.usetex': False}):
assert mpl.rcParams['text.usetex'] is False
with pytest.raises(KeyError):
with mpl.rc_context(rc={'text.usetex': True, 'test.blah': True}):
pass
assert mpl.rcParams['text.usetex'] is False
@pytest.mark.skipif(sys.platform != "linux", reason="Linux only")
def test_backend_fallback_headless_invalid_backend(tmp_path):
env = {**os.environ,
"DISPLAY": "", "WAYLAND_DISPLAY": "",
"MPLBACKEND": "", "MPLCONFIGDIR": str(tmp_path)}
# plotting should fail with the tkagg backend selected in a headless environment
with pytest.raises(subprocess.CalledProcessError):
subprocess_run_for_testing(
[sys.executable, "-c",
"import matplotlib;"
"matplotlib.use('tkagg');"
"import matplotlib.pyplot;"
"matplotlib.pyplot.plot(42);"
],
env=env, check=True, stderr=subprocess.DEVNULL)
@pytest.mark.skipif(sys.platform != "linux", reason="Linux only")
def test_backend_fallback_headless_auto_backend(tmp_path):
# specify a headless mpl environment, but request a graphical (tk) backend
env = {**os.environ,
"DISPLAY": "", "WAYLAND_DISPLAY": "",
"MPLBACKEND": "TkAgg", "MPLCONFIGDIR": str(tmp_path)}
# allow fallback to an available interactive backend explicitly in configuration
rc_path = tmp_path / "matplotlibrc"
rc_path.write_text("backend_fallback: true")
# plotting should succeed, by falling back to use the generic agg backend
backend = subprocess_run_for_testing(
[sys.executable, "-c",
"import matplotlib.pyplot;"
"matplotlib.pyplot.plot(42);"
"print(matplotlib.get_backend());"
],
env=env, text=True, check=True, capture_output=True).stdout
assert backend.strip().lower() == "agg"
@pytest.mark.skipif(
sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(),
reason="headless")
def test_backend_fallback_headful(tmp_path):
if parse_version(pytest.__version__) >= parse_version('8.2.0'):
pytest_kwargs = dict(exc_type=ImportError)
else:
pytest_kwargs = {}
pytest.importorskip("tkinter", **pytest_kwargs)
env = {**os.environ, "MPLBACKEND": "", "MPLCONFIGDIR": str(tmp_path)}
backend = subprocess_run_for_testing(
[sys.executable, "-c",
"import matplotlib as mpl; "
"sentinel = mpl.rcsetup._auto_backend_sentinel; "
# Check that access on another instance does not resolve the sentinel.
"assert mpl.RcParams({'backend': sentinel})['backend'] == sentinel; "
"assert mpl.rcParams._get('backend') == sentinel; "
"assert mpl.get_backend(auto_select=False) is None; "
"import matplotlib.pyplot; "
"print(matplotlib.get_backend())"],
env=env, text=True, check=True, capture_output=True).stdout
# The actual backend will depend on what's installed, but at least tkagg is
# present.
assert backend.strip().lower() != "agg"
def test_deprecation(monkeypatch):
monkeypatch.setitem(
mpl._deprecated_map, "patch.linewidth",
("0.0", "axes.linewidth", lambda old: 2 * old, lambda new: new / 2))
with pytest.warns(mpl.MatplotlibDeprecationWarning):
assert mpl.rcParams["patch.linewidth"] \
== mpl.rcParams["axes.linewidth"] / 2
with pytest.warns(mpl.MatplotlibDeprecationWarning):
mpl.rcParams["patch.linewidth"] = 1
assert mpl.rcParams["axes.linewidth"] == 2
monkeypatch.setitem(
mpl._deprecated_ignore_map, "patch.edgecolor",
("0.0", "axes.edgecolor"))
with pytest.warns(mpl.MatplotlibDeprecationWarning):
assert mpl.rcParams["patch.edgecolor"] \
== mpl.rcParams["axes.edgecolor"]
with pytest.warns(mpl.MatplotlibDeprecationWarning):
mpl.rcParams["patch.edgecolor"] = "#abcd"
assert mpl.rcParams["axes.edgecolor"] != "#abcd"
monkeypatch.setitem(
mpl._deprecated_ignore_map, "patch.force_edgecolor",
("0.0", None))
with pytest.warns(mpl.MatplotlibDeprecationWarning):
assert mpl.rcParams["patch.force_edgecolor"] is None
monkeypatch.setitem(
mpl._deprecated_remain_as_none, "svg.hashsalt",
("0.0",))
with pytest.warns(mpl.MatplotlibDeprecationWarning):
mpl.rcParams["svg.hashsalt"] = "foobar"
assert mpl.rcParams["svg.hashsalt"] == "foobar" # Doesn't warn.
mpl.rcParams["svg.hashsalt"] = None # Doesn't warn.
mpl.rcParams.update(mpl.rcParams.copy()) # Doesn't warn.
# Note that the warning suppression actually arises from the
# iteration over the updater rcParams being protected by
# suppress_matplotlib_deprecation_warning, rather than any explicit check.
@pytest.mark.parametrize("value", [
"best",
1,
"1",
(0.9, .7),
(-0.9, .7),
"(0.9, .7)"
])
def test_rcparams_legend_loc(value):
# rcParams['legend.loc'] should allow any of the following formats.
# if any of these are not allowed, an exception will be raised
# test for gh issue #22338
mpl.rcParams["legend.loc"] = value
@pytest.mark.parametrize("value", [
"best",
1,
(0.9, .7),
(-0.9, .7),
])
def test_rcparams_legend_loc_from_file(tmp_path, value):
# rcParams['legend.loc'] should be settable from matplotlibrc.
# if any of these are not allowed, an exception will be raised.
# test for gh issue #22338
rc_path = tmp_path / "matplotlibrc"
rc_path.write_text(f"legend.loc: {value}")
with mpl.rc_context(fname=rc_path):
assert mpl.rcParams["legend.loc"] == value
@pytest.mark.parametrize("value", [(1, 2, 3), '1, 2, 3', '(1, 2, 3)'])
def test_validate_sketch(value):
mpl.rcParams["path.sketch"] = value
assert mpl.rcParams["path.sketch"] == (1, 2, 3)
assert validate_sketch(value) == (1, 2, 3)
@pytest.mark.parametrize("value", [1, '1', '1 2 3'])
def test_validate_sketch_error(value):
with pytest.raises(ValueError, match="scale, length, randomness"):
validate_sketch(value)
with pytest.raises(ValueError, match="scale, length, randomness"):
mpl.rcParams["path.sketch"] = value
@pytest.mark.parametrize("value", ['1, 2, 3', '(1,2,3)'])
def test_rcparams_path_sketch_from_file(tmp_path, value):
rc_path = tmp_path / "matplotlibrc"
rc_path.write_text(f"path.sketch: {value}")
with mpl.rc_context(fname=rc_path):
assert mpl.rcParams["path.sketch"] == (1, 2, 3)
@@ -0,0 +1,105 @@
import pytest
from numpy.testing import assert_allclose, assert_array_equal
from matplotlib.sankey import Sankey
from matplotlib.testing.decorators import check_figures_equal
def test_sankey():
# lets just create a sankey instance and check the code runs
sankey = Sankey()
sankey.add()
def test_label():
s = Sankey(flows=[0.25], labels=['First'], orientations=[-1])
assert s.diagrams[0].texts[0].get_text() == 'First\n0.25'
def test_format_using_callable():
# test using callable by slightly incrementing above label example
def show_three_decimal_places(value):
return f'{value:.3f}'
s = Sankey(flows=[0.25], labels=['First'], orientations=[-1],
format=show_three_decimal_places)
assert s.diagrams[0].texts[0].get_text() == 'First\n0.250'
@pytest.mark.parametrize('kwargs, msg', (
({'gap': -1}, "'gap' is negative"),
({'gap': 1, 'radius': 2}, "'radius' is greater than 'gap'"),
({'head_angle': -1}, "'head_angle' is negative"),
({'tolerance': -1}, "'tolerance' is negative"),
({'flows': [1, -1], 'orientations': [-1, 0, 1]},
r"The shapes of 'flows' \(2,\) and 'orientations'"),
({'flows': [1, -1], 'labels': ['a', 'b', 'c']},
r"The shapes of 'flows' \(2,\) and 'labels'"),
))
def test_sankey_errors(kwargs, msg):
with pytest.raises(ValueError, match=msg):
Sankey(**kwargs)
@pytest.mark.parametrize('kwargs, msg', (
({'trunklength': -1}, "'trunklength' is negative"),
({'flows': [0.2, 0.3], 'prior': 0}, "The scaled sum of the connected"),
({'prior': -1}, "The index of the prior diagram is negative"),
({'prior': 1}, "The index of the prior diagram is 1"),
({'connect': (-1, 1), 'prior': 0}, "At least one of the connection"),
({'connect': (2, 1), 'prior': 0}, "The connection index to the source"),
({'connect': (1, 3), 'prior': 0}, "The connection index to this dia"),
({'connect': (1, 1), 'prior': 0, 'flows': [-0.2, 0.2],
'orientations': [2]}, "The value of orientations"),
({'connect': (1, 1), 'prior': 0, 'flows': [-0.2, 0.2],
'pathlengths': [2]}, "The lengths of 'flows'"),
))
def test_sankey_add_errors(kwargs, msg):
sankey = Sankey()
with pytest.raises(ValueError, match=msg):
sankey.add(flows=[0.2, -0.2])
sankey.add(**kwargs)
def test_sankey2():
s = Sankey(flows=[0.25, -0.25, 0.5, -0.5], labels=['Foo'],
orientations=[-1], unit='Bar')
sf = s.finish()
assert_array_equal(sf[0].flows, [0.25, -0.25, 0.5, -0.5])
assert sf[0].angles == [1, 3, 1, 3]
assert all([text.get_text()[0:3] == 'Foo' for text in sf[0].texts])
assert all([text.get_text()[-3:] == 'Bar' for text in sf[0].texts])
assert sf[0].text.get_text() == ''
assert_allclose(sf[0].tips,
[(-1.375, -0.52011255),
(1.375, -0.75506044),
(-0.75, -0.41522509),
(0.75, -0.8599479)])
s = Sankey(flows=[0.25, -0.25, 0, 0.5, -0.5], labels=['Foo'],
orientations=[-1], unit='Bar')
sf = s.finish()
assert_array_equal(sf[0].flows, [0.25, -0.25, 0, 0.5, -0.5])
assert sf[0].angles == [1, 3, None, 1, 3]
assert_allclose(sf[0].tips,
[(-1.375, -0.52011255),
(1.375, -0.75506044),
(0, 0),
(-0.75, -0.41522509),
(0.75, -0.8599479)])
@check_figures_equal(extensions=['png'])
def test_sankey3(fig_test, fig_ref):
ax_test = fig_test.gca()
s_test = Sankey(ax=ax_test, flows=[0.25, -0.25, -0.25, 0.25, 0.5, -0.5],
orientations=[1, -1, 1, -1, 0, 0])
s_test.finish()
ax_ref = fig_ref.gca()
s_ref = Sankey(ax=ax_ref)
s_ref.add(flows=[0.25, -0.25, -0.25, 0.25, 0.5, -0.5],
orientations=[1, -1, 1, -1, 0, 0])
s_ref.finish()
@@ -0,0 +1,295 @@
import copy
import matplotlib.pyplot as plt
from matplotlib.scale import (
AsinhScale, AsinhTransform,
LogTransform, InvertedLogTransform,
SymmetricalLogTransform)
import matplotlib.scale as mscale
from matplotlib.ticker import AsinhLocator, LogFormatterSciNotation
from matplotlib.testing.decorators import check_figures_equal, image_comparison
import numpy as np
from numpy.testing import assert_allclose
import io
import pytest
@check_figures_equal(extensions=['png'])
def test_log_scales(fig_test, fig_ref):
ax_test = fig_test.add_subplot(122, yscale='log', xscale='symlog')
ax_test.axvline(24.1)
ax_test.axhline(24.1)
xlim = ax_test.get_xlim()
ylim = ax_test.get_ylim()
ax_ref = fig_ref.add_subplot(122, yscale='log', xscale='symlog')
ax_ref.set(xlim=xlim, ylim=ylim)
ax_ref.plot([24.1, 24.1], ylim, 'b')
ax_ref.plot(xlim, [24.1, 24.1], 'b')
def test_symlog_mask_nan():
# Use a transform round-trip to verify that the forward and inverse
# transforms work, and that they respect nans and/or masking.
slt = SymmetricalLogTransform(10, 2, 1)
slti = slt.inverted()
x = np.arange(-1.5, 5, 0.5)
out = slti.transform_non_affine(slt.transform_non_affine(x))
assert_allclose(out, x)
assert type(out) is type(x)
x[4] = np.nan
out = slti.transform_non_affine(slt.transform_non_affine(x))
assert_allclose(out, x)
assert type(out) is type(x)
x = np.ma.array(x)
out = slti.transform_non_affine(slt.transform_non_affine(x))
assert_allclose(out, x)
assert type(out) is type(x)
x[3] = np.ma.masked
out = slti.transform_non_affine(slt.transform_non_affine(x))
assert_allclose(out, x)
assert type(out) is type(x)
@image_comparison(['logit_scales.png'], remove_text=True)
def test_logit_scales():
fig, ax = plt.subplots()
# Typical extinction curve for logit
x = np.array([0.001, 0.003, 0.01, 0.03, 0.1, 0.2, 0.3, 0.4, 0.5,
0.6, 0.7, 0.8, 0.9, 0.97, 0.99, 0.997, 0.999])
y = 1.0 / x
ax.plot(x, y)
ax.set_xscale('logit')
ax.grid(True)
bbox = ax.get_tightbbox(fig.canvas.get_renderer())
assert np.isfinite(bbox.x0)
assert np.isfinite(bbox.y0)
def test_log_scatter():
"""Issue #1799"""
fig, ax = plt.subplots(1)
x = np.arange(10)
y = np.arange(10) - 1
ax.scatter(x, y)
buf = io.BytesIO()
fig.savefig(buf, format='pdf')
buf = io.BytesIO()
fig.savefig(buf, format='eps')
buf = io.BytesIO()
fig.savefig(buf, format='svg')
def test_logscale_subs():
fig, ax = plt.subplots()
ax.set_yscale('log', subs=np.array([2, 3, 4]))
# force draw
fig.canvas.draw()
@image_comparison(['logscale_mask.png'], remove_text=True)
def test_logscale_mask():
# Check that zero values are masked correctly on log scales.
# See github issue 8045
xs = np.linspace(0, 50, 1001)
fig, ax = plt.subplots()
ax.plot(np.exp(-xs**2))
fig.canvas.draw()
ax.set(yscale="log")
def test_extra_kwargs_raise():
fig, ax = plt.subplots()
for scale in ['linear', 'log', 'symlog']:
with pytest.raises(TypeError):
ax.set_yscale(scale, foo='mask')
def test_logscale_invert_transform():
fig, ax = plt.subplots()
ax.set_yscale('log')
# get transformation from data to axes
tform = (ax.transAxes + ax.transData.inverted()).inverted()
# direct test of log transform inversion
inverted_transform = LogTransform(base=2).inverted()
assert isinstance(inverted_transform, InvertedLogTransform)
assert inverted_transform.base == 2
def test_logscale_transform_repr():
fig, ax = plt.subplots()
ax.set_yscale('log')
repr(ax.transData)
repr(LogTransform(10, nonpositive='clip'))
@image_comparison(['logscale_nonpos_values.png'],
remove_text=True, tol=0.02, style='mpl20')
def test_logscale_nonpos_values():
np.random.seed(19680801)
xs = np.random.normal(size=int(1e3))
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2)
ax1.hist(xs, range=(-5, 5), bins=10)
ax1.set_yscale('log')
ax2.hist(xs, range=(-5, 5), bins=10)
ax2.set_yscale('log', nonpositive='mask')
xdata = np.arange(0, 10, 0.01)
ydata = np.exp(-xdata)
edata = 0.2*(10-xdata)*np.cos(5*xdata)*np.exp(-xdata)
ax3.fill_between(xdata, ydata - edata, ydata + edata)
ax3.set_yscale('log')
x = np.logspace(-1, 1)
y = x ** 3
yerr = x**2
ax4.errorbar(x, y, yerr=yerr)
ax4.set_yscale('log')
ax4.set_xscale('log')
def test_invalid_log_lims():
# Check that invalid log scale limits are ignored
fig, ax = plt.subplots()
ax.scatter(range(0, 4), range(0, 4))
ax.set_xscale('log')
original_xlim = ax.get_xlim()
with pytest.warns(UserWarning):
ax.set_xlim(left=0)
assert ax.get_xlim() == original_xlim
with pytest.warns(UserWarning):
ax.set_xlim(right=-1)
assert ax.get_xlim() == original_xlim
ax.set_yscale('log')
original_ylim = ax.get_ylim()
with pytest.warns(UserWarning):
ax.set_ylim(bottom=0)
assert ax.get_ylim() == original_ylim
with pytest.warns(UserWarning):
ax.set_ylim(top=-1)
assert ax.get_ylim() == original_ylim
@image_comparison(['function_scales.png'], remove_text=True, style='mpl20')
def test_function_scale():
def inverse(x):
return x**2
def forward(x):
return x**(1/2)
fig, ax = plt.subplots()
x = np.arange(1, 1000)
ax.plot(x, x)
ax.set_xscale('function', functions=(forward, inverse))
ax.set_xlim(1, 1000)
def test_pass_scale():
# test passing a scale object works...
fig, ax = plt.subplots()
scale = mscale.LogScale(axis=None)
ax.set_xscale(scale)
scale = mscale.LogScale(axis=None)
ax.set_yscale(scale)
assert ax.xaxis.get_scale() == 'log'
assert ax.yaxis.get_scale() == 'log'
def test_scale_deepcopy():
sc = mscale.LogScale(axis='x', base=10)
sc2 = copy.deepcopy(sc)
assert str(sc.get_transform()) == str(sc2.get_transform())
assert sc._transform is not sc2._transform
class TestAsinhScale:
def test_transforms(self):
a0 = 17.0
a = np.linspace(-50, 50, 100)
forward = AsinhTransform(a0)
inverse = forward.inverted()
invinv = inverse.inverted()
a_forward = forward.transform_non_affine(a)
a_inverted = inverse.transform_non_affine(a_forward)
assert_allclose(a_inverted, a)
a_invinv = invinv.transform_non_affine(a)
assert_allclose(a_invinv, a0 * np.arcsinh(a / a0))
def test_init(self):
fig, ax = plt.subplots()
s = AsinhScale(axis=None, linear_width=23.0)
assert s.linear_width == 23
assert s._base == 10
assert s._subs == (2, 5)
tx = s.get_transform()
assert isinstance(tx, AsinhTransform)
assert tx.linear_width == s.linear_width
def test_base_init(self):
fig, ax = plt.subplots()
s3 = AsinhScale(axis=None, base=3)
assert s3._base == 3
assert s3._subs == (2,)
s7 = AsinhScale(axis=None, base=7, subs=(2, 4))
assert s7._base == 7
assert s7._subs == (2, 4)
def test_fmtloc(self):
class DummyAxis:
def __init__(self):
self.fields = {}
def set(self, **kwargs):
self.fields.update(**kwargs)
def set_major_formatter(self, f):
self.fields['major_formatter'] = f
ax0 = DummyAxis()
s0 = AsinhScale(axis=ax0, base=0)
s0.set_default_locators_and_formatters(ax0)
assert isinstance(ax0.fields['major_locator'], AsinhLocator)
assert isinstance(ax0.fields['major_formatter'], str)
ax5 = DummyAxis()
s7 = AsinhScale(axis=ax5, base=5)
s7.set_default_locators_and_formatters(ax5)
assert isinstance(ax5.fields['major_locator'], AsinhLocator)
assert isinstance(ax5.fields['major_formatter'],
LogFormatterSciNotation)
def test_bad_scale(self):
fig, ax = plt.subplots()
with pytest.raises(ValueError):
AsinhScale(axis=None, linear_width=0)
with pytest.raises(ValueError):
AsinhScale(axis=None, linear_width=-1)
s0 = AsinhScale(axis=None, )
s1 = AsinhScale(axis=None, linear_width=3.0)
@@ -0,0 +1,571 @@
import base64
import io
import platform
import numpy as np
from numpy.testing import assert_array_almost_equal, assert_array_equal
import pytest
from matplotlib.testing.decorators import (
check_figures_equal, image_comparison, remove_ticks_and_titles)
import matplotlib.pyplot as plt
from matplotlib import patches, transforms
from matplotlib.path import Path
# NOTE: All of these tests assume that path.simplify is set to True
# (the default)
@image_comparison(['clipping'], remove_text=True)
def test_clipping():
t = np.arange(0.0, 2.0, 0.01)
s = np.sin(2*np.pi*t)
fig, ax = plt.subplots()
ax.plot(t, s, linewidth=1.0)
ax.set_ylim((-0.20, -0.28))
@image_comparison(['overflow'], remove_text=True,
tol=0 if platform.machine() == 'x86_64' else 0.007)
def test_overflow():
x = np.array([1.0, 2.0, 3.0, 2.0e5])
y = np.arange(len(x))
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_xlim(2, 6)
@image_comparison(['clipping_diamond'], remove_text=True)
def test_diamond():
x = np.array([0.0, 1.0, 0.0, -1.0, 0.0])
y = np.array([1.0, 0.0, -1.0, 0.0, 1.0])
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_xlim(-0.6, 0.6)
ax.set_ylim(-0.6, 0.6)
def test_clipping_out_of_bounds():
# Should work on a Path *without* codes.
path = Path([(0, 0), (1, 2), (2, 1)])
simplified = path.cleaned(clip=(10, 10, 20, 20))
assert_array_equal(simplified.vertices, [(0, 0)])
assert simplified.codes == [Path.STOP]
# Should work on a Path *with* codes, and no curves.
path = Path([(0, 0), (1, 2), (2, 1)],
[Path.MOVETO, Path.LINETO, Path.LINETO])
simplified = path.cleaned(clip=(10, 10, 20, 20))
assert_array_equal(simplified.vertices, [(0, 0)])
assert simplified.codes == [Path.STOP]
# A Path with curves does not do any clipping yet.
path = Path([(0, 0), (1, 2), (2, 3)],
[Path.MOVETO, Path.CURVE3, Path.CURVE3])
simplified = path.cleaned()
simplified_clipped = path.cleaned(clip=(10, 10, 20, 20))
assert_array_equal(simplified.vertices, simplified_clipped.vertices)
assert_array_equal(simplified.codes, simplified_clipped.codes)
def test_noise():
np.random.seed(0)
x = np.random.uniform(size=50000) * 50
fig, ax = plt.subplots()
p1 = ax.plot(x, solid_joinstyle='round', linewidth=2.0)
# Ensure that the path's transform takes the new axes limits into account.
fig.canvas.draw()
path = p1[0].get_path()
transform = p1[0].get_transform()
path = transform.transform_path(path)
simplified = path.cleaned(simplify=True)
assert simplified.vertices.size == 25512
def test_antiparallel_simplification():
def _get_simplified(x, y):
fig, ax = plt.subplots()
p1 = ax.plot(x, y)
path = p1[0].get_path()
transform = p1[0].get_transform()
path = transform.transform_path(path)
simplified = path.cleaned(simplify=True)
simplified = transform.inverted().transform_path(simplified)
return simplified
# test ending on a maximum
x = [0, 0, 0, 0, 0, 1]
y = [.5, 1, -1, 1, 2, .5]
simplified = _get_simplified(x, y)
assert_array_almost_equal([[0., 0.5],
[0., -1.],
[0., 2.],
[1., 0.5]],
simplified.vertices[:-2, :])
# test ending on a minimum
x = [0, 0, 0, 0, 0, 1]
y = [.5, 1, -1, 1, -2, .5]
simplified = _get_simplified(x, y)
assert_array_almost_equal([[0., 0.5],
[0., 1.],
[0., -2.],
[1., 0.5]],
simplified.vertices[:-2, :])
# test ending in between
x = [0, 0, 0, 0, 0, 1]
y = [.5, 1, -1, 1, 0, .5]
simplified = _get_simplified(x, y)
assert_array_almost_equal([[0., 0.5],
[0., 1.],
[0., -1.],
[0., 0.],
[1., 0.5]],
simplified.vertices[:-2, :])
# test no anti-parallel ending at max
x = [0, 0, 0, 0, 0, 1]
y = [.5, 1, 2, 1, 3, .5]
simplified = _get_simplified(x, y)
assert_array_almost_equal([[0., 0.5],
[0., 3.],
[1., 0.5]],
simplified.vertices[:-2, :])
# test no anti-parallel ending in middle
x = [0, 0, 0, 0, 0, 1]
y = [.5, 1, 2, 1, 1, .5]
simplified = _get_simplified(x, y)
assert_array_almost_equal([[0., 0.5],
[0., 2.],
[0., 1.],
[1., 0.5]],
simplified.vertices[:-2, :])
# Only consider angles in 0 <= angle <= pi/2, otherwise
# using min/max will get the expected results out of order:
# min/max for simplification code depends on original vector,
# and if angle is outside above range then simplification
# min/max will be opposite from actual min/max.
@pytest.mark.parametrize('angle', [0, np.pi/4, np.pi/3, np.pi/2])
@pytest.mark.parametrize('offset', [0, .5])
def test_angled_antiparallel(angle, offset):
scale = 5
np.random.seed(19680801)
# get 15 random offsets
# TODO: guarantee offset > 0 results in some offsets < 0
vert_offsets = (np.random.rand(15) - offset) * scale
# always start at 0 so rotation makes sense
vert_offsets[0] = 0
# always take the first step the same direction
vert_offsets[1] = 1
# compute points along a diagonal line
x = np.sin(angle) * vert_offsets
y = np.cos(angle) * vert_offsets
# will check these later
x_max = x[1:].max()
x_min = x[1:].min()
y_max = y[1:].max()
y_min = y[1:].min()
if offset > 0:
p_expected = Path([[0, 0],
[x_max, y_max],
[x_min, y_min],
[x[-1], y[-1]],
[0, 0]],
codes=[1, 2, 2, 2, 0])
else:
p_expected = Path([[0, 0],
[x_max, y_max],
[x[-1], y[-1]],
[0, 0]],
codes=[1, 2, 2, 0])
p = Path(np.vstack([x, y]).T)
p2 = p.cleaned(simplify=True)
assert_array_almost_equal(p_expected.vertices,
p2.vertices)
assert_array_equal(p_expected.codes, p2.codes)
def test_sine_plus_noise():
np.random.seed(0)
x = (np.sin(np.linspace(0, np.pi * 2.0, 50000)) +
np.random.uniform(size=50000) * 0.01)
fig, ax = plt.subplots()
p1 = ax.plot(x, solid_joinstyle='round', linewidth=2.0)
# Ensure that the path's transform takes the new axes limits into account.
fig.canvas.draw()
path = p1[0].get_path()
transform = p1[0].get_transform()
path = transform.transform_path(path)
simplified = path.cleaned(simplify=True)
assert simplified.vertices.size == 25240
@image_comparison(['simplify_curve'], remove_text=True, tol=0.017)
def test_simplify_curve():
pp1 = patches.PathPatch(
Path([(0, 0), (1, 0), (1, 1), (np.nan, 1), (0, 0), (2, 0), (2, 2),
(0, 0)],
[Path.MOVETO, Path.CURVE3, Path.CURVE3, Path.CURVE3, Path.CURVE3,
Path.CURVE3, Path.CURVE3, Path.CLOSEPOLY]),
fc="none")
fig, ax = plt.subplots()
ax.add_patch(pp1)
ax.set_xlim((0, 2))
ax.set_ylim((0, 2))
@check_figures_equal()
def test_closed_path_nan_removal(fig_test, fig_ref):
ax_test = fig_test.subplots(2, 2).flatten()
ax_ref = fig_ref.subplots(2, 2).flatten()
# NaN on the first point also removes the last point, because it's closed.
path = Path(
[[-3, np.nan], [3, -3], [3, 3], [-3, 3], [-3, -3]],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
ax_test[0].add_patch(patches.PathPatch(path, facecolor='none'))
path = Path(
[[-3, np.nan], [3, -3], [3, 3], [-3, 3], [-3, np.nan]],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO])
ax_ref[0].add_patch(patches.PathPatch(path, facecolor='none'))
# NaN on second-last point should not re-close.
path = Path(
[[-2, -2], [2, -2], [2, 2], [-2, np.nan], [-2, -2]],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
ax_test[0].add_patch(patches.PathPatch(path, facecolor='none'))
path = Path(
[[-2, -2], [2, -2], [2, 2], [-2, np.nan], [-2, -2]],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO])
ax_ref[0].add_patch(patches.PathPatch(path, facecolor='none'))
# Test multiple loops in a single path (with same paths as above).
path = Path(
[[-3, np.nan], [3, -3], [3, 3], [-3, 3], [-3, -3],
[-2, -2], [2, -2], [2, 2], [-2, np.nan], [-2, -2]],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY,
Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
ax_test[1].add_patch(patches.PathPatch(path, facecolor='none'))
path = Path(
[[-3, np.nan], [3, -3], [3, 3], [-3, 3], [-3, np.nan],
[-2, -2], [2, -2], [2, 2], [-2, np.nan], [-2, -2]],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO])
ax_ref[1].add_patch(patches.PathPatch(path, facecolor='none'))
# NaN in first point of CURVE3 should not re-close, and hide entire curve.
path = Path(
[[-1, -1], [1, -1], [1, np.nan], [0, 1], [-1, 1], [-1, -1]],
[Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO,
Path.CLOSEPOLY])
ax_test[2].add_patch(patches.PathPatch(path, facecolor='none'))
path = Path(
[[-1, -1], [1, -1], [1, np.nan], [0, 1], [-1, 1], [-1, -1]],
[Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO,
Path.CLOSEPOLY])
ax_ref[2].add_patch(patches.PathPatch(path, facecolor='none'))
# NaN in second point of CURVE3 should not re-close, and hide entire curve
# plus next line segment.
path = Path(
[[-3, -3], [3, -3], [3, 0], [0, np.nan], [-3, 3], [-3, -3]],
[Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO,
Path.LINETO])
ax_test[2].add_patch(patches.PathPatch(path, facecolor='none'))
path = Path(
[[-3, -3], [3, -3], [3, 0], [0, np.nan], [-3, 3], [-3, -3]],
[Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO,
Path.LINETO])
ax_ref[2].add_patch(patches.PathPatch(path, facecolor='none'))
# NaN in first point of CURVE4 should not re-close, and hide entire curve.
path = Path(
[[-1, -1], [1, -1], [1, np.nan], [0, 0], [0, 1], [-1, 1], [-1, -1]],
[Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
Path.LINETO, Path.CLOSEPOLY])
ax_test[3].add_patch(patches.PathPatch(path, facecolor='none'))
path = Path(
[[-1, -1], [1, -1], [1, np.nan], [0, 0], [0, 1], [-1, 1], [-1, -1]],
[Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
Path.LINETO, Path.CLOSEPOLY])
ax_ref[3].add_patch(patches.PathPatch(path, facecolor='none'))
# NaN in second point of CURVE4 should not re-close, and hide entire curve.
path = Path(
[[-2, -2], [2, -2], [2, 0], [0, np.nan], [0, 2], [-2, 2], [-2, -2]],
[Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
Path.LINETO, Path.LINETO])
ax_test[3].add_patch(patches.PathPatch(path, facecolor='none'))
path = Path(
[[-2, -2], [2, -2], [2, 0], [0, np.nan], [0, 2], [-2, 2], [-2, -2]],
[Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
Path.LINETO, Path.LINETO])
ax_ref[3].add_patch(patches.PathPatch(path, facecolor='none'))
# NaN in third point of CURVE4 should not re-close, and hide entire curve
# plus next line segment.
path = Path(
[[-3, -3], [3, -3], [3, 0], [0, 0], [0, np.nan], [-3, 3], [-3, -3]],
[Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
Path.LINETO, Path.LINETO])
ax_test[3].add_patch(patches.PathPatch(path, facecolor='none'))
path = Path(
[[-3, -3], [3, -3], [3, 0], [0, 0], [0, np.nan], [-3, 3], [-3, -3]],
[Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
Path.LINETO, Path.LINETO])
ax_ref[3].add_patch(patches.PathPatch(path, facecolor='none'))
# Keep everything clean.
for ax in [*ax_test.flat, *ax_ref.flat]:
ax.set(xlim=(-3.5, 3.5), ylim=(-3.5, 3.5))
remove_ticks_and_titles(fig_test)
remove_ticks_and_titles(fig_ref)
@check_figures_equal()
def test_closed_path_clipping(fig_test, fig_ref):
vertices = []
for roll in range(8):
offset = 0.1 * roll + 0.1
# A U-like pattern.
pattern = [
[-0.5, 1.5], [-0.5, -0.5], [1.5, -0.5], [1.5, 1.5], # Outer square
# With a notch in the top.
[1 - offset / 2, 1.5], [1 - offset / 2, offset],
[offset / 2, offset], [offset / 2, 1.5],
]
# Place the initial/final point anywhere in/out of the clipping area.
pattern = np.roll(pattern, roll, axis=0)
pattern = np.concatenate((pattern, pattern[:1, :]))
vertices.append(pattern)
# Multiple subpaths are used here to ensure they aren't broken by closed
# loop clipping.
codes = np.full(len(vertices[0]), Path.LINETO)
codes[0] = Path.MOVETO
codes[-1] = Path.CLOSEPOLY
codes = np.tile(codes, len(vertices))
vertices = np.concatenate(vertices)
fig_test.set_size_inches((5, 5))
path = Path(vertices, codes)
fig_test.add_artist(patches.PathPatch(path, facecolor='none'))
# For reference, we draw the same thing, but unclosed by using a line to
# the last point only.
fig_ref.set_size_inches((5, 5))
codes = codes.copy()
codes[codes == Path.CLOSEPOLY] = Path.LINETO
path = Path(vertices, codes)
fig_ref.add_artist(patches.PathPatch(path, facecolor='none'))
@image_comparison(['hatch_simplify'], remove_text=True)
def test_hatch():
fig, ax = plt.subplots()
ax.add_patch(plt.Rectangle((0, 0), 1, 1, fill=False, hatch="/"))
ax.set_xlim((0.45, 0.55))
ax.set_ylim((0.45, 0.55))
@image_comparison(['fft_peaks'], remove_text=True)
def test_fft_peaks():
fig, ax = plt.subplots()
t = np.arange(65536)
p1 = ax.plot(abs(np.fft.fft(np.sin(2*np.pi*.01*t)*np.blackman(len(t)))))
# Ensure that the path's transform takes the new axes limits into account.
fig.canvas.draw()
path = p1[0].get_path()
transform = p1[0].get_transform()
path = transform.transform_path(path)
simplified = path.cleaned(simplify=True)
assert simplified.vertices.size == 36
def test_start_with_moveto():
# Should be entirely clipped away to a single MOVETO
data = b"""
ZwAAAAku+v9UAQAA+Tj6/z8CAADpQ/r/KAMAANlO+v8QBAAAyVn6//UEAAC6ZPr/2gUAAKpv+v+8
BgAAm3r6/50HAACLhfr/ewgAAHyQ+v9ZCQAAbZv6/zQKAABepvr/DgsAAE+x+v/lCwAAQLz6/7wM
AAAxx/r/kA0AACPS+v9jDgAAFN36/zQPAAAF6Pr/AxAAAPfy+v/QEAAA6f36/5wRAADbCPv/ZhIA
AMwT+/8uEwAAvh77//UTAACwKfv/uRQAAKM0+/98FQAAlT/7/z0WAACHSvv//RYAAHlV+/+7FwAA
bGD7/3cYAABea/v/MRkAAFF2+//pGQAARIH7/6AaAAA3jPv/VRsAACmX+/8JHAAAHKL7/7ocAAAP
rfv/ah0AAAO4+/8YHgAA9sL7/8QeAADpzfv/bx8AANzY+/8YIAAA0OP7/78gAADD7vv/ZCEAALf5
+/8IIgAAqwT8/6kiAACeD/z/SiMAAJIa/P/oIwAAhiX8/4QkAAB6MPz/HyUAAG47/P+4JQAAYkb8
/1AmAABWUfz/5SYAAEpc/P95JwAAPmf8/wsoAAAzcvz/nCgAACd9/P8qKQAAHIj8/7cpAAAQk/z/
QyoAAAWe/P/MKgAA+aj8/1QrAADus/z/2isAAOO+/P9eLAAA2Mn8/+AsAADM1Pz/YS0AAMHf/P/g
LQAAtur8/10uAACr9fz/2C4AAKEA/f9SLwAAlgv9/8ovAACLFv3/QDAAAIAh/f+1MAAAdSz9/ycx
AABrN/3/mDEAAGBC/f8IMgAAVk39/3UyAABLWP3/4TIAAEFj/f9LMwAANm79/7MzAAAsef3/GjQA
ACKE/f9+NAAAF4/9/+E0AAANmv3/QzUAAAOl/f+iNQAA+a/9/wA2AADvuv3/XDYAAOXF/f+2NgAA
29D9/w83AADR2/3/ZjcAAMfm/f+7NwAAvfH9/w44AACz/P3/XzgAAKkH/v+vOAAAnxL+//04AACW
Hf7/SjkAAIwo/v+UOQAAgjP+/905AAB5Pv7/JDoAAG9J/v9pOgAAZVT+/606AABcX/7/7zoAAFJq
/v8vOwAASXX+/207AAA/gP7/qjsAADaL/v/lOwAALZb+/x48AAAjof7/VTwAABqs/v+LPAAAELf+
/788AAAHwv7/8TwAAP7M/v8hPQAA9df+/1A9AADr4v7/fT0AAOLt/v+oPQAA2fj+/9E9AADQA///
+T0AAMYO//8fPgAAvRn//0M+AAC0JP//ZT4AAKsv//+GPgAAojr//6U+AACZRf//wj4AAJBQ///d
PgAAh1v///c+AAB+Zv//Dz8AAHRx//8lPwAAa3z//zk/AABih///TD8AAFmS//9dPwAAUJ3//2w/
AABHqP//ej8AAD6z//+FPwAANb7//48/AAAsyf//lz8AACPU//+ePwAAGt///6M/AAAR6v//pj8A
AAj1//+nPwAA/////w=="""
verts = np.frombuffer(base64.decodebytes(data), dtype='<i4')
verts = verts.reshape((len(verts) // 2, 2))
path = Path(verts)
segs = path.iter_segments(transforms.IdentityTransform(),
clip=(0.0, 0.0, 100.0, 100.0))
segs = list(segs)
assert len(segs) == 1
assert segs[0][1] == Path.MOVETO
def test_throw_rendering_complexity_exceeded():
plt.rcParams['path.simplify'] = False
xx = np.arange(2_000_000)
yy = np.random.rand(2_000_000)
yy[1000] = np.nan
fig, ax = plt.subplots()
ax.plot(xx, yy)
with pytest.raises(OverflowError):
fig.savefig(io.BytesIO())
@image_comparison(['clipper_edge'], remove_text=True)
def test_clipper():
dat = (0, 1, 0, 2, 0, 3, 0, 4, 0, 5)
fig = plt.figure(figsize=(2, 1))
fig.subplots_adjust(left=0, bottom=0, wspace=0, hspace=0)
ax = fig.add_axes((0, 0, 1.0, 1.0), ylim=(0, 5), autoscale_on=False)
ax.plot(dat)
ax.xaxis.set_major_locator(plt.MultipleLocator(1))
ax.yaxis.set_major_locator(plt.MultipleLocator(1))
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
ax.set_xlim(5, 9)
@image_comparison(['para_equal_perp'], remove_text=True)
def test_para_equal_perp():
x = np.array([0, 1, 2, 1, 0, -1, 0, 1] + [1] * 128)
y = np.array([1, 1, 2, 1, 0, -1, 0, 0] + [0] * 128)
fig, ax = plt.subplots()
ax.plot(x + 1, y + 1)
ax.plot(x + 1, y + 1, 'ro')
@image_comparison(['clipping_with_nans'])
def test_clipping_with_nans():
x = np.linspace(0, 3.14 * 2, 3000)
y = np.sin(x)
x[::100] = np.nan
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_ylim(-0.25, 0.25)
def test_clipping_full():
p = Path([[1e30, 1e30]] * 5)
simplified = list(p.iter_segments(clip=[0, 0, 100, 100]))
assert simplified == []
p = Path([[50, 40], [75, 65]], [1, 2])
simplified = list(p.iter_segments(clip=[0, 0, 100, 100]))
assert ([(list(x), y) for x, y in simplified] ==
[([50, 40], 1), ([75, 65], 2)])
p = Path([[50, 40]], [1])
simplified = list(p.iter_segments(clip=[0, 0, 100, 100]))
assert ([(list(x), y) for x, y in simplified] ==
[([50, 40], 1)])
def test_simplify_closepoly():
# The values of the vertices in a CLOSEPOLY should always be ignored,
# in favor of the most recent MOVETO's vertex values
paths = [Path([(1, 1), (2, 1), (2, 2), (np.nan, np.nan)],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]),
Path([(1, 1), (2, 1), (2, 2), (40, 50)],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])]
expected_path = Path([(1, 1), (2, 1), (2, 2), (1, 1), (1, 1), (0, 0)],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
Path.LINETO, Path.STOP])
for path in paths:
simplified_path = path.cleaned(simplify=True)
assert_array_equal(expected_path.vertices, simplified_path.vertices)
assert_array_equal(expected_path.codes, simplified_path.codes)
# test that a compound path also works
path = Path([(1, 1), (2, 1), (2, 2), (np.nan, np.nan),
(-1, 0), (-2, 0), (-2, 1), (np.nan, np.nan)],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY,
Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
expected_path = Path([(1, 1), (2, 1), (2, 2), (1, 1),
(-1, 0), (-2, 0), (-2, 1), (-1, 0), (-1, 0), (0, 0)],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
Path.LINETO, Path.STOP])
simplified_path = path.cleaned(simplify=True)
assert_array_equal(expected_path.vertices, simplified_path.vertices)
assert_array_equal(expected_path.codes, simplified_path.codes)
# test for a path with an invalid MOVETO
# CLOSEPOLY with an invalid MOVETO should be ignored
path = Path([(1, 0), (1, -1), (2, -1),
(np.nan, np.nan), (-1, -1), (-2, 1), (-1, 1),
(2, 2), (0, -1)],
[Path.MOVETO, Path.LINETO, Path.LINETO,
Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
Path.CLOSEPOLY, Path.LINETO])
expected_path = Path([(1, 0), (1, -1), (2, -1),
(np.nan, np.nan), (-1, -1), (-2, 1), (-1, 1),
(0, -1), (0, -1), (0, 0)],
[Path.MOVETO, Path.LINETO, Path.LINETO,
Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
Path.LINETO, Path.LINETO, Path.STOP])
simplified_path = path.cleaned(simplify=True)
assert_array_equal(expected_path.vertices, simplified_path.vertices)
assert_array_equal(expected_path.codes, simplified_path.codes)
@@ -0,0 +1,170 @@
"""
Testing that skewed Axes properly work.
"""
from contextlib import ExitStack
import itertools
import platform
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import image_comparison
from matplotlib.axes import Axes
import matplotlib.transforms as transforms
import matplotlib.axis as maxis
import matplotlib.spines as mspines
import matplotlib.patches as mpatch
from matplotlib.projections import register_projection
# The sole purpose of this class is to look at the upper, lower, or total
# interval as appropriate and see what parts of the tick to draw, if any.
class SkewXTick(maxis.XTick):
def draw(self, renderer):
with ExitStack() as stack:
for artist in [self.gridline, self.tick1line, self.tick2line,
self.label1, self.label2]:
stack.callback(artist.set_visible, artist.get_visible())
needs_lower = transforms.interval_contains(
self.axes.lower_xlim, self.get_loc())
needs_upper = transforms.interval_contains(
self.axes.upper_xlim, self.get_loc())
self.tick1line.set_visible(
self.tick1line.get_visible() and needs_lower)
self.label1.set_visible(
self.label1.get_visible() and needs_lower)
self.tick2line.set_visible(
self.tick2line.get_visible() and needs_upper)
self.label2.set_visible(
self.label2.get_visible() and needs_upper)
super().draw(renderer)
def get_view_interval(self):
return self.axes.xaxis.get_view_interval()
# This class exists to provide two separate sets of intervals to the tick,
# as well as create instances of the custom tick
class SkewXAxis(maxis.XAxis):
def _get_tick(self, major):
return SkewXTick(self.axes, None, major=major)
def get_view_interval(self):
return self.axes.upper_xlim[0], self.axes.lower_xlim[1]
# This class exists to calculate the separate data range of the
# upper X-axis and draw the spine there. It also provides this range
# to the X-axis artist for ticking and gridlines
class SkewSpine(mspines.Spine):
def _adjust_location(self):
pts = self._path.vertices
if self.spine_type == 'top':
pts[:, 0] = self.axes.upper_xlim
else:
pts[:, 0] = self.axes.lower_xlim
# This class handles registration of the skew-xaxes as a projection as well
# as setting up the appropriate transformations. It also overrides standard
# spines and axes instances as appropriate.
class SkewXAxes(Axes):
# The projection must specify a name. This will be used be the
# user to select the projection, i.e. ``subplot(projection='skewx')``.
name = 'skewx'
def _init_axis(self):
# Taken from Axes and modified to use our modified X-axis
self.xaxis = SkewXAxis(self)
self.spines.top.register_axis(self.xaxis)
self.spines.bottom.register_axis(self.xaxis)
self.yaxis = maxis.YAxis(self)
self.spines.left.register_axis(self.yaxis)
self.spines.right.register_axis(self.yaxis)
def _gen_axes_spines(self):
spines = {'top': SkewSpine.linear_spine(self, 'top'),
'bottom': mspines.Spine.linear_spine(self, 'bottom'),
'left': mspines.Spine.linear_spine(self, 'left'),
'right': mspines.Spine.linear_spine(self, 'right')}
return spines
def _set_lim_and_transforms(self):
"""
This is called once when the plot is created to set up all the
transforms for the data, text and grids.
"""
rot = 30
# Get the standard transform setup from the Axes base class
super()._set_lim_and_transforms()
# Need to put the skew in the middle, after the scale and limits,
# but before the transAxes. This way, the skew is done in Axes
# coordinates thus performing the transform around the proper origin
# We keep the pre-transAxes transform around for other users, like the
# spines for finding bounds
self.transDataToAxes = (self.transScale +
(self.transLimits +
transforms.Affine2D().skew_deg(rot, 0)))
# Create the full transform from Data to Pixels
self.transData = self.transDataToAxes + self.transAxes
# Blended transforms like this need to have the skewing applied using
# both axes, in axes coords like before.
self._xaxis_transform = (transforms.blended_transform_factory(
self.transScale + self.transLimits,
transforms.IdentityTransform()) +
transforms.Affine2D().skew_deg(rot, 0)) + self.transAxes
@property
def lower_xlim(self):
return self.axes.viewLim.intervalx
@property
def upper_xlim(self):
pts = [[0., 1.], [1., 1.]]
return self.transDataToAxes.inverted().transform(pts)[:, 0]
# Now register the projection with matplotlib so the user can select
# it.
register_projection(SkewXAxes)
@image_comparison(['skew_axes.png'], remove_text=True)
def test_set_line_coll_dash_image():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection='skewx')
ax.set_xlim(-50, 50)
ax.set_ylim(50, -50)
ax.grid(True)
# An example of a slanted line at constant X
ax.axvline(0, color='b')
@image_comparison(['skew_rects.png'], remove_text=True,
tol=0 if platform.machine() == 'x86_64' else 0.009)
def test_skew_rectangle():
fix, axes = plt.subplots(5, 5, sharex=True, sharey=True, figsize=(8, 8))
axes = axes.flat
rotations = list(itertools.product([-3, -1, 0, 1, 3], repeat=2))
axes[0].set_xlim([-3, 3])
axes[0].set_ylim([-3, 3])
axes[0].set_aspect('equal', share=True)
for ax, (xrots, yrots) in zip(axes, rotations):
xdeg, ydeg = 45 * xrots, 45 * yrots
t = transforms.Affine2D().skew_deg(xdeg, ydeg)
ax.set_title(f'Skew of {xdeg} in X and {ydeg} in Y')
ax.add_patch(mpatch.Rectangle([-1, -1], 2, 2,
transform=t + ax.transData,
alpha=0.5, facecolor='coral'))
plt.subplots_adjust(wspace=0, left=0.01, right=0.99, bottom=0.01, top=0.99)
@@ -0,0 +1,227 @@
"""Tests for tinypages build using sphinx extensions."""
import filecmp
import os
from pathlib import Path
import shutil
import sys
from matplotlib.testing import subprocess_run_for_testing
import pytest
pytest.importorskip('sphinx', minversion='4.1.3')
def build_sphinx_html(source_dir, doctree_dir, html_dir, extra_args=None):
# Build the pages with warnings turned into errors
extra_args = [] if extra_args is None else extra_args
cmd = [sys.executable, '-msphinx', '-W', '-b', 'html',
'-d', str(doctree_dir), str(source_dir), str(html_dir), *extra_args]
proc = subprocess_run_for_testing(
cmd, capture_output=True, text=True,
env={**os.environ, "MPLBACKEND": ""})
out = proc.stdout
err = proc.stderr
assert proc.returncode == 0, \
f"sphinx build failed with stdout:\n{out}\nstderr:\n{err}\n"
if err:
pytest.fail(f"sphinx build emitted the following warnings:\n{err}")
assert html_dir.is_dir()
def test_tinypages(tmp_path):
shutil.copytree(Path(__file__).parent / 'tinypages', tmp_path,
dirs_exist_ok=True)
html_dir = tmp_path / '_build' / 'html'
img_dir = html_dir / '_images'
doctree_dir = tmp_path / 'doctrees'
# Build the pages with warnings turned into errors
cmd = [sys.executable, '-msphinx', '-W', '-b', 'html',
'-d', str(doctree_dir),
str(Path(__file__).parent / 'tinypages'), str(html_dir)]
# On CI, gcov emits warnings (due to agg headers being included with the
# same name in multiple extension modules -- but we don't care about their
# coverage anyways); hide them using GCOV_ERROR_FILE.
proc = subprocess_run_for_testing(
cmd, capture_output=True, text=True,
env={**os.environ, "MPLBACKEND": "", "GCOV_ERROR_FILE": os.devnull}
)
out = proc.stdout
err = proc.stderr
# Build the pages with warnings turned into errors
build_sphinx_html(tmp_path, doctree_dir, html_dir)
def plot_file(num):
return img_dir / f'some_plots-{num}.png'
def plot_directive_file(num):
# This is always next to the doctree dir.
return doctree_dir.parent / 'plot_directive' / f'some_plots-{num}.png'
range_10, range_6, range_4 = (plot_file(i) for i in range(1, 4))
# Plot 5 is range(6) plot
assert filecmp.cmp(range_6, plot_file(5))
# Plot 7 is range(4) plot
assert filecmp.cmp(range_4, plot_file(7))
# Plot 11 is range(10) plot
assert filecmp.cmp(range_10, plot_file(11))
# Plot 12 uses the old range(10) figure and the new range(6) figure
assert filecmp.cmp(range_10, plot_file('12_00'))
assert filecmp.cmp(range_6, plot_file('12_01'))
# Plot 13 shows close-figs in action
assert filecmp.cmp(range_4, plot_file(13))
# Plot 14 has included source
html_contents = (html_dir / 'some_plots.html').read_text(encoding='utf-8')
assert '# Only a comment' in html_contents
# check plot defined in external file.
assert filecmp.cmp(range_4, img_dir / 'range4.png')
assert filecmp.cmp(range_6, img_dir / 'range6_range6.png')
# check if figure caption made it into html file
assert 'This is the caption for plot 15.' in html_contents
# check if figure caption using :caption: made it into html file (because this plot
# doesn't use srcset, the caption preserves newlines in the output.)
assert 'Plot 17 uses the caption option,\nwith multi-line input.' in html_contents
# check if figure alt text using :alt: made it into html file
assert 'Plot 17 uses the alt option, with multi-line input.' in html_contents
# check if figure caption made it into html file
assert 'This is the caption for plot 18.' in html_contents
# check if the custom classes made it into the html file
assert 'plot-directive my-class my-other-class' in html_contents
# check that the multi-image caption is applied twice
assert html_contents.count('This caption applies to both plots.') == 2
# Plot 21 is range(6) plot via an include directive. But because some of
# the previous plots are repeated, the argument to plot_file() is only 17.
assert filecmp.cmp(range_6, plot_file(17))
# plot 22 is from the range6.py file again, but a different function
assert filecmp.cmp(range_10, img_dir / 'range6_range10.png')
# Modify the included plot
contents = (tmp_path / 'included_plot_21.rst').read_bytes()
contents = contents.replace(b'plt.plot(range(6))', b'plt.plot(range(4))')
(tmp_path / 'included_plot_21.rst').write_bytes(contents)
# Build the pages again and check that the modified file was updated
modification_times = [plot_directive_file(i).stat().st_mtime
for i in (1, 2, 3, 5)]
build_sphinx_html(tmp_path, doctree_dir, html_dir)
assert filecmp.cmp(range_4, plot_file(17))
# Check that the plots in the plot_directive folder weren't changed.
# (plot_directive_file(1) won't be modified, but it will be copied to html/
# upon compilation, so plot_file(1) will be modified)
assert plot_directive_file(1).stat().st_mtime == modification_times[0]
assert plot_directive_file(2).stat().st_mtime == modification_times[1]
assert plot_directive_file(3).stat().st_mtime == modification_times[2]
assert filecmp.cmp(range_10, plot_file(1))
assert filecmp.cmp(range_6, plot_file(2))
assert filecmp.cmp(range_4, plot_file(3))
# Make sure that figures marked with context are re-created (but that the
# contents are the same)
assert plot_directive_file(5).stat().st_mtime > modification_times[3]
assert filecmp.cmp(range_6, plot_file(5))
def test_plot_html_show_source_link(tmp_path):
parent = Path(__file__).parent
shutil.copyfile(parent / 'tinypages/conf.py', tmp_path / 'conf.py')
shutil.copytree(parent / 'tinypages/_static', tmp_path / '_static')
doctree_dir = tmp_path / 'doctrees'
(tmp_path / 'index.rst').write_text("""
.. plot::
plt.plot(range(2))
""")
# Make sure source scripts are created by default
html_dir1 = tmp_path / '_build' / 'html1'
build_sphinx_html(tmp_path, doctree_dir, html_dir1)
assert len(list(html_dir1.glob("**/index-1.py"))) == 1
# Make sure source scripts are NOT created when
# plot_html_show_source_link` is False
html_dir2 = tmp_path / '_build' / 'html2'
build_sphinx_html(tmp_path, doctree_dir, html_dir2,
extra_args=['-D', 'plot_html_show_source_link=0'])
assert len(list(html_dir2.glob("**/index-1.py"))) == 0
@pytest.mark.parametrize('plot_html_show_source_link', [0, 1])
def test_show_source_link_true(tmp_path, plot_html_show_source_link):
# Test that a source link is generated if :show-source-link: is true,
# whether or not plot_html_show_source_link is true.
parent = Path(__file__).parent
shutil.copyfile(parent / 'tinypages/conf.py', tmp_path / 'conf.py')
shutil.copytree(parent / 'tinypages/_static', tmp_path / '_static')
doctree_dir = tmp_path / 'doctrees'
(tmp_path / 'index.rst').write_text("""
.. plot::
:show-source-link: true
plt.plot(range(2))
""")
html_dir = tmp_path / '_build' / 'html'
build_sphinx_html(tmp_path, doctree_dir, html_dir, extra_args=[
'-D', f'plot_html_show_source_link={plot_html_show_source_link}'])
assert len(list(html_dir.glob("**/index-1.py"))) == 1
@pytest.mark.parametrize('plot_html_show_source_link', [0, 1])
def test_show_source_link_false(tmp_path, plot_html_show_source_link):
# Test that a source link is NOT generated if :show-source-link: is false,
# whether or not plot_html_show_source_link is true.
parent = Path(__file__).parent
shutil.copyfile(parent / 'tinypages/conf.py', tmp_path / 'conf.py')
shutil.copytree(parent / 'tinypages/_static', tmp_path / '_static')
doctree_dir = tmp_path / 'doctrees'
(tmp_path / 'index.rst').write_text("""
.. plot::
:show-source-link: false
plt.plot(range(2))
""")
html_dir = tmp_path / '_build' / 'html'
build_sphinx_html(tmp_path, doctree_dir, html_dir, extra_args=[
'-D', f'plot_html_show_source_link={plot_html_show_source_link}'])
assert len(list(html_dir.glob("**/index-1.py"))) == 0
def test_srcset_version(tmp_path):
shutil.copytree(Path(__file__).parent / 'tinypages', tmp_path,
dirs_exist_ok=True)
html_dir = tmp_path / '_build' / 'html'
img_dir = html_dir / '_images'
doctree_dir = tmp_path / 'doctrees'
build_sphinx_html(tmp_path, doctree_dir, html_dir, extra_args=[
'-D', 'plot_srcset=2x'])
def plot_file(num, suff=''):
return img_dir / f'some_plots-{num}{suff}.png'
# check some-plots
for ind in [1, 2, 3, 5, 7, 11, 13, 15, 17]:
assert plot_file(ind).exists()
assert plot_file(ind, suff='.2x').exists()
assert (img_dir / 'nestedpage-index-1.png').exists()
assert (img_dir / 'nestedpage-index-1.2x.png').exists()
assert (img_dir / 'nestedpage-index-2.png').exists()
assert (img_dir / 'nestedpage-index-2.2x.png').exists()
assert (img_dir / 'nestedpage2-index-1.png').exists()
assert (img_dir / 'nestedpage2-index-1.2x.png').exists()
assert (img_dir / 'nestedpage2-index-2.png').exists()
assert (img_dir / 'nestedpage2-index-2.2x.png').exists()
# Check html for srcset
assert ('srcset="_images/some_plots-1.png, _images/some_plots-1.2x.png 2.00x"'
in (html_dir / 'some_plots.html').read_text(encoding='utf-8'))
st = ('srcset="../_images/nestedpage-index-1.png, '
'../_images/nestedpage-index-1.2x.png 2.00x"')
assert st in (html_dir / 'nestedpage/index.html').read_text(encoding='utf-8')
st = ('srcset="../_images/nestedpage2-index-2.png, '
'../_images/nestedpage2-index-2.2x.png 2.00x"')
assert st in (html_dir / 'nestedpage2/index.html').read_text(encoding='utf-8')
@@ -0,0 +1,168 @@
import numpy as np
import pytest
import matplotlib.pyplot as plt
from matplotlib.spines import Spines
from matplotlib.testing.decorators import check_figures_equal, image_comparison
def test_spine_class():
"""Test Spines and SpinesProxy in isolation."""
class SpineMock:
def __init__(self):
self.val = None
def set(self, **kwargs):
vars(self).update(kwargs)
def set_val(self, val):
self.val = val
spines_dict = {
'left': SpineMock(),
'right': SpineMock(),
'top': SpineMock(),
'bottom': SpineMock(),
}
spines = Spines(**spines_dict)
assert spines['left'] is spines_dict['left']
assert spines.left is spines_dict['left']
spines[['left', 'right']].set_val('x')
assert spines.left.val == 'x'
assert spines.right.val == 'x'
assert spines.top.val is None
assert spines.bottom.val is None
spines[:].set_val('y')
assert all(spine.val == 'y' for spine in spines.values())
spines[:].set(foo='bar')
assert all(spine.foo == 'bar' for spine in spines.values())
with pytest.raises(AttributeError, match='foo'):
spines.foo
with pytest.raises(KeyError, match='foo'):
spines['foo']
with pytest.raises(KeyError, match='foo, bar'):
spines[['left', 'foo', 'right', 'bar']]
with pytest.raises(ValueError, match='single list'):
spines['left', 'right']
with pytest.raises(ValueError, match='Spines does not support slicing'):
spines['left':'right']
with pytest.raises(ValueError, match='Spines does not support slicing'):
spines['top':]
@image_comparison(['spines_axes_positions.png'])
def test_spines_axes_positions():
# SF bug 2852168
fig = plt.figure()
x = np.linspace(0, 2*np.pi, 100)
y = 2*np.sin(x)
ax = fig.add_subplot(1, 1, 1)
ax.set_title('centered spines')
ax.plot(x, y)
ax.spines.right.set_position(('axes', 0.1))
ax.yaxis.set_ticks_position('right')
ax.spines.top.set_position(('axes', 0.25))
ax.xaxis.set_ticks_position('top')
ax.spines.left.set_color('none')
ax.spines.bottom.set_color('none')
@image_comparison(['spines_data_positions.png'])
def test_spines_data_positions():
fig, ax = plt.subplots()
ax.spines.left.set_position(('data', -1.5))
ax.spines.top.set_position(('data', 0.5))
ax.spines.right.set_position(('data', -0.5))
ax.spines.bottom.set_position('zero')
ax.set_xlim([-2, 2])
ax.set_ylim([-2, 2])
@check_figures_equal(extensions=["png"])
def test_spine_nonlinear_data_positions(fig_test, fig_ref):
plt.style.use("default")
ax = fig_test.add_subplot()
ax.set(xscale="log", xlim=(.1, 1))
# Use position="data" to visually swap the left and right spines, using
# linewidth to distinguish them. The calls to tick_params removes labels
# (for image comparison purposes) and harmonizes tick positions with the
# reference).
ax.spines.left.set_position(("data", 1))
ax.spines.left.set_linewidth(2)
ax.spines.right.set_position(("data", .1))
ax.tick_params(axis="y", labelleft=False, direction="in")
ax = fig_ref.add_subplot()
ax.set(xscale="log", xlim=(.1, 1))
ax.spines.right.set_linewidth(2)
ax.tick_params(axis="y", labelleft=False, left=False, right=True)
@image_comparison(['spines_capstyle.png'])
def test_spines_capstyle():
# issue 2542
plt.rc('axes', linewidth=20)
fig, ax = plt.subplots()
ax.set_xticks([])
ax.set_yticks([])
def test_label_without_ticks():
fig, ax = plt.subplots()
plt.subplots_adjust(left=0.3, bottom=0.3)
ax.plot(np.arange(10))
ax.yaxis.set_ticks_position('left')
ax.spines.left.set_position(('outward', 30))
ax.spines.right.set_visible(False)
ax.set_ylabel('y label')
ax.xaxis.set_ticks_position('bottom')
ax.spines.bottom.set_position(('outward', 30))
ax.spines.top.set_visible(False)
ax.set_xlabel('x label')
ax.xaxis.set_ticks([])
ax.yaxis.set_ticks([])
plt.draw()
spine = ax.spines.left
spinebbox = spine.get_transform().transform_path(
spine.get_path()).get_extents()
assert ax.yaxis.label.get_position()[0] < spinebbox.xmin, \
"Y-Axis label not left of the spine"
spine = ax.spines.bottom
spinebbox = spine.get_transform().transform_path(
spine.get_path()).get_extents()
assert ax.xaxis.label.get_position()[1] < spinebbox.ymin, \
"X-Axis label not below the spine"
@image_comparison(['black_axes.png'])
def test_spines_black_axes():
# GitHub #18804
plt.rcParams["savefig.pad_inches"] = 0
plt.rcParams["savefig.bbox"] = 'tight'
fig = plt.figure(0, figsize=(4, 4))
ax = fig.add_axes((0, 0, 1, 1))
ax.set_xticklabels([])
ax.set_yticklabels([])
ax.set_xticks([])
ax.set_yticks([])
ax.set_facecolor((0, 0, 0))
def test_arc_spine_inner_no_axis():
# Backcompat: smoke test that inner arc spine does not need a registered
# axis in order to be drawn
fig = plt.figure()
ax = fig.add_subplot(projection="polar")
inner_spine = ax.spines["inner"]
inner_spine.register_axis(None)
assert ax.spines["inner"].axis is None
fig.draw_without_rendering()
@@ -0,0 +1,169 @@
import numpy as np
from numpy.testing import assert_array_almost_equal
import pytest
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import image_comparison
import matplotlib.transforms as mtransforms
def velocity_field():
Y, X = np.mgrid[-3:3:100j, -3:3:200j]
U = -1 - X**2 + Y
V = 1 + X - Y**2
return X, Y, U, V
def swirl_velocity_field():
x = np.linspace(-3., 3., 200)
y = np.linspace(-3., 3., 100)
X, Y = np.meshgrid(x, y)
a = 0.1
U = np.cos(a) * (-Y) - np.sin(a) * X
V = np.sin(a) * (-Y) + np.cos(a) * X
return x, y, U, V
@image_comparison(['streamplot_startpoints'], remove_text=True, style='mpl20',
extensions=['png'])
def test_startpoints():
X, Y, U, V = velocity_field()
start_x, start_y = np.meshgrid(np.linspace(X.min(), X.max(), 5),
np.linspace(Y.min(), Y.max(), 5))
start_points = np.column_stack([start_x.ravel(), start_y.ravel()])
plt.streamplot(X, Y, U, V, start_points=start_points)
plt.plot(start_x, start_y, 'ok')
@image_comparison(['streamplot_colormap.png'], remove_text=True, style='mpl20',
tol=0.022)
def test_colormap():
X, Y, U, V = velocity_field()
plt.streamplot(X, Y, U, V, color=U, density=0.6, linewidth=2,
cmap=plt.cm.autumn)
plt.colorbar()
@image_comparison(['streamplot_linewidth'], remove_text=True, style='mpl20',
tol=0.004)
def test_linewidth():
X, Y, U, V = velocity_field()
speed = np.hypot(U, V)
lw = 5 * speed / speed.max()
ax = plt.figure().subplots()
ax.streamplot(X, Y, U, V, density=[0.5, 1], color='k', linewidth=lw)
@image_comparison(['streamplot_masks_and_nans.png'],
remove_text=True, style='mpl20')
def test_masks_and_nans():
X, Y, U, V = velocity_field()
mask = np.zeros(U.shape, dtype=bool)
mask[40:60, 80:120] = 1
U[:20, :40] = np.nan
U = np.ma.array(U, mask=mask)
ax = plt.figure().subplots()
with np.errstate(invalid='ignore'):
ax.streamplot(X, Y, U, V, color=U, cmap=plt.cm.Blues)
@image_comparison(['streamplot_maxlength.png'],
remove_text=True, style='mpl20', tol=0.302)
def test_maxlength():
x, y, U, V = swirl_velocity_field()
ax = plt.figure().subplots()
ax.streamplot(x, y, U, V, maxlength=10., start_points=[[0., 1.5]],
linewidth=2, density=2)
assert ax.get_xlim()[-1] == ax.get_ylim()[-1] == 3
# Compatibility for old test image
ax.set(xlim=(None, 3.2555988021882305), ylim=(None, 3.078326760195413))
@image_comparison(['streamplot_maxlength_no_broken.png'],
remove_text=True, style='mpl20', tol=0.302)
def test_maxlength_no_broken():
x, y, U, V = swirl_velocity_field()
ax = plt.figure().subplots()
ax.streamplot(x, y, U, V, maxlength=10., start_points=[[0., 1.5]],
linewidth=2, density=2, broken_streamlines=False)
assert ax.get_xlim()[-1] == ax.get_ylim()[-1] == 3
# Compatibility for old test image
ax.set(xlim=(None, 3.2555988021882305), ylim=(None, 3.078326760195413))
@image_comparison(['streamplot_direction.png'],
remove_text=True, style='mpl20', tol=0.073)
def test_direction():
x, y, U, V = swirl_velocity_field()
plt.streamplot(x, y, U, V, integration_direction='backward',
maxlength=1.5, start_points=[[1.5, 0.]],
linewidth=2, density=2)
def test_streamplot_limits():
ax = plt.axes()
x = np.linspace(-5, 10, 20)
y = np.linspace(-2, 4, 10)
y, x = np.meshgrid(y, x)
trans = mtransforms.Affine2D().translate(25, 32) + ax.transData
plt.barbs(x, y, np.sin(x), np.cos(y), transform=trans)
# The calculated bounds are approximately the bounds of the original data,
# this is because the entire path is taken into account when updating the
# datalim.
assert_array_almost_equal(ax.dataLim.bounds, (20, 30, 15, 6),
decimal=1)
def test_streamplot_grid():
u = np.ones((2, 2))
v = np.zeros((2, 2))
# Test for same rows and columns
x = np.array([[10, 20], [10, 30]])
y = np.array([[10, 10], [20, 20]])
with pytest.raises(ValueError, match="The rows of 'x' must be equal"):
plt.streamplot(x, y, u, v)
x = np.array([[10, 20], [10, 20]])
y = np.array([[10, 10], [20, 30]])
with pytest.raises(ValueError, match="The columns of 'y' must be equal"):
plt.streamplot(x, y, u, v)
x = np.array([[10, 20], [10, 20]])
y = np.array([[10, 10], [20, 20]])
plt.streamplot(x, y, u, v)
# Test for maximum dimensions
x = np.array([0, 10])
y = np.array([[[0, 10]]])
with pytest.raises(ValueError, match="'y' can have at maximum "
"2 dimensions"):
plt.streamplot(x, y, u, v)
# Test for equal spacing
u = np.ones((3, 3))
v = np.zeros((3, 3))
x = np.array([0, 10, 20])
y = np.array([0, 10, 30])
with pytest.raises(ValueError, match="'y' values must be equally spaced"):
plt.streamplot(x, y, u, v)
# Test for strictly increasing
x = np.array([0, 20, 40])
y = np.array([0, 20, 10])
with pytest.raises(ValueError, match="'y' must be strictly increasing"):
plt.streamplot(x, y, u, v)
def test_streamplot_inputs(): # test no exception occurs.
# fully-masked
plt.streamplot(np.arange(3), np.arange(3),
np.full((3, 3), np.nan), np.full((3, 3), np.nan),
color=np.random.rand(3, 3))
# array-likes
plt.streamplot(range(3), range(3),
np.random.rand(3, 3), np.random.rand(3, 3))
@@ -0,0 +1,197 @@
from contextlib import contextmanager
from pathlib import Path
from tempfile import TemporaryDirectory
import sys
import numpy as np
import pytest
import matplotlib as mpl
from matplotlib import pyplot as plt, style
from matplotlib.style.core import USER_LIBRARY_PATHS, STYLE_EXTENSION
PARAM = 'image.cmap'
VALUE = 'pink'
DUMMY_SETTINGS = {PARAM: VALUE}
@contextmanager
def temp_style(style_name, settings=None):
"""Context manager to create a style sheet in a temporary directory."""
if not settings:
settings = DUMMY_SETTINGS
temp_file = f'{style_name}.{STYLE_EXTENSION}'
try:
with TemporaryDirectory() as tmpdir:
# Write style settings to file in the tmpdir.
Path(tmpdir, temp_file).write_text(
"\n".join(f"{k}: {v}" for k, v in settings.items()),
encoding="utf-8")
# Add tmpdir to style path and reload so we can access this style.
USER_LIBRARY_PATHS.append(tmpdir)
style.reload_library()
yield
finally:
style.reload_library()
def test_invalid_rc_warning_includes_filename(caplog):
SETTINGS = {'foo': 'bar'}
basename = 'basename'
with temp_style(basename, SETTINGS):
# style.reload_library() in temp_style() triggers the warning
pass
assert (len(caplog.records) == 1
and basename in caplog.records[0].getMessage())
def test_available():
with temp_style('_test_', DUMMY_SETTINGS):
assert '_test_' in style.available
def test_use():
mpl.rcParams[PARAM] = 'gray'
with temp_style('test', DUMMY_SETTINGS):
with style.context('test'):
assert mpl.rcParams[PARAM] == VALUE
def test_use_url(tmp_path):
path = tmp_path / 'file'
path.write_text('axes.facecolor: adeade', encoding='utf-8')
with temp_style('test', DUMMY_SETTINGS):
url = ('file:'
+ ('///' if sys.platform == 'win32' else '')
+ path.resolve().as_posix())
with style.context(url):
assert mpl.rcParams['axes.facecolor'] == "#adeade"
def test_single_path(tmp_path):
mpl.rcParams[PARAM] = 'gray'
path = tmp_path / f'text.{STYLE_EXTENSION}'
path.write_text(f'{PARAM} : {VALUE}', encoding='utf-8')
with style.context(path):
assert mpl.rcParams[PARAM] == VALUE
assert mpl.rcParams[PARAM] == 'gray'
def test_context():
mpl.rcParams[PARAM] = 'gray'
with temp_style('test', DUMMY_SETTINGS):
with style.context('test'):
assert mpl.rcParams[PARAM] == VALUE
# Check that this value is reset after the exiting the context.
assert mpl.rcParams[PARAM] == 'gray'
def test_context_with_dict():
original_value = 'gray'
other_value = 'blue'
mpl.rcParams[PARAM] = original_value
with style.context({PARAM: other_value}):
assert mpl.rcParams[PARAM] == other_value
assert mpl.rcParams[PARAM] == original_value
def test_context_with_dict_after_namedstyle():
# Test dict after style name where dict modifies the same parameter.
original_value = 'gray'
other_value = 'blue'
mpl.rcParams[PARAM] = original_value
with temp_style('test', DUMMY_SETTINGS):
with style.context(['test', {PARAM: other_value}]):
assert mpl.rcParams[PARAM] == other_value
assert mpl.rcParams[PARAM] == original_value
def test_context_with_dict_before_namedstyle():
# Test dict before style name where dict modifies the same parameter.
original_value = 'gray'
other_value = 'blue'
mpl.rcParams[PARAM] = original_value
with temp_style('test', DUMMY_SETTINGS):
with style.context([{PARAM: other_value}, 'test']):
assert mpl.rcParams[PARAM] == VALUE
assert mpl.rcParams[PARAM] == original_value
def test_context_with_union_of_dict_and_namedstyle():
# Test dict after style name where dict modifies the a different parameter.
original_value = 'gray'
other_param = 'text.usetex'
other_value = True
d = {other_param: other_value}
mpl.rcParams[PARAM] = original_value
mpl.rcParams[other_param] = (not other_value)
with temp_style('test', DUMMY_SETTINGS):
with style.context(['test', d]):
assert mpl.rcParams[PARAM] == VALUE
assert mpl.rcParams[other_param] == other_value
assert mpl.rcParams[PARAM] == original_value
assert mpl.rcParams[other_param] == (not other_value)
def test_context_with_badparam():
original_value = 'gray'
other_value = 'blue'
with style.context({PARAM: other_value}):
assert mpl.rcParams[PARAM] == other_value
x = style.context({PARAM: original_value, 'badparam': None})
with pytest.raises(KeyError):
with x:
pass
assert mpl.rcParams[PARAM] == other_value
@pytest.mark.parametrize('equiv_styles',
[('mpl20', 'default'),
('mpl15', 'classic')],
ids=['mpl20', 'mpl15'])
def test_alias(equiv_styles):
rc_dicts = []
for sty in equiv_styles:
with style.context(sty):
rc_dicts.append(mpl.rcParams.copy())
rc_base = rc_dicts[0]
for nm, rc in zip(equiv_styles[1:], rc_dicts[1:]):
assert rc_base == rc
def test_xkcd_no_cm():
assert mpl.rcParams["path.sketch"] is None
plt.xkcd()
assert mpl.rcParams["path.sketch"] == (1, 100, 2)
np.testing.break_cycles()
assert mpl.rcParams["path.sketch"] == (1, 100, 2)
def test_xkcd_cm():
assert mpl.rcParams["path.sketch"] is None
with plt.xkcd():
assert mpl.rcParams["path.sketch"] == (1, 100, 2)
assert mpl.rcParams["path.sketch"] is None
def test_up_to_date_blacklist():
assert mpl.style.core.STYLE_BLACKLIST <= {*mpl.rcsetup._validators}
def test_style_from_module(tmp_path, monkeypatch):
monkeypatch.syspath_prepend(tmp_path)
monkeypatch.chdir(tmp_path)
pkg_path = tmp_path / "mpl_test_style_pkg"
pkg_path.mkdir()
(pkg_path / "test_style.mplstyle").write_text(
"lines.linewidth: 42", encoding="utf-8")
pkg_path.with_suffix(".mplstyle").write_text(
"lines.linewidth: 84", encoding="utf-8")
mpl.style.use("mpl_test_style_pkg.test_style")
assert mpl.rcParams["lines.linewidth"] == 42
mpl.style.use("mpl_test_style_pkg.mplstyle")
assert mpl.rcParams["lines.linewidth"] == 84
mpl.style.use("./mpl_test_style_pkg.mplstyle")
assert mpl.rcParams["lines.linewidth"] == 84
@@ -0,0 +1,293 @@
import itertools
import platform
import numpy as np
import pytest
import matplotlib as mpl
from matplotlib.axes import Axes, SubplotBase
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import check_figures_equal, image_comparison
def check_shared(axs, x_shared, y_shared):
"""
x_shared and y_shared are n x n boolean matrices; entry (i, j) indicates
whether the x (or y) axes of subplots i and j should be shared.
"""
for (i1, ax1), (i2, ax2), (i3, (name, shared)) in itertools.product(
enumerate(axs),
enumerate(axs),
enumerate(zip("xy", [x_shared, y_shared]))):
if i2 <= i1:
continue
assert axs[0]._shared_axes[name].joined(ax1, ax2) == shared[i1, i2], \
"axes %i and %i incorrectly %ssharing %s axis" % (
i1, i2, "not " if shared[i1, i2] else "", name)
def check_ticklabel_visible(axs, x_visible, y_visible):
"""Check that the x and y ticklabel visibility is as specified."""
for i, (ax, vx, vy) in enumerate(zip(axs, x_visible, y_visible)):
for l in ax.get_xticklabels() + [ax.xaxis.offsetText]:
assert l.get_visible() == vx, \
f"Visibility of x axis #{i} is incorrectly {vx}"
for l in ax.get_yticklabels() + [ax.yaxis.offsetText]:
assert l.get_visible() == vy, \
f"Visibility of y axis #{i} is incorrectly {vy}"
# axis label "visibility" is toggled by label_outer by resetting the
# label to empty, but it can also be empty to start with.
if not vx:
assert ax.get_xlabel() == ""
if not vy:
assert ax.get_ylabel() == ""
def check_tick1_visible(axs, x_visible, y_visible):
"""
Check that the x and y tick visibility is as specified.
Note: This only checks the tick1line, i.e. bottom / left ticks.
"""
for ax, visible, in zip(axs, x_visible):
for tick in ax.xaxis.get_major_ticks():
assert tick.tick1line.get_visible() == visible
for ax, y_visible, in zip(axs, y_visible):
for tick in ax.yaxis.get_major_ticks():
assert tick.tick1line.get_visible() == visible
def test_shared():
rdim = (4, 4, 2)
share = {
'all': np.ones(rdim[:2], dtype=bool),
'none': np.zeros(rdim[:2], dtype=bool),
'row': np.array([
[False, True, False, False],
[True, False, False, False],
[False, False, False, True],
[False, False, True, False]]),
'col': np.array([
[False, False, True, False],
[False, False, False, True],
[True, False, False, False],
[False, True, False, False]]),
}
visible = {
'x': {
'all': [False, False, True, True],
'col': [False, False, True, True],
'row': [True] * 4,
'none': [True] * 4,
False: [True] * 4,
True: [False, False, True, True],
},
'y': {
'all': [True, False, True, False],
'col': [True] * 4,
'row': [True, False, True, False],
'none': [True] * 4,
False: [True] * 4,
True: [True, False, True, False],
},
}
share[False] = share['none']
share[True] = share['all']
# test default
f, ((a1, a2), (a3, a4)) = plt.subplots(2, 2)
axs = [a1, a2, a3, a4]
check_shared(axs, share['none'], share['none'])
plt.close(f)
# test all option combinations
ops = [False, True, 'all', 'none', 'row', 'col', 0, 1]
for xo in ops:
for yo in ops:
f, ((a1, a2), (a3, a4)) = plt.subplots(2, 2, sharex=xo, sharey=yo)
axs = [a1, a2, a3, a4]
check_shared(axs, share[xo], share[yo])
check_ticklabel_visible(axs, visible['x'][xo], visible['y'][yo])
plt.close(f)
@pytest.mark.parametrize('remove_ticks', [True, False])
@pytest.mark.parametrize('layout_engine', ['none', 'tight', 'constrained'])
@pytest.mark.parametrize('with_colorbar', [True, False])
def test_label_outer(remove_ticks, layout_engine, with_colorbar):
fig = plt.figure(layout=layout_engine)
axs = fig.subplots(2, 2, sharex=True, sharey=True)
for ax in axs.flat:
ax.set(xlabel="foo", ylabel="bar")
if with_colorbar:
fig.colorbar(mpl.cm.ScalarMappable(), ax=ax)
ax.label_outer(remove_inner_ticks=remove_ticks)
check_ticklabel_visible(
axs.flat, [False, False, True, True], [True, False, True, False])
if remove_ticks:
check_tick1_visible(
axs.flat, [False, False, True, True], [True, False, True, False])
else:
check_tick1_visible(
axs.flat, [True, True, True, True], [True, True, True, True])
def test_label_outer_span():
fig = plt.figure()
gs = fig.add_gridspec(3, 3)
# +---+---+---+
# | 1 | |
# +---+---+---+
# | | | 3 |
# + 2 +---+---+
# | | 4 | |
# +---+---+---+
a1 = fig.add_subplot(gs[0, 0:2])
a2 = fig.add_subplot(gs[1:3, 0])
a3 = fig.add_subplot(gs[1, 2])
a4 = fig.add_subplot(gs[2, 1])
for ax in fig.axes:
ax.label_outer()
check_ticklabel_visible(
fig.axes, [False, True, False, True], [True, True, False, False])
def test_label_outer_non_gridspec():
ax = plt.axes((0, 0, 1, 1))
ax.label_outer() # Does nothing.
check_ticklabel_visible([ax], [True], [True])
def test_shared_and_moved():
# test if sharey is on, but then tick_left is called that labels don't
# re-appear. Seaborn does this just to be sure yaxis is on left...
f, (a1, a2) = plt.subplots(1, 2, sharey=True)
check_ticklabel_visible([a2], [True], [False])
a2.yaxis.tick_left()
check_ticklabel_visible([a2], [True], [False])
f, (a1, a2) = plt.subplots(2, 1, sharex=True)
check_ticklabel_visible([a1], [False], [True])
a2.xaxis.tick_bottom()
check_ticklabel_visible([a1], [False], [True])
def test_exceptions():
# TODO should this test more options?
with pytest.raises(ValueError):
plt.subplots(2, 2, sharex='blah')
with pytest.raises(ValueError):
plt.subplots(2, 2, sharey='blah')
@image_comparison(['subplots_offset_text.png'],
tol=0 if platform.machine() == 'x86_64' else 0.028)
def test_subplots_offsettext():
x = np.arange(0, 1e10, 1e9)
y = np.arange(0, 100, 10)+1e4
fig, axs = plt.subplots(2, 2, sharex='col', sharey='all')
axs[0, 0].plot(x, x)
axs[1, 0].plot(x, x)
axs[0, 1].plot(y, x)
axs[1, 1].plot(y, x)
@pytest.mark.parametrize("top", [True, False])
@pytest.mark.parametrize("bottom", [True, False])
@pytest.mark.parametrize("left", [True, False])
@pytest.mark.parametrize("right", [True, False])
def test_subplots_hide_ticklabels(top, bottom, left, right):
# Ideally, we would also test offset-text visibility (and remove
# test_subplots_offsettext), but currently, setting rcParams fails to move
# the offset texts as well.
with plt.rc_context({"xtick.labeltop": top, "xtick.labelbottom": bottom,
"ytick.labelleft": left, "ytick.labelright": right}):
axs = plt.figure().subplots(3, 3, sharex=True, sharey=True)
for (i, j), ax in np.ndenumerate(axs):
xtop = ax.xaxis._major_tick_kw["label2On"]
xbottom = ax.xaxis._major_tick_kw["label1On"]
yleft = ax.yaxis._major_tick_kw["label1On"]
yright = ax.yaxis._major_tick_kw["label2On"]
assert xtop == (top and i == 0)
assert xbottom == (bottom and i == 2)
assert yleft == (left and j == 0)
assert yright == (right and j == 2)
@pytest.mark.parametrize("xlabel_position", ["bottom", "top"])
@pytest.mark.parametrize("ylabel_position", ["left", "right"])
def test_subplots_hide_axislabels(xlabel_position, ylabel_position):
axs = plt.figure().subplots(3, 3, sharex=True, sharey=True)
for (i, j), ax in np.ndenumerate(axs):
ax.set(xlabel="foo", ylabel="bar")
ax.xaxis.set_label_position(xlabel_position)
ax.yaxis.set_label_position(ylabel_position)
ax.label_outer()
assert bool(ax.get_xlabel()) == (
xlabel_position == "bottom" and i == 2
or xlabel_position == "top" and i == 0)
assert bool(ax.get_ylabel()) == (
ylabel_position == "left" and j == 0
or ylabel_position == "right" and j == 2)
def test_get_gridspec():
# ahem, pretty trivial, but...
fig, ax = plt.subplots()
assert ax.get_subplotspec().get_gridspec() == ax.get_gridspec()
def test_dont_mutate_kwargs():
subplot_kw = {'sharex': 'all'}
gridspec_kw = {'width_ratios': [1, 2]}
fig, ax = plt.subplots(1, 2, subplot_kw=subplot_kw,
gridspec_kw=gridspec_kw)
assert subplot_kw == {'sharex': 'all'}
assert gridspec_kw == {'width_ratios': [1, 2]}
@pytest.mark.parametrize("width_ratios", [None, [1, 3, 2]])
@pytest.mark.parametrize("height_ratios", [None, [1, 2]])
@check_figures_equal(extensions=['png'])
def test_width_and_height_ratios(fig_test, fig_ref,
height_ratios, width_ratios):
fig_test.subplots(2, 3, height_ratios=height_ratios,
width_ratios=width_ratios)
fig_ref.subplots(2, 3, gridspec_kw={
'height_ratios': height_ratios,
'width_ratios': width_ratios})
@pytest.mark.parametrize("width_ratios", [None, [1, 3, 2]])
@pytest.mark.parametrize("height_ratios", [None, [1, 2]])
@check_figures_equal(extensions=['png'])
def test_width_and_height_ratios_mosaic(fig_test, fig_ref,
height_ratios, width_ratios):
mosaic_spec = [['A', 'B', 'B'], ['A', 'C', 'D']]
fig_test.subplot_mosaic(mosaic_spec, height_ratios=height_ratios,
width_ratios=width_ratios)
fig_ref.subplot_mosaic(mosaic_spec, gridspec_kw={
'height_ratios': height_ratios,
'width_ratios': width_ratios})
@pytest.mark.parametrize('method,args', [
('subplots', (2, 3)),
('subplot_mosaic', ('abc;def', ))
]
)
def test_ratio_overlapping_kws(method, args):
with pytest.raises(ValueError, match='height_ratios'):
getattr(plt, method)(*args, height_ratios=[1, 2],
gridspec_kw={'height_ratios': [1, 2]})
with pytest.raises(ValueError, match='width_ratios'):
getattr(plt, method)(*args, width_ratios=[1, 2, 3],
gridspec_kw={'width_ratios': [1, 2, 3]})
def test_old_subplot_compat():
fig = plt.figure()
assert isinstance(fig.add_subplot(), SubplotBase)
assert not isinstance(fig.add_axes(rect=[0, 0, 1, 1]), SubplotBase)
with pytest.raises(TypeError):
Axes(fig, [0, 0, 1, 1], rect=[0, 0, 1, 1])
@@ -0,0 +1,283 @@
import datetime
from unittest.mock import Mock
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.table import CustomCell, Table
from matplotlib.testing.decorators import image_comparison, check_figures_equal
from matplotlib.transforms import Bbox
import matplotlib.units as munits
def test_non_square():
# Check that creating a non-square table works
cellcolors = ['b', 'r']
plt.table(cellColours=cellcolors)
@image_comparison(['table_zorder.png'], remove_text=True)
def test_zorder():
data = [[66386, 174296],
[58230, 381139]]
colLabels = ('Freeze', 'Wind')
rowLabels = ['%d year' % x for x in (100, 50)]
cellText = []
yoff = np.zeros(len(colLabels))
for row in reversed(data):
yoff += row
cellText.append(['%1.1f' % (x/1000.0) for x in yoff])
t = np.linspace(0, 2*np.pi, 100)
plt.plot(t, np.cos(t), lw=4, zorder=2)
plt.table(cellText=cellText,
rowLabels=rowLabels,
colLabels=colLabels,
loc='center',
zorder=-2,
)
plt.table(cellText=cellText,
rowLabels=rowLabels,
colLabels=colLabels,
loc='upper center',
zorder=4,
)
plt.yticks([])
@image_comparison(['table_labels.png'])
def test_label_colours():
dim = 3
c = np.linspace(0, 1, dim)
colours = plt.cm.RdYlGn(c)
cellText = [['1'] * dim] * dim
fig = plt.figure()
ax1 = fig.add_subplot(4, 1, 1)
ax1.axis('off')
ax1.table(cellText=cellText,
rowColours=colours,
loc='best')
ax2 = fig.add_subplot(4, 1, 2)
ax2.axis('off')
ax2.table(cellText=cellText,
rowColours=colours,
rowLabels=['Header'] * dim,
loc='best')
ax3 = fig.add_subplot(4, 1, 3)
ax3.axis('off')
ax3.table(cellText=cellText,
colColours=colours,
loc='best')
ax4 = fig.add_subplot(4, 1, 4)
ax4.axis('off')
ax4.table(cellText=cellText,
colColours=colours,
colLabels=['Header'] * dim,
loc='best')
@image_comparison(['table_cell_manipulation.png'], style='mpl20')
def test_diff_cell_table(text_placeholders):
cells = ('horizontal', 'vertical', 'open', 'closed', 'T', 'R', 'B', 'L')
cellText = [['1'] * len(cells)] * 2
colWidths = [0.1] * len(cells)
_, axs = plt.subplots(nrows=len(cells), figsize=(4, len(cells)+1), layout='tight')
for ax, cell in zip(axs, cells):
ax.table(
colWidths=colWidths,
cellText=cellText,
loc='center',
edges=cell,
)
ax.axis('off')
def test_customcell():
types = ('horizontal', 'vertical', 'open', 'closed', 'T', 'R', 'B', 'L')
codes = (
(Path.MOVETO, Path.LINETO, Path.MOVETO, Path.LINETO, Path.MOVETO),
(Path.MOVETO, Path.MOVETO, Path.LINETO, Path.MOVETO, Path.LINETO),
(Path.MOVETO, Path.MOVETO, Path.MOVETO, Path.MOVETO, Path.MOVETO),
(Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY),
(Path.MOVETO, Path.MOVETO, Path.MOVETO, Path.LINETO, Path.MOVETO),
(Path.MOVETO, Path.MOVETO, Path.LINETO, Path.MOVETO, Path.MOVETO),
(Path.MOVETO, Path.LINETO, Path.MOVETO, Path.MOVETO, Path.MOVETO),
(Path.MOVETO, Path.MOVETO, Path.MOVETO, Path.MOVETO, Path.LINETO),
)
for t, c in zip(types, codes):
cell = CustomCell((0, 0), visible_edges=t, width=1, height=1)
code = tuple(s for _, s in cell.get_path().iter_segments())
assert c == code
@image_comparison(['table_auto_column.png'])
def test_auto_column():
fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1)
# iterable list input
ax1.axis('off')
tb1 = ax1.table(
cellText=[['Fit Text', 2],
['very long long text, Longer text than default', 1]],
rowLabels=["A", "B"],
colLabels=["Col1", "Col2"],
loc="center")
tb1.auto_set_font_size(False)
tb1.set_fontsize(12)
tb1.auto_set_column_width([-1, 0, 1])
# iterable tuple input
ax2.axis('off')
tb2 = ax2.table(
cellText=[['Fit Text', 2],
['very long long text, Longer text than default', 1]],
rowLabels=["A", "B"],
colLabels=["Col1", "Col2"],
loc="center")
tb2.auto_set_font_size(False)
tb2.set_fontsize(12)
tb2.auto_set_column_width((-1, 0, 1))
# 3 single inputs
ax3.axis('off')
tb3 = ax3.table(
cellText=[['Fit Text', 2],
['very long long text, Longer text than default', 1]],
rowLabels=["A", "B"],
colLabels=["Col1", "Col2"],
loc="center")
tb3.auto_set_font_size(False)
tb3.set_fontsize(12)
tb3.auto_set_column_width(-1)
tb3.auto_set_column_width(0)
tb3.auto_set_column_width(1)
# 4 this used to test non-integer iterable input, which did nothing, but only
# remains to avoid re-generating the test image.
ax4.axis('off')
tb4 = ax4.table(
cellText=[['Fit Text', 2],
['very long long text, Longer text than default', 1]],
rowLabels=["A", "B"],
colLabels=["Col1", "Col2"],
loc="center")
tb4.auto_set_font_size(False)
tb4.set_fontsize(12)
def test_table_cells():
fig, ax = plt.subplots()
table = Table(ax)
cell = table.add_cell(1, 2, 1, 1)
assert isinstance(cell, CustomCell)
assert cell is table[1, 2]
cell2 = CustomCell((0, 0), 1, 2, visible_edges=None)
table[2, 1] = cell2
assert table[2, 1] is cell2
# make sure getitem support has not broken
# properties and setp
table.properties()
plt.setp(table)
@check_figures_equal(extensions=["png"])
def test_table_bbox(fig_test, fig_ref):
data = [[2, 3],
[4, 5]]
col_labels = ('Foo', 'Bar')
row_labels = ('Ada', 'Bob')
cell_text = [[f"{x}" for x in row] for row in data]
ax_list = fig_test.subplots()
ax_list.table(cellText=cell_text,
rowLabels=row_labels,
colLabels=col_labels,
loc='center',
bbox=[0.1, 0.2, 0.8, 0.6]
)
ax_bbox = fig_ref.subplots()
ax_bbox.table(cellText=cell_text,
rowLabels=row_labels,
colLabels=col_labels,
loc='center',
bbox=Bbox.from_extents(0.1, 0.2, 0.9, 0.8)
)
@check_figures_equal(extensions=['png'])
def test_table_unit(fig_test, fig_ref):
# test that table doesn't participate in unit machinery, instead uses repr/str
class FakeUnit:
def __init__(self, thing):
pass
def __repr__(self):
return "Hello"
fake_convertor = munits.ConversionInterface()
# v, u, a = value, unit, axis
fake_convertor.convert = Mock(side_effect=lambda v, u, a: 0)
# not used, here for completeness
fake_convertor.default_units = Mock(side_effect=lambda v, a: None)
fake_convertor.axisinfo = Mock(side_effect=lambda u, a: munits.AxisInfo())
munits.registry[FakeUnit] = fake_convertor
data = [[FakeUnit("yellow"), FakeUnit(42)],
[FakeUnit(datetime.datetime(1968, 8, 1)), FakeUnit(True)]]
fig_test.subplots().table(data)
fig_ref.subplots().table([["Hello", "Hello"], ["Hello", "Hello"]])
fig_test.canvas.draw()
fake_convertor.convert.assert_not_called()
munits.registry.pop(FakeUnit)
assert not munits.registry.get_converter(FakeUnit)
def test_table_dataframe(pd):
# Test if Pandas Data Frame can be passed in cellText
data = {
'Letter': ['A', 'B', 'C'],
'Number': [100, 200, 300]
}
df = pd.DataFrame(data)
fig, ax = plt.subplots()
table = ax.table(df, loc='center')
for r, (index, row) in enumerate(df.iterrows()):
for c, col in enumerate(df.columns if r == 0 else row.values):
assert table[r if r == 0 else r+1, c].get_text().get_text() == str(col)
def test_table_fontsize():
# Test that the passed fontsize propagates to cells
tableData = [['a', 1], ['b', 2]]
fig, ax = plt.subplots()
test_fontsize = 20
t = ax.table(cellText=tableData, loc='top', fontsize=test_fontsize)
cell_fontsize = t[(0, 0)].get_fontsize()
assert cell_fontsize == test_fontsize, f"Actual:{test_fontsize},got:{cell_fontsize}"
cell_fontsize = t[(1, 1)].get_fontsize()
assert cell_fontsize == test_fontsize, f"Actual:{test_fontsize},got:{cell_fontsize}"
@@ -0,0 +1,41 @@
import warnings
import pytest
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import check_figures_equal
@pytest.mark.xfail(
strict=True, reason="testing that warnings fail tests"
)
def test_warn_to_fail():
warnings.warn("This should fail the test")
@pytest.mark.parametrize("a", [1])
@check_figures_equal(extensions=["png"])
@pytest.mark.parametrize("b", [1])
def test_parametrize_with_check_figure_equal(a, fig_ref, b, fig_test):
assert a == b
def test_wrap_failure():
with pytest.raises(ValueError, match="^The decorated function"):
@check_figures_equal()
def should_fail(test, ref):
pass
@pytest.mark.xfail(raises=RuntimeError, strict=True,
reason='Test for check_figures_equal test creating '
'new figures')
@check_figures_equal()
def test_check_figures_equal_extra_fig(fig_test, fig_ref):
plt.figure()
@check_figures_equal()
def test_check_figures_equal_closed_fig(fig_test, fig_ref):
fig = plt.figure()
plt.close(fig)
@@ -0,0 +1,75 @@
import os
from pathlib import Path
import re
import sys
import pytest
import matplotlib.pyplot as plt
from matplotlib.testing import subprocess_run_for_testing
from matplotlib.testing._markers import needs_usetex
from matplotlib.texmanager import TexManager
def test_fontconfig_preamble():
"""Test that the preamble is included in the source."""
plt.rcParams['text.usetex'] = True
src1 = TexManager()._get_tex_source("", fontsize=12)
plt.rcParams['text.latex.preamble'] = '\\usepackage{txfonts}'
src2 = TexManager()._get_tex_source("", fontsize=12)
assert src1 != src2
@pytest.mark.parametrize(
"rc, preamble, family", [
({"font.family": "sans-serif", "font.sans-serif": "helvetica"},
r"\usepackage{helvet}", r"\sffamily"),
({"font.family": "serif", "font.serif": "palatino"},
r"\usepackage{mathpazo}", r"\rmfamily"),
({"font.family": "cursive", "font.cursive": "zapf chancery"},
r"\usepackage{chancery}", r"\rmfamily"),
({"font.family": "monospace", "font.monospace": "courier"},
r"\usepackage{courier}", r"\ttfamily"),
({"font.family": "helvetica"}, r"\usepackage{helvet}", r"\sffamily"),
({"font.family": "palatino"}, r"\usepackage{mathpazo}", r"\rmfamily"),
({"font.family": "zapf chancery"},
r"\usepackage{chancery}", r"\rmfamily"),
({"font.family": "courier"}, r"\usepackage{courier}", r"\ttfamily")
])
def test_font_selection(rc, preamble, family):
plt.rcParams.update(rc)
tm = TexManager()
src = Path(tm.make_tex("hello, world", fontsize=12)).read_text()
assert preamble in src
assert [*re.findall(r"\\\w+family", src)] == [family]
@needs_usetex
def test_unicode_characters():
# Smoke test to see that Unicode characters does not cause issues
# See #23019
plt.rcParams['text.usetex'] = True
fig, ax = plt.subplots()
ax.set_ylabel('\\textit{Velocity (\N{DEGREE SIGN}/sec)}')
ax.set_xlabel('\N{VULGAR FRACTION ONE QUARTER}Öøæ')
fig.canvas.draw()
# But not all characters.
# Should raise RuntimeError, not UnicodeDecodeError
with pytest.raises(RuntimeError):
ax.set_title('\N{SNOWMAN}')
fig.canvas.draw()
@needs_usetex
def test_openin_any_paranoid():
completed = subprocess_run_for_testing(
[sys.executable, "-c",
'import matplotlib.pyplot as plt;'
'plt.rcParams.update({"text.usetex": True});'
'plt.title("paranoid");'
'plt.show(block=False);'],
env={**os.environ, 'openin_any': 'p'}, check=True, capture_output=True)
assert completed.stderr == ""
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,10 @@
import copy
from matplotlib.textpath import TextPath
def test_copy():
tp = TextPath((0, 0), ".")
assert copy.deepcopy(tp).vertices is not tp.vertices
assert (copy.deepcopy(tp).vertices == tp.vertices).all()
assert copy.copy(tp).vertices is tp.vertices
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,430 @@
import warnings
import numpy as np
from numpy.testing import assert_array_equal
import pytest
import matplotlib as mpl
from matplotlib.testing.decorators import image_comparison
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnchoredOffsetbox, DrawingArea
from matplotlib.patches import Rectangle
pytestmark = [
pytest.mark.usefixtures('text_placeholders')
]
def example_plot(ax, fontsize=12):
ax.plot([1, 2])
ax.locator_params(nbins=3)
ax.set_xlabel('x-label', fontsize=fontsize)
ax.set_ylabel('y-label', fontsize=fontsize)
ax.set_title('Title', fontsize=fontsize)
@image_comparison(['tight_layout1'], style='mpl20')
def test_tight_layout1():
"""Test tight_layout for a single subplot."""
fig, ax = plt.subplots()
example_plot(ax, fontsize=24)
plt.tight_layout()
@image_comparison(['tight_layout2'], style='mpl20')
def test_tight_layout2():
"""Test tight_layout for multiple subplots."""
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(nrows=2, ncols=2)
example_plot(ax1)
example_plot(ax2)
example_plot(ax3)
example_plot(ax4)
plt.tight_layout()
@image_comparison(['tight_layout3'], style='mpl20')
def test_tight_layout3():
"""Test tight_layout for multiple subplots."""
ax1 = plt.subplot(221)
ax2 = plt.subplot(223)
ax3 = plt.subplot(122)
example_plot(ax1)
example_plot(ax2)
example_plot(ax3)
plt.tight_layout()
@image_comparison(['tight_layout4'], style='mpl20')
def test_tight_layout4():
"""Test tight_layout for subplot2grid."""
ax1 = plt.subplot2grid((3, 3), (0, 0))
ax2 = plt.subplot2grid((3, 3), (0, 1), colspan=2)
ax3 = plt.subplot2grid((3, 3), (1, 0), colspan=2, rowspan=2)
ax4 = plt.subplot2grid((3, 3), (1, 2), rowspan=2)
example_plot(ax1)
example_plot(ax2)
example_plot(ax3)
example_plot(ax4)
plt.tight_layout()
@image_comparison(['tight_layout5'], style='mpl20')
def test_tight_layout5():
"""Test tight_layout for image."""
ax = plt.subplot()
arr = np.arange(100).reshape((10, 10))
ax.imshow(arr, interpolation="none")
plt.tight_layout()
@image_comparison(['tight_layout6'], style='mpl20')
def test_tight_layout6():
"""Test tight_layout for gridspec."""
# This raises warnings since tight layout cannot
# do this fully automatically. But the test is
# correct since the layout is manually edited
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
fig = plt.figure()
gs1 = mpl.gridspec.GridSpec(2, 1)
ax1 = fig.add_subplot(gs1[0])
ax2 = fig.add_subplot(gs1[1])
example_plot(ax1)
example_plot(ax2)
gs1.tight_layout(fig, rect=[0, 0, 0.5, 1])
gs2 = mpl.gridspec.GridSpec(3, 1)
for ss in gs2:
ax = fig.add_subplot(ss)
example_plot(ax)
ax.set_title("")
ax.set_xlabel("")
ax.set_xlabel("x-label", fontsize=12)
gs2.tight_layout(fig, rect=[0.5, 0, 1, 1], h_pad=0.45)
top = min(gs1.top, gs2.top)
bottom = max(gs1.bottom, gs2.bottom)
gs1.tight_layout(fig, rect=[None, 0 + (bottom-gs1.bottom),
0.5, 1 - (gs1.top-top)])
gs2.tight_layout(fig, rect=[0.5, 0 + (bottom-gs2.bottom),
None, 1 - (gs2.top-top)],
h_pad=0.45)
@image_comparison(['tight_layout7'], style='mpl20')
def test_tight_layout7():
# tight layout with left and right titles
fontsize = 24
fig, ax = plt.subplots()
ax.plot([1, 2])
ax.locator_params(nbins=3)
ax.set_xlabel('x-label', fontsize=fontsize)
ax.set_ylabel('y-label', fontsize=fontsize)
ax.set_title('Left Title', loc='left', fontsize=fontsize)
ax.set_title('Right Title', loc='right', fontsize=fontsize)
plt.tight_layout()
@image_comparison(['tight_layout8'], style='mpl20', tol=0.005)
def test_tight_layout8():
"""Test automatic use of tight_layout."""
fig = plt.figure()
fig.set_layout_engine(layout='tight', pad=0.1)
ax = fig.add_subplot()
example_plot(ax, fontsize=24)
fig.draw_without_rendering()
@image_comparison(['tight_layout9'], style='mpl20')
def test_tight_layout9():
# Test tight_layout for non-visible subplots
# GH 8244
f, axarr = plt.subplots(2, 2)
axarr[1][1].set_visible(False)
plt.tight_layout()
def test_outward_ticks():
"""Test automatic use of tight_layout."""
fig = plt.figure()
ax = fig.add_subplot(221)
ax.xaxis.set_tick_params(tickdir='out', length=16, width=3)
ax.yaxis.set_tick_params(tickdir='out', length=16, width=3)
ax.xaxis.set_tick_params(
tickdir='out', length=32, width=3, tick1On=True, which='minor')
ax.yaxis.set_tick_params(
tickdir='out', length=32, width=3, tick1On=True, which='minor')
ax.xaxis.set_ticks([0], minor=True)
ax.yaxis.set_ticks([0], minor=True)
ax = fig.add_subplot(222)
ax.xaxis.set_tick_params(tickdir='in', length=32, width=3)
ax.yaxis.set_tick_params(tickdir='in', length=32, width=3)
ax = fig.add_subplot(223)
ax.xaxis.set_tick_params(tickdir='inout', length=32, width=3)
ax.yaxis.set_tick_params(tickdir='inout', length=32, width=3)
ax = fig.add_subplot(224)
ax.xaxis.set_tick_params(tickdir='out', length=32, width=3)
ax.yaxis.set_tick_params(tickdir='out', length=32, width=3)
plt.tight_layout()
# These values were obtained after visual checking that they correspond
# to a tight layouting that did take the ticks into account.
expected = [
[[0.092, 0.605], [0.433, 0.933]],
[[0.581, 0.605], [0.922, 0.933]],
[[0.092, 0.138], [0.433, 0.466]],
[[0.581, 0.138], [0.922, 0.466]],
]
for nn, ax in enumerate(fig.axes):
assert_array_equal(np.round(ax.get_position().get_points(), 3),
expected[nn])
def add_offsetboxes(ax, size=10, margin=.1, color='black'):
"""
Surround ax with OffsetBoxes
"""
m, mp = margin, 1+margin
anchor_points = [(-m, -m), (-m, .5), (-m, mp),
(.5, mp), (mp, mp), (mp, .5),
(mp, -m), (.5, -m)]
for point in anchor_points:
da = DrawingArea(size, size)
background = Rectangle((0, 0), width=size,
height=size,
facecolor=color,
edgecolor='None',
linewidth=0,
antialiased=False)
da.add_artist(background)
anchored_box = AnchoredOffsetbox(
loc='center',
child=da,
pad=0.,
frameon=False,
bbox_to_anchor=point,
bbox_transform=ax.transAxes,
borderpad=0.)
ax.add_artist(anchored_box)
def test_tight_layout_offsetboxes():
# 0.
# - Create 4 subplots
# - Plot a diagonal line on them
# - Use tight_layout
#
# 1.
# - Same 4 subplots
# - Surround each plot with 7 boxes
# - Use tight_layout
# - See that the squares are included in the tight_layout and that the squares do
# not overlap
#
# 2.
# - Make the squares around the Axes invisible
# - See that the invisible squares do not affect the tight_layout
rows = cols = 2
colors = ['red', 'blue', 'green', 'yellow']
x = y = [0, 1]
def _subplots(with_boxes):
fig, axs = plt.subplots(rows, cols)
for ax, color in zip(axs.flat, colors):
ax.plot(x, y, color=color)
if with_boxes:
add_offsetboxes(ax, 20, color=color)
return fig, axs
# 0.
fig0, axs0 = _subplots(False)
fig0.tight_layout()
# 1.
fig1, axs1 = _subplots(True)
fig1.tight_layout()
# The AnchoredOffsetbox should be added to the bounding of the Axes, causing them to
# be smaller than the plain figure.
for ax0, ax1 in zip(axs0.flat, axs1.flat):
bbox0 = ax0.get_position()
bbox1 = ax1.get_position()
assert bbox1.x0 > bbox0.x0
assert bbox1.x1 < bbox0.x1
assert bbox1.y0 > bbox0.y0
assert bbox1.y1 < bbox0.y1
# No AnchoredOffsetbox should overlap with another.
bboxes = []
for ax1 in axs1.flat:
for child in ax1.get_children():
if not isinstance(child, AnchoredOffsetbox):
continue
bbox = child.get_window_extent()
for other_bbox in bboxes:
assert not bbox.overlaps(other_bbox)
bboxes.append(bbox)
# 2.
fig2, axs2 = _subplots(True)
for ax in axs2.flat:
for child in ax.get_children():
if isinstance(child, AnchoredOffsetbox):
child.set_visible(False)
fig2.tight_layout()
# The invisible AnchoredOffsetbox should not count for tight layout, so it should
# look the same as when they were never added.
for ax0, ax2 in zip(axs0.flat, axs2.flat):
bbox0 = ax0.get_position()
bbox2 = ax2.get_position()
assert_array_equal(bbox2.get_points(), bbox0.get_points())
def test_empty_layout():
"""Test that tight layout doesn't cause an error when there are no Axes."""
fig = plt.gcf()
fig.tight_layout()
@pytest.mark.parametrize("label", ["xlabel", "ylabel"])
def test_verybig_decorators(label):
"""Test that no warning emitted when xlabel/ylabel too big."""
fig, ax = plt.subplots(figsize=(3, 2))
ax.set(**{label: 'a' * 100})
def test_big_decorators_horizontal():
"""Test that doesn't warn when xlabel too big."""
fig, axs = plt.subplots(1, 2, figsize=(3, 2))
axs[0].set_xlabel('a' * 30)
axs[1].set_xlabel('b' * 30)
def test_big_decorators_vertical():
"""Test that doesn't warn when ylabel too big."""
fig, axs = plt.subplots(2, 1, figsize=(3, 2))
axs[0].set_ylabel('a' * 20)
axs[1].set_ylabel('b' * 20)
def test_badsubplotgrid():
# test that we get warning for mismatched subplot grids, not than an error
plt.subplot2grid((4, 5), (0, 0))
# this is the bad entry:
plt.subplot2grid((5, 5), (0, 3), colspan=3, rowspan=5)
with pytest.warns(UserWarning):
plt.tight_layout()
def test_collapsed():
# test that if the amount of space required to make all the axes
# decorations fit would mean that the actual Axes would end up with size
# zero (i.e. margins add up to more than the available width) that a call
# to tight_layout will not get applied:
fig, ax = plt.subplots(tight_layout=True)
ax.set_xlim([0, 1])
ax.set_ylim([0, 1])
ax.annotate('BIG LONG STRING', xy=(1.25, 2), xytext=(10.5, 1.75),
annotation_clip=False)
p1 = ax.get_position()
with pytest.warns(UserWarning):
plt.tight_layout()
p2 = ax.get_position()
assert p1.width == p2.width
# test that passing a rect doesn't crash...
with pytest.warns(UserWarning):
plt.tight_layout(rect=[0, 0, 0.8, 0.8])
def test_suptitle():
fig, ax = plt.subplots(tight_layout=True)
st = fig.suptitle("foo")
t = ax.set_title("bar")
fig.canvas.draw()
assert st.get_window_extent().y0 > t.get_window_extent().y1
@pytest.mark.backend("pdf")
def test_non_agg_renderer(monkeypatch, recwarn):
unpatched_init = mpl.backend_bases.RendererBase.__init__
def __init__(self, *args, **kwargs):
# Check that we don't instantiate any other renderer than a pdf
# renderer to perform pdf tight layout.
assert isinstance(self, mpl.backends.backend_pdf.RendererPdf)
unpatched_init(self, *args, **kwargs)
monkeypatch.setattr(mpl.backend_bases.RendererBase, "__init__", __init__)
fig, ax = plt.subplots()
fig.tight_layout()
def test_manual_colorbar():
# This should warn, but not raise
fig, axes = plt.subplots(1, 2)
pts = axes[1].scatter([0, 1], [0, 1], c=[1, 5])
ax_rect = axes[1].get_position()
cax = fig.add_axes(
[ax_rect.x1 + 0.005, ax_rect.y0, 0.015, ax_rect.height]
)
fig.colorbar(pts, cax=cax)
with pytest.warns(UserWarning, match="This figure includes Axes"):
fig.tight_layout()
def test_clipped_to_axes():
# Ensure that _fully_clipped_to_axes() returns True under default
# conditions for all projection types. Axes.get_tightbbox()
# uses this to skip artists in layout calculations.
arr = np.arange(100).reshape((10, 10))
fig = plt.figure(figsize=(6, 2))
ax1 = fig.add_subplot(131, projection='rectilinear')
ax2 = fig.add_subplot(132, projection='mollweide')
ax3 = fig.add_subplot(133, projection='polar')
for ax in (ax1, ax2, ax3):
# Default conditions (clipped by ax.bbox or ax.patch)
ax.grid(False)
h, = ax.plot(arr[:, 0])
m = ax.pcolor(arr)
assert h._fully_clipped_to_axes()
assert m._fully_clipped_to_axes()
# Non-default conditions (not clipped by ax.patch)
rect = Rectangle((0, 0), 0.5, 0.5, transform=ax.transAxes)
h.set_clip_path(rect)
m.set_clip_path(rect.get_path(), rect.get_transform())
assert not h._fully_clipped_to_axes()
assert not m._fully_clipped_to_axes()
def test_tight_pads():
fig, ax = plt.subplots()
with pytest.warns(PendingDeprecationWarning,
match='will be deprecated'):
fig.set_tight_layout({'pad': 0.15})
fig.draw_without_rendering()
def test_tight_kwargs():
fig, ax = plt.subplots(tight_layout={'pad': 0.15})
fig.draw_without_rendering()
def test_tight_toggle():
fig, ax = plt.subplots()
with pytest.warns(PendingDeprecationWarning):
fig.set_tight_layout(True)
assert fig.get_tight_layout()
fig.set_tight_layout(False)
assert not fig.get_tight_layout()
fig.set_tight_layout(True)
assert fig.get_tight_layout()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,160 @@
import matplotlib._type1font as t1f
import os.path
import difflib
import pytest
def test_Type1Font():
filename = os.path.join(os.path.dirname(__file__), 'cmr10.pfb')
font = t1f.Type1Font(filename)
slanted = font.transform({'slant': 1})
condensed = font.transform({'extend': 0.5})
with open(filename, 'rb') as fd:
rawdata = fd.read()
assert font.parts[0] == rawdata[0x0006:0x10c5]
assert font.parts[1] == rawdata[0x10cb:0x897f]
assert font.parts[2] == rawdata[0x8985:0x8ba6]
assert font.decrypted.startswith(b'dup\n/Private 18 dict dup begin')
assert font.decrypted.endswith(b'mark currentfile closefile\n')
assert slanted.decrypted.startswith(b'dup\n/Private 18 dict dup begin')
assert slanted.decrypted.endswith(b'mark currentfile closefile\n')
assert b'UniqueID 5000793' in font.parts[0]
assert b'UniqueID 5000793' in font.decrypted
assert font._pos['UniqueID'] == [(797, 818), (4483, 4504)]
len0 = len(font.parts[0])
for key in font._pos.keys():
for pos0, pos1 in font._pos[key]:
if pos0 < len0:
data = font.parts[0][pos0:pos1]
else:
data = font.decrypted[pos0-len0:pos1-len0]
assert data.startswith(f'/{key}'.encode('ascii'))
assert {'FontType', 'FontMatrix', 'PaintType', 'ItalicAngle', 'RD'
} < set(font._pos.keys())
assert b'UniqueID 5000793' not in slanted.parts[0]
assert b'UniqueID 5000793' not in slanted.decrypted
assert 'UniqueID' not in slanted._pos
assert font.prop['Weight'] == 'Medium'
assert not font.prop['isFixedPitch']
assert font.prop['ItalicAngle'] == 0
assert slanted.prop['ItalicAngle'] == -45
assert font.prop['Encoding'][5] == 'Pi'
assert isinstance(font.prop['CharStrings']['Pi'], bytes)
assert font._abbr['ND'] == 'ND'
differ = difflib.Differ()
diff = list(differ.compare(
font.parts[0].decode('latin-1').splitlines(),
slanted.parts[0].decode('latin-1').splitlines()))
for line in (
# Removes UniqueID
'- /UniqueID 5000793 def',
# Changes the font name
'- /FontName /CMR10 def',
'+ /FontName/CMR10_Slant_1000 def',
# Alters FontMatrix
'- /FontMatrix [0.001 0 0 0.001 0 0 ]readonly def',
'+ /FontMatrix [0.001 0 0.001 0.001 0 0] readonly def',
# Alters ItalicAngle
'- /ItalicAngle 0 def',
'+ /ItalicAngle -45.0 def'):
assert line in diff, 'diff to slanted font must contain %s' % line
diff = list(differ.compare(
font.parts[0].decode('latin-1').splitlines(),
condensed.parts[0].decode('latin-1').splitlines()))
for line in (
# Removes UniqueID
'- /UniqueID 5000793 def',
# Changes the font name
'- /FontName /CMR10 def',
'+ /FontName/CMR10_Extend_500 def',
# Alters FontMatrix
'- /FontMatrix [0.001 0 0 0.001 0 0 ]readonly def',
'+ /FontMatrix [0.0005 0 0 0.001 0 0] readonly def'):
assert line in diff, 'diff to condensed font must contain %s' % line
def test_Type1Font_2():
filename = os.path.join(os.path.dirname(__file__),
'Courier10PitchBT-Bold.pfb')
font = t1f.Type1Font(filename)
assert font.prop['Weight'] == 'Bold'
assert font.prop['isFixedPitch']
assert font.prop['Encoding'][65] == 'A' # the font uses StandardEncoding
(pos0, pos1), = font._pos['Encoding']
assert font.parts[0][pos0:pos1] == b'/Encoding StandardEncoding'
assert font._abbr['ND'] == '|-'
def test_tokenize():
data = (b'1234/abc false -9.81 Foo <<[0 1 2]<0 1ef a\t>>>\n'
b'(string with(nested\t\\) par)ens\\\\)')
# 1 2 x 2 xx1
# 1 and 2 are matching parens, x means escaped character
n, w, num, kw, d = 'name', 'whitespace', 'number', 'keyword', 'delimiter'
b, s = 'boolean', 'string'
correct = [
(num, 1234), (n, 'abc'), (w, ' '), (b, False), (w, ' '), (num, -9.81),
(w, ' '), (kw, 'Foo'), (w, ' '), (d, '<<'), (d, '['), (num, 0),
(w, ' '), (num, 1), (w, ' '), (num, 2), (d, ']'), (s, b'\x01\xef\xa0'),
(d, '>>'), (w, '\n'), (s, 'string with(nested\t) par)ens\\')
]
correct_no_ws = [x for x in correct if x[0] != w]
def convert(tokens):
return [(t.kind, t.value()) for t in tokens]
assert convert(t1f._tokenize(data, False)) == correct
assert convert(t1f._tokenize(data, True)) == correct_no_ws
def bin_after(n):
tokens = t1f._tokenize(data, True)
result = []
for _ in range(n):
result.append(next(tokens))
result.append(tokens.send(10))
return convert(result)
for n in range(1, len(correct_no_ws)):
result = bin_after(n)
assert result[:-1] == correct_no_ws[:n]
assert result[-1][0] == 'binary'
assert isinstance(result[-1][1], bytes)
def test_tokenize_errors():
with pytest.raises(ValueError):
list(t1f._tokenize(b'1234 (this (string) is unterminated\\)', True))
with pytest.raises(ValueError):
list(t1f._tokenize(b'/Foo<01234', True))
with pytest.raises(ValueError):
list(t1f._tokenize(b'/Foo<01234abcg>/Bar', True))
def test_overprecision():
# We used to output too many digits in FontMatrix entries and
# ItalicAngle, which could make Type-1 parsers unhappy.
filename = os.path.join(os.path.dirname(__file__), 'cmr10.pfb')
font = t1f.Type1Font(filename)
slanted = font.transform({'slant': .167})
lines = slanted.parts[0].decode('ascii').splitlines()
matrix, = (line[line.index('[')+1:line.index(']')]
for line in lines if '/FontMatrix' in line)
angle, = (word
for line in lines if '/ItalicAngle' in line
for word in line.split() if word[0] in '-0123456789')
# the following used to include 0.00016700000000000002
assert matrix == '0.001 0 0.000167 0.001 0 0'
# and here we had -9.48090361795083
assert angle == '-9.4809'
def test_encrypt_decrypt_roundtrip():
data = b'this is my plaintext \0\1\2\3'
encrypted = t1f.Type1Font._encrypt(data, 'eexec')
decrypted = t1f.Type1Font._decrypt(encrypted, 'eexec')
assert encrypted != decrypted
assert data == decrypted
@@ -0,0 +1,353 @@
from datetime import datetime, timezone, timedelta
import platform
from unittest.mock import MagicMock
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import check_figures_equal, image_comparison
import matplotlib.patches as mpatches
import matplotlib.units as munits
from matplotlib.category import StrCategoryConverter, UnitData
from matplotlib.dates import DateConverter
import numpy as np
import pytest
# Basic class that wraps numpy array and has units
class Quantity:
def __init__(self, data, units):
self.magnitude = data
self.units = units
def to(self, new_units):
factors = {('hours', 'seconds'): 3600, ('minutes', 'hours'): 1 / 60,
('minutes', 'seconds'): 60, ('feet', 'miles'): 1 / 5280.,
('feet', 'inches'): 12, ('miles', 'inches'): 12 * 5280}
if self.units != new_units:
mult = factors[self.units, new_units]
return Quantity(mult * self.magnitude, new_units)
else:
return Quantity(self.magnitude, self.units)
def __copy__(self):
return Quantity(self.magnitude, self.units)
def __getattr__(self, attr):
return getattr(self.magnitude, attr)
def __getitem__(self, item):
if np.iterable(self.magnitude):
return Quantity(self.magnitude[item], self.units)
else:
return Quantity(self.magnitude, self.units)
def __array__(self):
return np.asarray(self.magnitude)
@pytest.fixture
def quantity_converter():
# Create an instance of the conversion interface and
# mock so we can check methods called
qc = munits.ConversionInterface()
def convert(value, unit, axis):
if hasattr(value, 'units'):
return value.to(unit).magnitude
elif np.iterable(value):
try:
return [v.to(unit).magnitude for v in value]
except AttributeError:
return [Quantity(v, axis.get_units()).to(unit).magnitude
for v in value]
else:
return Quantity(value, axis.get_units()).to(unit).magnitude
def default_units(value, axis):
if hasattr(value, 'units'):
return value.units
elif np.iterable(value):
for v in value:
if hasattr(v, 'units'):
return v.units
return None
qc.convert = MagicMock(side_effect=convert)
qc.axisinfo = MagicMock(side_effect=lambda u, a:
munits.AxisInfo(label=u, default_limits=(0, 100)))
qc.default_units = MagicMock(side_effect=default_units)
return qc
# Tests that the conversion machinery works properly for classes that
# work as a facade over numpy arrays (like pint)
@image_comparison(['plot_pint.png'], style='mpl20',
tol=0 if platform.machine() == 'x86_64' else 0.03)
def test_numpy_facade(quantity_converter):
# use former defaults to match existing baseline image
plt.rcParams['axes.formatter.limits'] = -7, 7
# Register the class
munits.registry[Quantity] = quantity_converter
# Simple test
y = Quantity(np.linspace(0, 30), 'miles')
x = Quantity(np.linspace(0, 5), 'hours')
fig, ax = plt.subplots()
fig.subplots_adjust(left=0.15) # Make space for label
ax.plot(x, y, 'tab:blue')
ax.axhline(Quantity(26400, 'feet'), color='tab:red')
ax.axvline(Quantity(120, 'minutes'), color='tab:green')
ax.yaxis.set_units('inches')
ax.xaxis.set_units('seconds')
assert quantity_converter.convert.called
assert quantity_converter.axisinfo.called
assert quantity_converter.default_units.called
# Tests gh-8908
@image_comparison(['plot_masked_units.png'], remove_text=True, style='mpl20',
tol=0 if platform.machine() == 'x86_64' else 0.02)
def test_plot_masked_units():
data = np.linspace(-5, 5)
data_masked = np.ma.array(data, mask=(data > -2) & (data < 2))
data_masked_units = Quantity(data_masked, 'meters')
fig, ax = plt.subplots()
ax.plot(data_masked_units)
def test_empty_set_limits_with_units(quantity_converter):
# Register the class
munits.registry[Quantity] = quantity_converter
fig, ax = plt.subplots()
ax.set_xlim(Quantity(-1, 'meters'), Quantity(6, 'meters'))
ax.set_ylim(Quantity(-1, 'hours'), Quantity(16, 'hours'))
@image_comparison(['jpl_bar_units.png'],
savefig_kwarg={'dpi': 120}, style='mpl20')
def test_jpl_bar_units():
import matplotlib.testing.jpl_units as units
units.register()
day = units.Duration("ET", 24.0 * 60.0 * 60.0)
x = [0 * units.km, 1 * units.km, 2 * units.km]
w = [1 * day, 2 * day, 3 * day]
b = units.Epoch("ET", dt=datetime(2009, 4, 26))
fig, ax = plt.subplots()
ax.bar(x, w, bottom=b)
ax.set_ylim([b - 1 * day, b + w[-1] + (1.001) * day])
@image_comparison(['jpl_barh_units.png'],
savefig_kwarg={'dpi': 120}, style='mpl20')
def test_jpl_barh_units():
import matplotlib.testing.jpl_units as units
units.register()
day = units.Duration("ET", 24.0 * 60.0 * 60.0)
x = [0 * units.km, 1 * units.km, 2 * units.km]
w = [1 * day, 2 * day, 3 * day]
b = units.Epoch("ET", dt=datetime(2009, 4, 26))
fig, ax = plt.subplots()
ax.barh(x, w, left=b)
ax.set_xlim([b - 1 * day, b + w[-1] + (1.001) * day])
def test_jpl_datetime_units_consistent():
import matplotlib.testing.jpl_units as units
units.register()
dt = datetime(2009, 4, 26)
jpl = units.Epoch("ET", dt=dt)
dt_conv = munits.registry.get_converter(dt).convert(dt, None, None)
jpl_conv = munits.registry.get_converter(jpl).convert(jpl, None, None)
assert dt_conv == jpl_conv
def test_empty_arrays():
# Check that plotting an empty array with a dtype works
plt.scatter(np.array([], dtype='datetime64[ns]'), np.array([]))
def test_scatter_element0_masked():
times = np.arange('2005-02', '2005-03', dtype='datetime64[D]')
y = np.arange(len(times), dtype=float)
y[0] = np.nan
fig, ax = plt.subplots()
ax.scatter(times, y)
fig.canvas.draw()
def test_errorbar_mixed_units():
x = np.arange(10)
y = [datetime(2020, 5, i * 2 + 1) for i in x]
fig, ax = plt.subplots()
ax.errorbar(x, y, timedelta(days=0.5))
fig.canvas.draw()
@check_figures_equal(extensions=["png"])
def test_subclass(fig_test, fig_ref):
class subdate(datetime):
pass
fig_test.subplots().plot(subdate(2000, 1, 1), 0, "o")
fig_ref.subplots().plot(datetime(2000, 1, 1), 0, "o")
def test_shared_axis_quantity(quantity_converter):
munits.registry[Quantity] = quantity_converter
x = Quantity(np.linspace(0, 1, 10), "hours")
y1 = Quantity(np.linspace(1, 2, 10), "feet")
y2 = Quantity(np.linspace(3, 4, 10), "feet")
fig, (ax1, ax2) = plt.subplots(2, 1, sharex='all', sharey='all')
ax1.plot(x, y1)
ax2.plot(x, y2)
assert ax1.xaxis.get_units() == ax2.xaxis.get_units() == "hours"
assert ax2.yaxis.get_units() == ax2.yaxis.get_units() == "feet"
ax1.xaxis.set_units("seconds")
ax2.yaxis.set_units("inches")
assert ax1.xaxis.get_units() == ax2.xaxis.get_units() == "seconds"
assert ax1.yaxis.get_units() == ax2.yaxis.get_units() == "inches"
def test_shared_axis_datetime():
# datetime uses dates.DateConverter
y1 = [datetime(2020, i, 1, tzinfo=timezone.utc) for i in range(1, 13)]
y2 = [datetime(2021, i, 1, tzinfo=timezone.utc) for i in range(1, 13)]
fig, (ax1, ax2) = plt.subplots(1, 2, sharey=True)
ax1.plot(y1)
ax2.plot(y2)
ax1.yaxis.set_units(timezone(timedelta(hours=5)))
assert ax2.yaxis.units == timezone(timedelta(hours=5))
def test_shared_axis_categorical():
# str uses category.StrCategoryConverter
d1 = {"a": 1, "b": 2}
d2 = {"a": 3, "b": 4}
fig, (ax1, ax2) = plt.subplots(1, 2, sharex=True, sharey=True)
ax1.plot(d1.keys(), d1.values())
ax2.plot(d2.keys(), d2.values())
ax1.xaxis.set_units(UnitData(["c", "d"]))
assert "c" in ax2.xaxis.get_units()._mapping.keys()
def test_explicit_converter():
d1 = {"a": 1, "b": 2}
str_cat_converter = StrCategoryConverter()
str_cat_converter_2 = StrCategoryConverter()
date_converter = DateConverter()
# Explicit is set
fig1, ax1 = plt.subplots()
ax1.xaxis.set_converter(str_cat_converter)
assert ax1.xaxis.get_converter() == str_cat_converter
# Explicit not overridden by implicit
ax1.plot(d1.keys(), d1.values())
assert ax1.xaxis.get_converter() == str_cat_converter
# No error when called twice with equivalent input
ax1.xaxis.set_converter(str_cat_converter)
# Error when explicit called twice
with pytest.raises(RuntimeError):
ax1.xaxis.set_converter(str_cat_converter_2)
fig2, ax2 = plt.subplots()
ax2.plot(d1.keys(), d1.values())
# No error when equivalent type is used
ax2.xaxis.set_converter(str_cat_converter)
fig3, ax3 = plt.subplots()
ax3.plot(d1.keys(), d1.values())
# Warn when implicit overridden
with pytest.warns():
ax3.xaxis.set_converter(date_converter)
def test_empty_default_limits(quantity_converter):
munits.registry[Quantity] = quantity_converter
fig, ax1 = plt.subplots()
ax1.xaxis.update_units(Quantity([10], "miles"))
fig.draw_without_rendering()
assert ax1.get_xlim() == (0, 100)
ax1.yaxis.update_units(Quantity([10], "miles"))
fig.draw_without_rendering()
assert ax1.get_ylim() == (0, 100)
fig, ax = plt.subplots()
ax.axhline(30)
ax.plot(Quantity(np.arange(0, 3), "miles"),
Quantity(np.arange(0, 6, 2), "feet"))
fig.draw_without_rendering()
assert ax.get_xlim() == (0, 2)
assert ax.get_ylim() == (0, 30)
fig, ax = plt.subplots()
ax.axvline(30)
ax.plot(Quantity(np.arange(0, 3), "miles"),
Quantity(np.arange(0, 6, 2), "feet"))
fig.draw_without_rendering()
assert ax.get_xlim() == (0, 30)
assert ax.get_ylim() == (0, 4)
fig, ax = plt.subplots()
ax.xaxis.update_units(Quantity([10], "miles"))
ax.axhline(30)
fig.draw_without_rendering()
assert ax.get_xlim() == (0, 100)
assert ax.get_ylim() == (28.5, 31.5)
fig, ax = plt.subplots()
ax.yaxis.update_units(Quantity([10], "miles"))
ax.axvline(30)
fig.draw_without_rendering()
assert ax.get_ylim() == (0, 100)
assert ax.get_xlim() == (28.5, 31.5)
# test array-like objects...
class Kernel:
def __init__(self, array):
self._array = np.asanyarray(array)
def __array__(self, dtype=None, copy=None):
if dtype is not None and dtype != self._array.dtype:
if copy is not None and not copy:
raise ValueError(
f"Converting array from {self._array.dtype} to "
f"{dtype} requires a copy"
)
arr = np.asarray(self._array, dtype=dtype)
return (arr if not copy else np.copy(arr))
@property
def shape(self):
return self._array.shape
def test_plot_kernel():
# just a smoketest that fail
kernel = Kernel([1, 2, 3, 4, 5])
plt.plot(kernel)
def test_connection_patch_units(pd):
# tests that this doesn't raise an error
fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(10, 5))
x = pd.Timestamp('2017-01-01T12')
ax1.axvline(x)
y = "test test"
ax2.axhline(y)
arr = mpatches.ConnectionPatch((x, 0), (0, y),
coordsA='data', coordsB='data',
axesA=ax1, axesB=ax2)
fig.add_artist(arr)
fig.draw_without_rendering()
@@ -0,0 +1,187 @@
from tempfile import TemporaryFile
import numpy as np
from packaging.version import parse as parse_version
import pytest
import matplotlib as mpl
from matplotlib import dviread
from matplotlib.testing import _has_tex_package
from matplotlib.testing.decorators import check_figures_equal, image_comparison
from matplotlib.testing._markers import needs_usetex
import matplotlib.pyplot as plt
pytestmark = needs_usetex
@image_comparison(
baseline_images=['test_usetex'],
extensions=['pdf', 'png'],
style="mpl20")
def test_usetex():
mpl.rcParams['text.usetex'] = True
fig, ax = plt.subplots()
kwargs = {"verticalalignment": "baseline", "size": 24,
"bbox": dict(pad=0, edgecolor="k", facecolor="none")}
ax.text(0.2, 0.7,
# the \LaTeX macro exercises character sizing and placement,
# \left[ ... \right\} draw some variable-height characters,
# \sqrt and \frac draw horizontal rules, \mathrm changes the font
r'\LaTeX\ $\left[\int\limits_e^{2e}'
r'\sqrt\frac{\log^3 x}{x}\,\mathrm{d}x \right\}$',
**kwargs)
ax.text(0.2, 0.3, "lg", **kwargs)
ax.text(0.4, 0.3, r"$\frac{1}{2}\pi$", **kwargs)
ax.text(0.6, 0.3, "$p^{3^A}$", **kwargs)
ax.text(0.8, 0.3, "$p_{3_2}$", **kwargs)
for x in {t.get_position()[0] for t in ax.texts}:
ax.axvline(x)
for y in {t.get_position()[1] for t in ax.texts}:
ax.axhline(y)
ax.set_axis_off()
@check_figures_equal()
def test_empty(fig_test, fig_ref):
mpl.rcParams['text.usetex'] = True
fig_test.text(.5, .5, "% a comment")
@check_figures_equal()
def test_unicode_minus(fig_test, fig_ref):
mpl.rcParams['text.usetex'] = True
fig_test.text(.5, .5, "$-$")
fig_ref.text(.5, .5, "\N{MINUS SIGN}")
def test_mathdefault():
plt.rcParams["axes.formatter.use_mathtext"] = True
fig = plt.figure()
fig.add_subplot().set_xlim(-1, 1)
# Check that \mathdefault commands generated by tickers don't cause
# problems when later switching usetex on.
mpl.rcParams['text.usetex'] = True
fig.canvas.draw()
@image_comparison(['eqnarray.png'])
def test_multiline_eqnarray():
text = (
r'\begin{eqnarray*}'
r'foo\\'
r'bar\\'
r'baz\\'
r'\end{eqnarray*}'
)
fig = plt.figure(figsize=(1, 1))
fig.text(0.5, 0.5, text, usetex=True,
horizontalalignment='center', verticalalignment='center')
@pytest.mark.parametrize("fontsize", [8, 10, 12])
def test_minus_no_descent(fontsize):
# Test special-casing of minus descent in DviFont._height_depth_of, by
# checking that overdrawing a 1 and a -1 results in an overall height
# equivalent to drawing either of them separately.
mpl.style.use("mpl20")
mpl.rcParams['font.size'] = fontsize
heights = {}
fig = plt.figure()
for vals in [(1,), (-1,), (-1, 1)]:
fig.clear()
for x in vals:
fig.text(.5, .5, f"${x}$", usetex=True)
fig.canvas.draw()
# The following counts the number of non-fully-blank pixel rows.
heights[vals] = ((np.array(fig.canvas.buffer_rgba())[..., 0] != 255)
.any(axis=1).sum())
assert len({*heights.values()}) == 1
@pytest.mark.parametrize('pkg', ['xcolor', 'chemformula'])
def test_usetex_packages(pkg):
if not _has_tex_package(pkg):
pytest.skip(f'{pkg} is not available')
mpl.rcParams['text.usetex'] = True
fig = plt.figure()
text = fig.text(0.5, 0.5, "Some text 0123456789")
fig.canvas.draw()
mpl.rcParams['text.latex.preamble'] = (
r'\PassOptionsToPackage{dvipsnames}{xcolor}\usepackage{%s}' % pkg)
fig = plt.figure()
text2 = fig.text(0.5, 0.5, "Some text 0123456789")
fig.canvas.draw()
np.testing.assert_array_equal(text2.get_window_extent(),
text.get_window_extent())
@pytest.mark.parametrize(
"preamble",
[r"\usepackage[full]{textcomp}", r"\usepackage{underscore}"],
)
def test_latex_pkg_already_loaded(preamble):
plt.rcParams["text.latex.preamble"] = preamble
fig = plt.figure()
fig.text(.5, .5, "hello, world", usetex=True)
fig.canvas.draw()
def test_usetex_with_underscore():
plt.rcParams["text.usetex"] = True
df = {"a_b": range(5)[::-1], "c": range(5)}
fig, ax = plt.subplots()
ax.plot("c", "a_b", data=df)
ax.legend()
ax.text(0, 0, "foo_bar", usetex=True)
plt.draw()
@pytest.mark.flaky(reruns=3) # Tends to hit a TeX cache lock on AppVeyor.
@pytest.mark.parametrize("fmt", ["pdf", "svg"])
def test_missing_psfont(fmt, monkeypatch):
"""An error is raised if a TeX font lacks a Type-1 equivalent"""
monkeypatch.setattr(
dviread.PsfontsMap, '__getitem__',
lambda self, k: dviread.PsFont(
texname=b'texfont', psname=b'Some Font',
effects=None, encoding=None, filename=None))
mpl.rcParams['text.usetex'] = True
fig, ax = plt.subplots()
ax.text(0.5, 0.5, 'hello')
with TemporaryFile() as tmpfile, pytest.raises(ValueError):
fig.savefig(tmpfile, format=fmt)
try:
_old_gs_version = mpl._get_executable_info('gs').version < parse_version('9.55')
except mpl.ExecutableNotFoundError:
_old_gs_version = True
@image_comparison(baseline_images=['rotation'], extensions=['eps', 'pdf', 'png', 'svg'],
style='mpl20', tol=3.91 if _old_gs_version else 0)
def test_rotation():
mpl.rcParams['text.usetex'] = True
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
ax.set(xlim=[-0.5, 5], xticks=[], ylim=[-0.5, 3], yticks=[], frame_on=False)
text = {val: val[0] for val in ['top', 'center', 'bottom', 'left', 'right']}
text['baseline'] = 'B'
text['center_baseline'] = 'C'
for i, va in enumerate(['top', 'center', 'bottom', 'baseline', 'center_baseline']):
for j, ha in enumerate(['left', 'center', 'right']):
for k, angle in enumerate([0, 90, 180, 270]):
k //= 2
x = i + k / 2
y = j + k / 2
ax.plot(x, y, '+', c=f'C{k}', markersize=20, markeredgewidth=0.5)
# 'My' checks full height letters plus descenders.
ax.text(x, y, f"$\\mathrm{{My {text[ha]}{text[va]} {angle}}}$",
rotation=angle, horizontalalignment=ha, verticalalignment=va)
File diff suppressed because it is too large Load Diff