init
This commit is contained in:
@@ -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 '<'"&>"' 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
Reference in New Issue
Block a user