test_contour.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899
  1. import datetime
  2. import platform
  3. import re
  4. from unittest import mock
  5. import contourpy
  6. import numpy as np
  7. from numpy.testing import (
  8. assert_array_almost_equal, assert_array_almost_equal_nulp, assert_array_equal)
  9. import matplotlib as mpl
  10. from matplotlib import pyplot as plt, rc_context, ticker
  11. from matplotlib.colors import LogNorm, same_color
  12. import matplotlib.patches as mpatches
  13. from matplotlib.testing.decorators import check_figures_equal, image_comparison
  14. import pytest
  15. # Helper to test the transition from ContourSets holding multiple Collections to being a
  16. # single Collection; remove once the deprecated old layout expires.
  17. def _maybe_split_collections(do_split):
  18. if not do_split:
  19. return
  20. for fig in map(plt.figure, plt.get_fignums()):
  21. for ax in fig.axes:
  22. for coll in ax.collections:
  23. if isinstance(coll, mpl.contour.ContourSet):
  24. with pytest.warns(mpl._api.MatplotlibDeprecationWarning):
  25. coll.collections
  26. def test_contour_shape_1d_valid():
  27. x = np.arange(10)
  28. y = np.arange(9)
  29. z = np.random.random((9, 10))
  30. fig, ax = plt.subplots()
  31. ax.contour(x, y, z)
  32. def test_contour_shape_2d_valid():
  33. x = np.arange(10)
  34. y = np.arange(9)
  35. xg, yg = np.meshgrid(x, y)
  36. z = np.random.random((9, 10))
  37. fig, ax = plt.subplots()
  38. ax.contour(xg, yg, z)
  39. @pytest.mark.parametrize("args, message", [
  40. ((np.arange(9), np.arange(9), np.empty((9, 10))),
  41. 'Length of x (9) must match number of columns in z (10)'),
  42. ((np.arange(10), np.arange(10), np.empty((9, 10))),
  43. 'Length of y (10) must match number of rows in z (9)'),
  44. ((np.empty((10, 10)), np.arange(10), np.empty((9, 10))),
  45. 'Number of dimensions of x (2) and y (1) do not match'),
  46. ((np.arange(10), np.empty((10, 10)), np.empty((9, 10))),
  47. 'Number of dimensions of x (1) and y (2) do not match'),
  48. ((np.empty((9, 9)), np.empty((9, 10)), np.empty((9, 10))),
  49. 'Shapes of x (9, 9) and z (9, 10) do not match'),
  50. ((np.empty((9, 10)), np.empty((9, 9)), np.empty((9, 10))),
  51. 'Shapes of y (9, 9) and z (9, 10) do not match'),
  52. ((np.empty((3, 3, 3)), np.empty((3, 3, 3)), np.empty((9, 10))),
  53. 'Inputs x and y must be 1D or 2D, not 3D'),
  54. ((np.empty((3, 3, 3)), np.empty((3, 3, 3)), np.empty((3, 3, 3))),
  55. 'Input z must be 2D, not 3D'),
  56. (([[0]],), # github issue 8197
  57. 'Input z must be at least a (2, 2) shaped array, but has shape (1, 1)'),
  58. (([0], [0], [[0]]),
  59. 'Input z must be at least a (2, 2) shaped array, but has shape (1, 1)'),
  60. ])
  61. def test_contour_shape_error(args, message):
  62. fig, ax = plt.subplots()
  63. with pytest.raises(TypeError, match=re.escape(message)):
  64. ax.contour(*args)
  65. def test_contour_no_valid_levels():
  66. fig, ax = plt.subplots()
  67. # no warning for empty levels.
  68. ax.contour(np.random.rand(9, 9), levels=[])
  69. # no warning if levels is given and is not within the range of z.
  70. cs = ax.contour(np.arange(81).reshape((9, 9)), levels=[100])
  71. # ... and if fmt is given.
  72. ax.clabel(cs, fmt={100: '%1.2f'})
  73. # no warning if z is uniform.
  74. ax.contour(np.ones((9, 9)))
  75. def test_contour_Nlevels():
  76. # A scalar levels arg or kwarg should trigger auto level generation.
  77. # https://github.com/matplotlib/matplotlib/issues/11913
  78. z = np.arange(12).reshape((3, 4))
  79. fig, ax = plt.subplots()
  80. cs1 = ax.contour(z, 5)
  81. assert len(cs1.levels) > 1
  82. cs2 = ax.contour(z, levels=5)
  83. assert (cs1.levels == cs2.levels).all()
  84. @check_figures_equal(extensions=['png'])
  85. def test_contour_set_paths(fig_test, fig_ref):
  86. cs_test = fig_test.subplots().contour([[0, 1], [1, 2]])
  87. cs_ref = fig_ref.subplots().contour([[1, 0], [2, 1]])
  88. cs_test.set_paths(cs_ref.get_paths())
  89. @pytest.mark.parametrize("split_collections", [False, True])
  90. @image_comparison(['contour_manual_labels'], remove_text=True, style='mpl20', tol=0.26)
  91. def test_contour_manual_labels(split_collections):
  92. x, y = np.meshgrid(np.arange(0, 10), np.arange(0, 10))
  93. z = np.max(np.dstack([abs(x), abs(y)]), 2)
  94. plt.figure(figsize=(6, 2), dpi=200)
  95. cs = plt.contour(x, y, z)
  96. _maybe_split_collections(split_collections)
  97. pts = np.array([(1.0, 3.0), (1.0, 4.4), (1.0, 6.0)])
  98. plt.clabel(cs, manual=pts)
  99. pts = np.array([(2.0, 3.0), (2.0, 4.4), (2.0, 6.0)])
  100. plt.clabel(cs, manual=pts, fontsize='small', colors=('r', 'g'))
  101. def test_contour_manual_moveto():
  102. x = np.linspace(-10, 10)
  103. y = np.linspace(-10, 10)
  104. X, Y = np.meshgrid(x, y)
  105. Z = X**2 * 1 / Y**2 - 1
  106. contours = plt.contour(X, Y, Z, levels=[0, 100])
  107. # This point lies on the `MOVETO` line for the 100 contour
  108. # but is actually closest to the 0 contour
  109. point = (1.3, 1)
  110. clabels = plt.clabel(contours, manual=[point])
  111. # Ensure that the 0 contour was chosen, not the 100 contour
  112. assert clabels[0].get_text() == "0"
  113. @pytest.mark.parametrize("split_collections", [False, True])
  114. @image_comparison(['contour_disconnected_segments'],
  115. remove_text=True, style='mpl20', extensions=['png'])
  116. def test_contour_label_with_disconnected_segments(split_collections):
  117. x, y = np.mgrid[-1:1:21j, -1:1:21j]
  118. z = 1 / np.sqrt(0.01 + (x + 0.3) ** 2 + y ** 2)
  119. z += 1 / np.sqrt(0.01 + (x - 0.3) ** 2 + y ** 2)
  120. plt.figure()
  121. cs = plt.contour(x, y, z, levels=[7])
  122. # Adding labels should invalidate the old style
  123. _maybe_split_collections(split_collections)
  124. cs.clabel(manual=[(0.2, 0.1)])
  125. _maybe_split_collections(split_collections)
  126. @pytest.mark.parametrize("split_collections", [False, True])
  127. @image_comparison(['contour_manual_colors_and_levels.png'], remove_text=True)
  128. def test_given_colors_levels_and_extends(split_collections):
  129. # Remove this line when this test image is regenerated.
  130. plt.rcParams['pcolormesh.snap'] = False
  131. _, axs = plt.subplots(2, 4)
  132. data = np.arange(12).reshape(3, 4)
  133. colors = ['red', 'yellow', 'pink', 'blue', 'black']
  134. levels = [2, 4, 8, 10]
  135. for i, ax in enumerate(axs.flat):
  136. filled = i % 2 == 0.
  137. extend = ['neither', 'min', 'max', 'both'][i // 2]
  138. if filled:
  139. # If filled, we have 3 colors with no extension,
  140. # 4 colors with one extension, and 5 colors with both extensions
  141. first_color = 1 if extend in ['max', 'neither'] else None
  142. last_color = -1 if extend in ['min', 'neither'] else None
  143. c = ax.contourf(data, colors=colors[first_color:last_color],
  144. levels=levels, extend=extend)
  145. else:
  146. # If not filled, we have 4 levels and 4 colors
  147. c = ax.contour(data, colors=colors[:-1],
  148. levels=levels, extend=extend)
  149. plt.colorbar(c, ax=ax)
  150. _maybe_split_collections(split_collections)
  151. @pytest.mark.parametrize("split_collections", [False, True])
  152. @image_comparison(['contour_log_locator.svg'], style='mpl20', remove_text=False)
  153. def test_log_locator_levels(split_collections):
  154. fig, ax = plt.subplots()
  155. N = 100
  156. x = np.linspace(-3.0, 3.0, N)
  157. y = np.linspace(-2.0, 2.0, N)
  158. X, Y = np.meshgrid(x, y)
  159. Z1 = np.exp(-X**2 - Y**2)
  160. Z2 = np.exp(-(X * 10)**2 - (Y * 10)**2)
  161. data = Z1 + 50 * Z2
  162. c = ax.contourf(data, locator=ticker.LogLocator())
  163. assert_array_almost_equal(c.levels, np.power(10.0, np.arange(-6, 3)))
  164. cb = fig.colorbar(c, ax=ax)
  165. assert_array_almost_equal(cb.ax.get_yticks(), c.levels)
  166. _maybe_split_collections(split_collections)
  167. @pytest.mark.parametrize("split_collections", [False, True])
  168. @image_comparison(['contour_datetime_axis.png'], style='mpl20')
  169. def test_contour_datetime_axis(split_collections):
  170. fig = plt.figure()
  171. fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15)
  172. base = datetime.datetime(2013, 1, 1)
  173. x = np.array([base + datetime.timedelta(days=d) for d in range(20)])
  174. y = np.arange(20)
  175. z1, z2 = np.meshgrid(np.arange(20), np.arange(20))
  176. z = z1 * z2
  177. plt.subplot(221)
  178. plt.contour(x, y, z)
  179. plt.subplot(222)
  180. plt.contourf(x, y, z)
  181. x = np.repeat(x[np.newaxis], 20, axis=0)
  182. y = np.repeat(y[:, np.newaxis], 20, axis=1)
  183. plt.subplot(223)
  184. plt.contour(x, y, z)
  185. plt.subplot(224)
  186. plt.contourf(x, y, z)
  187. for ax in fig.get_axes():
  188. for label in ax.get_xticklabels():
  189. label.set_ha('right')
  190. label.set_rotation(30)
  191. _maybe_split_collections(split_collections)
  192. @pytest.mark.parametrize("split_collections", [False, True])
  193. @image_comparison(['contour_test_label_transforms.png'],
  194. remove_text=True, style='mpl20', tol=1.1)
  195. def test_labels(split_collections):
  196. # Adapted from pylab_examples example code: contour_demo.py
  197. # see issues #2475, #2843, and #2818 for explanation
  198. delta = 0.025
  199. x = np.arange(-3.0, 3.0, delta)
  200. y = np.arange(-2.0, 2.0, delta)
  201. X, Y = np.meshgrid(x, y)
  202. Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi)
  203. Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) /
  204. (2 * np.pi * 0.5 * 1.5))
  205. # difference of Gaussians
  206. Z = 10.0 * (Z2 - Z1)
  207. fig, ax = plt.subplots(1, 1)
  208. CS = ax.contour(X, Y, Z)
  209. disp_units = [(216, 177), (359, 290), (521, 406)]
  210. data_units = [(-2, .5), (0, -1.5), (2.8, 1)]
  211. # Adding labels should invalidate the old style
  212. _maybe_split_collections(split_collections)
  213. CS.clabel()
  214. for x, y in data_units:
  215. CS.add_label_near(x, y, inline=True, transform=None)
  216. for x, y in disp_units:
  217. CS.add_label_near(x, y, inline=True, transform=False)
  218. _maybe_split_collections(split_collections)
  219. def test_label_contour_start():
  220. # Set up data and figure/axes that result in automatic labelling adding the
  221. # label to the start of a contour
  222. _, ax = plt.subplots(dpi=100)
  223. lats = lons = np.linspace(-np.pi / 2, np.pi / 2, 50)
  224. lons, lats = np.meshgrid(lons, lats)
  225. wave = 0.75 * (np.sin(2 * lats) ** 8) * np.cos(4 * lons)
  226. mean = 0.5 * np.cos(2 * lats) * ((np.sin(2 * lats)) ** 2 + 2)
  227. data = wave + mean
  228. cs = ax.contour(lons, lats, data)
  229. with mock.patch.object(
  230. cs, '_split_path_and_get_label_rotation',
  231. wraps=cs._split_path_and_get_label_rotation) as mocked_splitter:
  232. # Smoke test that we can add the labels
  233. cs.clabel(fontsize=9)
  234. # Verify at least one label was added to the start of a contour. I.e. the
  235. # splitting method was called with idx=0 at least once.
  236. idxs = [cargs[0][1] for cargs in mocked_splitter.call_args_list]
  237. assert 0 in idxs
  238. @pytest.mark.parametrize("split_collections", [False, True])
  239. @image_comparison(['contour_corner_mask_False.png', 'contour_corner_mask_True.png'],
  240. remove_text=True, tol=1.88)
  241. def test_corner_mask(split_collections):
  242. n = 60
  243. mask_level = 0.95
  244. noise_amp = 1.0
  245. np.random.seed([1])
  246. x, y = np.meshgrid(np.linspace(0, 2.0, n), np.linspace(0, 2.0, n))
  247. z = np.cos(7*x)*np.sin(8*y) + noise_amp*np.random.rand(n, n)
  248. mask = np.random.rand(n, n) >= mask_level
  249. z = np.ma.array(z, mask=mask)
  250. for corner_mask in [False, True]:
  251. plt.figure()
  252. plt.contourf(z, corner_mask=corner_mask)
  253. _maybe_split_collections(split_collections)
  254. def test_contourf_decreasing_levels():
  255. # github issue 5477.
  256. z = [[0.1, 0.3], [0.5, 0.7]]
  257. plt.figure()
  258. with pytest.raises(ValueError):
  259. plt.contourf(z, [1.0, 0.0])
  260. def test_contourf_symmetric_locator():
  261. # github issue 7271
  262. z = np.arange(12).reshape((3, 4))
  263. locator = plt.MaxNLocator(nbins=4, symmetric=True)
  264. cs = plt.contourf(z, locator=locator)
  265. assert_array_almost_equal(cs.levels, np.linspace(-12, 12, 5))
  266. def test_circular_contour_warning():
  267. # Check that almost circular contours don't throw a warning
  268. x, y = np.meshgrid(np.linspace(-2, 2, 4), np.linspace(-2, 2, 4))
  269. r = np.hypot(x, y)
  270. plt.figure()
  271. cs = plt.contour(x, y, r)
  272. plt.clabel(cs)
  273. @pytest.mark.parametrize("use_clabeltext, contour_zorder, clabel_zorder",
  274. [(True, 123, 1234), (False, 123, 1234),
  275. (True, 123, None), (False, 123, None)])
  276. def test_clabel_zorder(use_clabeltext, contour_zorder, clabel_zorder):
  277. x, y = np.meshgrid(np.arange(0, 10), np.arange(0, 10))
  278. z = np.max(np.dstack([abs(x), abs(y)]), 2)
  279. fig, (ax1, ax2) = plt.subplots(ncols=2)
  280. cs = ax1.contour(x, y, z, zorder=contour_zorder)
  281. cs_filled = ax2.contourf(x, y, z, zorder=contour_zorder)
  282. clabels1 = cs.clabel(zorder=clabel_zorder, use_clabeltext=use_clabeltext)
  283. clabels2 = cs_filled.clabel(zorder=clabel_zorder,
  284. use_clabeltext=use_clabeltext)
  285. if clabel_zorder is None:
  286. expected_clabel_zorder = 2+contour_zorder
  287. else:
  288. expected_clabel_zorder = clabel_zorder
  289. for clabel in clabels1:
  290. assert clabel.get_zorder() == expected_clabel_zorder
  291. for clabel in clabels2:
  292. assert clabel.get_zorder() == expected_clabel_zorder
  293. def test_clabel_with_large_spacing():
  294. # When the inline spacing is large relative to the contour, it may cause the
  295. # entire contour to be removed. In current implementation, one line segment is
  296. # retained between the identified points.
  297. # This behavior may be worth reconsidering, but check to be sure we do not produce
  298. # an invalid path, which results in an error at clabel call time.
  299. # see gh-27045 for more information
  300. x = y = np.arange(-3.0, 3.01, 0.05)
  301. X, Y = np.meshgrid(x, y)
  302. Z = np.exp(-X**2 - Y**2)
  303. fig, ax = plt.subplots()
  304. contourset = ax.contour(X, Y, Z, levels=[0.01, 0.2, .5, .8])
  305. ax.clabel(contourset, inline_spacing=100)
  306. # tol because ticks happen to fall on pixel boundaries so small
  307. # floating point changes in tick location flip which pixel gets
  308. # the tick.
  309. @pytest.mark.parametrize("split_collections", [False, True])
  310. @image_comparison(['contour_log_extension.png'],
  311. remove_text=True, style='mpl20',
  312. tol=1.444)
  313. def test_contourf_log_extension(split_collections):
  314. # Remove this line when this test image is regenerated.
  315. plt.rcParams['pcolormesh.snap'] = False
  316. # Test that contourf with lognorm is extended correctly
  317. fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(10, 5))
  318. fig.subplots_adjust(left=0.05, right=0.95)
  319. # make data set with large range e.g. between 1e-8 and 1e10
  320. data_exp = np.linspace(-7.5, 9.5, 1200)
  321. data = np.power(10, data_exp).reshape(30, 40)
  322. # make manual levels e.g. between 1e-4 and 1e-6
  323. levels_exp = np.arange(-4., 7.)
  324. levels = np.power(10., levels_exp)
  325. # original data
  326. c1 = ax1.contourf(data,
  327. norm=LogNorm(vmin=data.min(), vmax=data.max()))
  328. # just show data in levels
  329. c2 = ax2.contourf(data, levels=levels,
  330. norm=LogNorm(vmin=levels.min(), vmax=levels.max()),
  331. extend='neither')
  332. # extend data from levels
  333. c3 = ax3.contourf(data, levels=levels,
  334. norm=LogNorm(vmin=levels.min(), vmax=levels.max()),
  335. extend='both')
  336. cb = plt.colorbar(c1, ax=ax1)
  337. assert cb.ax.get_ylim() == (1e-8, 1e10)
  338. cb = plt.colorbar(c2, ax=ax2)
  339. assert_array_almost_equal_nulp(cb.ax.get_ylim(), np.array((1e-4, 1e6)))
  340. cb = plt.colorbar(c3, ax=ax3)
  341. _maybe_split_collections(split_collections)
  342. @pytest.mark.parametrize("split_collections", [False, True])
  343. @image_comparison(
  344. ['contour_addlines.png'], remove_text=True, style='mpl20',
  345. tol=0.15 if platform.machine() in ('aarch64', 'ppc64le', 's390x')
  346. else 0.03)
  347. # tolerance is because image changed minutely when tick finding on
  348. # colorbars was cleaned up...
  349. def test_contour_addlines(split_collections):
  350. # Remove this line when this test image is regenerated.
  351. plt.rcParams['pcolormesh.snap'] = False
  352. fig, ax = plt.subplots()
  353. np.random.seed(19680812)
  354. X = np.random.rand(10, 10)*10000
  355. pcm = ax.pcolormesh(X)
  356. # add 1000 to make colors visible...
  357. cont = ax.contour(X+1000)
  358. cb = fig.colorbar(pcm)
  359. cb.add_lines(cont)
  360. assert_array_almost_equal(cb.ax.get_ylim(), [114.3091, 9972.30735], 3)
  361. _maybe_split_collections(split_collections)
  362. @pytest.mark.parametrize("split_collections", [False, True])
  363. @image_comparison(baseline_images=['contour_uneven'],
  364. extensions=['png'], remove_text=True, style='mpl20')
  365. def test_contour_uneven(split_collections):
  366. # Remove this line when this test image is regenerated.
  367. plt.rcParams['pcolormesh.snap'] = False
  368. z = np.arange(24).reshape(4, 6)
  369. fig, axs = plt.subplots(1, 2)
  370. ax = axs[0]
  371. cs = ax.contourf(z, levels=[2, 4, 6, 10, 20])
  372. fig.colorbar(cs, ax=ax, spacing='proportional')
  373. ax = axs[1]
  374. cs = ax.contourf(z, levels=[2, 4, 6, 10, 20])
  375. fig.colorbar(cs, ax=ax, spacing='uniform')
  376. _maybe_split_collections(split_collections)
  377. @pytest.mark.parametrize(
  378. "rc_lines_linewidth, rc_contour_linewidth, call_linewidths, expected", [
  379. (1.23, None, None, 1.23),
  380. (1.23, 4.24, None, 4.24),
  381. (1.23, 4.24, 5.02, 5.02)
  382. ])
  383. def test_contour_linewidth(
  384. rc_lines_linewidth, rc_contour_linewidth, call_linewidths, expected):
  385. with rc_context(rc={"lines.linewidth": rc_lines_linewidth,
  386. "contour.linewidth": rc_contour_linewidth}):
  387. fig, ax = plt.subplots()
  388. X = np.arange(4*3).reshape(4, 3)
  389. cs = ax.contour(X, linewidths=call_linewidths)
  390. assert cs.get_linewidths()[0] == expected
  391. with pytest.warns(mpl.MatplotlibDeprecationWarning, match="tlinewidths"):
  392. assert cs.tlinewidths[0][0] == expected
  393. @pytest.mark.backend("pdf")
  394. def test_label_nonagg():
  395. # This should not crash even if the canvas doesn't have a get_renderer().
  396. plt.clabel(plt.contour([[1, 2], [3, 4]]))
  397. @pytest.mark.parametrize("split_collections", [False, True])
  398. @image_comparison(baseline_images=['contour_closed_line_loop'],
  399. extensions=['png'], remove_text=True)
  400. def test_contour_closed_line_loop(split_collections):
  401. # github issue 19568.
  402. z = [[0, 0, 0], [0, 2, 0], [0, 0, 0], [2, 1, 2]]
  403. fig, ax = plt.subplots(figsize=(2, 2))
  404. ax.contour(z, [0.5], linewidths=[20], alpha=0.7)
  405. ax.set_xlim(-0.1, 2.1)
  406. ax.set_ylim(-0.1, 3.1)
  407. _maybe_split_collections(split_collections)
  408. def test_quadcontourset_reuse():
  409. # If QuadContourSet returned from one contour(f) call is passed as first
  410. # argument to another the underlying C++ contour generator will be reused.
  411. x, y = np.meshgrid([0.0, 1.0], [0.0, 1.0])
  412. z = x + y
  413. fig, ax = plt.subplots()
  414. qcs1 = ax.contourf(x, y, z)
  415. qcs2 = ax.contour(x, y, z)
  416. assert qcs2._contour_generator != qcs1._contour_generator
  417. qcs3 = ax.contour(qcs1, z)
  418. assert qcs3._contour_generator == qcs1._contour_generator
  419. @pytest.mark.parametrize("split_collections", [False, True])
  420. @image_comparison(baseline_images=['contour_manual'],
  421. extensions=['png'], remove_text=True, tol=0.89)
  422. def test_contour_manual(split_collections):
  423. # Manually specifying contour lines/polygons to plot.
  424. from matplotlib.contour import ContourSet
  425. fig, ax = plt.subplots(figsize=(4, 4))
  426. cmap = 'viridis'
  427. # Segments only (no 'kind' codes).
  428. lines0 = [[[2, 0], [1, 2], [1, 3]]] # Single line.
  429. lines1 = [[[3, 0], [3, 2]], [[3, 3], [3, 4]]] # Two lines.
  430. filled01 = [[[0, 0], [0, 4], [1, 3], [1, 2], [2, 0]]]
  431. filled12 = [[[2, 0], [3, 0], [3, 2], [1, 3], [1, 2]], # Two polygons.
  432. [[1, 4], [3, 4], [3, 3]]]
  433. ContourSet(ax, [0, 1, 2], [filled01, filled12], filled=True, cmap=cmap)
  434. ContourSet(ax, [1, 2], [lines0, lines1], linewidths=3, colors=['r', 'k'])
  435. # Segments and kind codes (1 = MOVETO, 2 = LINETO, 79 = CLOSEPOLY).
  436. segs = [[[4, 0], [7, 0], [7, 3], [4, 3], [4, 0],
  437. [5, 1], [5, 2], [6, 2], [6, 1], [5, 1]]]
  438. kinds = [[1, 2, 2, 2, 79, 1, 2, 2, 2, 79]] # Polygon containing hole.
  439. ContourSet(ax, [2, 3], [segs], [kinds], filled=True, cmap=cmap)
  440. ContourSet(ax, [2], [segs], [kinds], colors='k', linewidths=3)
  441. _maybe_split_collections(split_collections)
  442. @pytest.mark.parametrize("split_collections", [False, True])
  443. @image_comparison(baseline_images=['contour_line_start_on_corner_edge'],
  444. extensions=['png'], remove_text=True)
  445. def test_contour_line_start_on_corner_edge(split_collections):
  446. fig, ax = plt.subplots(figsize=(6, 5))
  447. x, y = np.meshgrid([0, 1, 2, 3, 4], [0, 1, 2])
  448. z = 1.2 - (x - 2)**2 + (y - 1)**2
  449. mask = np.zeros_like(z, dtype=bool)
  450. mask[1, 1] = mask[1, 3] = True
  451. z = np.ma.array(z, mask=mask)
  452. filled = ax.contourf(x, y, z, corner_mask=True)
  453. cbar = fig.colorbar(filled)
  454. lines = ax.contour(x, y, z, corner_mask=True, colors='k')
  455. cbar.add_lines(lines)
  456. _maybe_split_collections(split_collections)
  457. def test_find_nearest_contour():
  458. xy = np.indices((15, 15))
  459. img = np.exp(-np.pi * (np.sum((xy - 5)**2, 0)/5.**2))
  460. cs = plt.contour(img, 10)
  461. nearest_contour = cs.find_nearest_contour(1, 1, pixel=False)
  462. expected_nearest = (1, 0, 33, 1.965966, 1.965966, 1.866183)
  463. assert_array_almost_equal(nearest_contour, expected_nearest)
  464. nearest_contour = cs.find_nearest_contour(8, 1, pixel=False)
  465. expected_nearest = (1, 0, 5, 7.550173, 1.587542, 0.547550)
  466. assert_array_almost_equal(nearest_contour, expected_nearest)
  467. nearest_contour = cs.find_nearest_contour(2, 5, pixel=False)
  468. expected_nearest = (3, 0, 21, 1.884384, 5.023335, 0.013911)
  469. assert_array_almost_equal(nearest_contour, expected_nearest)
  470. nearest_contour = cs.find_nearest_contour(2, 5, indices=(5, 7), pixel=False)
  471. expected_nearest = (5, 0, 16, 2.628202, 5.0, 0.394638)
  472. assert_array_almost_equal(nearest_contour, expected_nearest)
  473. def test_find_nearest_contour_no_filled():
  474. xy = np.indices((15, 15))
  475. img = np.exp(-np.pi * (np.sum((xy - 5)**2, 0)/5.**2))
  476. cs = plt.contourf(img, 10)
  477. with pytest.raises(ValueError, match="Method does not support filled contours"):
  478. cs.find_nearest_contour(1, 1, pixel=False)
  479. with pytest.raises(ValueError, match="Method does not support filled contours"):
  480. cs.find_nearest_contour(1, 10, indices=(5, 7), pixel=False)
  481. with pytest.raises(ValueError, match="Method does not support filled contours"):
  482. cs.find_nearest_contour(2, 5, indices=(2, 7), pixel=True)
  483. @mpl.style.context("default")
  484. def test_contour_autolabel_beyond_powerlimits():
  485. ax = plt.figure().add_subplot()
  486. cs = plt.contour(np.geomspace(1e-6, 1e-4, 100).reshape(10, 10),
  487. levels=[.25e-5, 1e-5, 4e-5])
  488. ax.clabel(cs)
  489. # Currently, the exponent is missing, but that may be fixed in the future.
  490. assert {text.get_text() for text in ax.texts} == {"0.25", "1.00", "4.00"}
  491. def test_contourf_legend_elements():
  492. from matplotlib.patches import Rectangle
  493. x = np.arange(1, 10)
  494. y = x.reshape(-1, 1)
  495. h = x * y
  496. cs = plt.contourf(h, levels=[10, 30, 50],
  497. colors=['#FFFF00', '#FF00FF', '#00FFFF'],
  498. extend='both')
  499. cs.cmap.set_over('red')
  500. cs.cmap.set_under('blue')
  501. cs.changed()
  502. artists, labels = cs.legend_elements()
  503. assert labels == ['$x \\leq -1e+250s$',
  504. '$10.0 < x \\leq 30.0$',
  505. '$30.0 < x \\leq 50.0$',
  506. '$x > 1e+250s$']
  507. expected_colors = ('blue', '#FFFF00', '#FF00FF', 'red')
  508. assert all(isinstance(a, Rectangle) for a in artists)
  509. assert all(same_color(a.get_facecolor(), c)
  510. for a, c in zip(artists, expected_colors))
  511. def test_contour_legend_elements():
  512. x = np.arange(1, 10)
  513. y = x.reshape(-1, 1)
  514. h = x * y
  515. colors = ['blue', '#00FF00', 'red']
  516. cs = plt.contour(h, levels=[10, 30, 50],
  517. colors=colors,
  518. extend='both')
  519. artists, labels = cs.legend_elements()
  520. assert labels == ['$x = 10.0$', '$x = 30.0$', '$x = 50.0$']
  521. assert all(isinstance(a, mpl.lines.Line2D) for a in artists)
  522. assert all(same_color(a.get_color(), c)
  523. for a, c in zip(artists, colors))
  524. @pytest.mark.parametrize(
  525. "algorithm, klass",
  526. [('mpl2005', contourpy.Mpl2005ContourGenerator),
  527. ('mpl2014', contourpy.Mpl2014ContourGenerator),
  528. ('serial', contourpy.SerialContourGenerator),
  529. ('threaded', contourpy.ThreadedContourGenerator),
  530. ('invalid', None)])
  531. def test_algorithm_name(algorithm, klass):
  532. z = np.array([[1.0, 2.0], [3.0, 4.0]])
  533. if klass is not None:
  534. cs = plt.contourf(z, algorithm=algorithm)
  535. assert isinstance(cs._contour_generator, klass)
  536. else:
  537. with pytest.raises(ValueError):
  538. plt.contourf(z, algorithm=algorithm)
  539. @pytest.mark.parametrize(
  540. "algorithm", ['mpl2005', 'mpl2014', 'serial', 'threaded'])
  541. def test_algorithm_supports_corner_mask(algorithm):
  542. z = np.array([[1.0, 2.0], [3.0, 4.0]])
  543. # All algorithms support corner_mask=False
  544. plt.contourf(z, algorithm=algorithm, corner_mask=False)
  545. # Only some algorithms support corner_mask=True
  546. if algorithm != 'mpl2005':
  547. plt.contourf(z, algorithm=algorithm, corner_mask=True)
  548. else:
  549. with pytest.raises(ValueError):
  550. plt.contourf(z, algorithm=algorithm, corner_mask=True)
  551. @pytest.mark.parametrize("split_collections", [False, True])
  552. @image_comparison(baseline_images=['contour_all_algorithms'],
  553. extensions=['png'], remove_text=True, tol=0.06)
  554. def test_all_algorithms(split_collections):
  555. algorithms = ['mpl2005', 'mpl2014', 'serial', 'threaded']
  556. rng = np.random.default_rng(2981)
  557. x, y = np.meshgrid(np.linspace(0.0, 1.0, 10), np.linspace(0.0, 1.0, 6))
  558. z = np.sin(15*x)*np.cos(10*y) + rng.normal(scale=0.5, size=(6, 10))
  559. mask = np.zeros_like(z, dtype=bool)
  560. mask[3, 7] = True
  561. z = np.ma.array(z, mask=mask)
  562. _, axs = plt.subplots(2, 2)
  563. for ax, algorithm in zip(axs.ravel(), algorithms):
  564. ax.contourf(x, y, z, algorithm=algorithm)
  565. ax.contour(x, y, z, algorithm=algorithm, colors='k')
  566. ax.set_title(algorithm)
  567. _maybe_split_collections(split_collections)
  568. def test_subfigure_clabel():
  569. # Smoke test for gh#23173
  570. delta = 0.025
  571. x = np.arange(-3.0, 3.0, delta)
  572. y = np.arange(-2.0, 2.0, delta)
  573. X, Y = np.meshgrid(x, y)
  574. Z1 = np.exp(-(X**2) - Y**2)
  575. Z2 = np.exp(-((X - 1) ** 2) - (Y - 1) ** 2)
  576. Z = (Z1 - Z2) * 2
  577. fig = plt.figure()
  578. figs = fig.subfigures(nrows=1, ncols=2)
  579. for f in figs:
  580. ax = f.subplots()
  581. CS = ax.contour(X, Y, Z)
  582. ax.clabel(CS, inline=True, fontsize=10)
  583. ax.set_title("Simplest default with labels")
  584. @pytest.mark.parametrize(
  585. "style", ['solid', 'dashed', 'dashdot', 'dotted'])
  586. def test_linestyles(style):
  587. delta = 0.025
  588. x = np.arange(-3.0, 3.0, delta)
  589. y = np.arange(-2.0, 2.0, delta)
  590. X, Y = np.meshgrid(x, y)
  591. Z1 = np.exp(-X**2 - Y**2)
  592. Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
  593. Z = (Z1 - Z2) * 2
  594. # Positive contour defaults to solid
  595. fig1, ax1 = plt.subplots()
  596. CS1 = ax1.contour(X, Y, Z, 6, colors='k')
  597. ax1.clabel(CS1, fontsize=9, inline=True)
  598. ax1.set_title('Single color - positive contours solid (default)')
  599. assert CS1.linestyles is None # default
  600. # Change linestyles using linestyles kwarg
  601. fig2, ax2 = plt.subplots()
  602. CS2 = ax2.contour(X, Y, Z, 6, colors='k', linestyles=style)
  603. ax2.clabel(CS2, fontsize=9, inline=True)
  604. ax2.set_title(f'Single color - positive contours {style}')
  605. assert CS2.linestyles == style
  606. # Ensure linestyles do not change when negative_linestyles is defined
  607. fig3, ax3 = plt.subplots()
  608. CS3 = ax3.contour(X, Y, Z, 6, colors='k', linestyles=style,
  609. negative_linestyles='dashdot')
  610. ax3.clabel(CS3, fontsize=9, inline=True)
  611. ax3.set_title(f'Single color - positive contours {style}')
  612. assert CS3.linestyles == style
  613. @pytest.mark.parametrize(
  614. "style", ['solid', 'dashed', 'dashdot', 'dotted'])
  615. def test_negative_linestyles(style):
  616. delta = 0.025
  617. x = np.arange(-3.0, 3.0, delta)
  618. y = np.arange(-2.0, 2.0, delta)
  619. X, Y = np.meshgrid(x, y)
  620. Z1 = np.exp(-X**2 - Y**2)
  621. Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
  622. Z = (Z1 - Z2) * 2
  623. # Negative contour defaults to dashed
  624. fig1, ax1 = plt.subplots()
  625. CS1 = ax1.contour(X, Y, Z, 6, colors='k')
  626. ax1.clabel(CS1, fontsize=9, inline=True)
  627. ax1.set_title('Single color - negative contours dashed (default)')
  628. assert CS1.negative_linestyles == 'dashed' # default
  629. # Change negative_linestyles using rcParams
  630. plt.rcParams['contour.negative_linestyle'] = style
  631. fig2, ax2 = plt.subplots()
  632. CS2 = ax2.contour(X, Y, Z, 6, colors='k')
  633. ax2.clabel(CS2, fontsize=9, inline=True)
  634. ax2.set_title(f'Single color - negative contours {style}'
  635. '(using rcParams)')
  636. assert CS2.negative_linestyles == style
  637. # Change negative_linestyles using negative_linestyles kwarg
  638. fig3, ax3 = plt.subplots()
  639. CS3 = ax3.contour(X, Y, Z, 6, colors='k', negative_linestyles=style)
  640. ax3.clabel(CS3, fontsize=9, inline=True)
  641. ax3.set_title(f'Single color - negative contours {style}')
  642. assert CS3.negative_linestyles == style
  643. # Ensure negative_linestyles do not change when linestyles is defined
  644. fig4, ax4 = plt.subplots()
  645. CS4 = ax4.contour(X, Y, Z, 6, colors='k', linestyles='dashdot',
  646. negative_linestyles=style)
  647. ax4.clabel(CS4, fontsize=9, inline=True)
  648. ax4.set_title(f'Single color - negative contours {style}')
  649. assert CS4.negative_linestyles == style
  650. def test_contour_remove():
  651. ax = plt.figure().add_subplot()
  652. orig_children = ax.get_children()
  653. cs = ax.contour(np.arange(16).reshape((4, 4)))
  654. cs.clabel()
  655. assert ax.get_children() != orig_children
  656. cs.remove()
  657. assert ax.get_children() == orig_children
  658. def test_contour_no_args():
  659. fig, ax = plt.subplots()
  660. data = [[0, 1], [1, 0]]
  661. with pytest.raises(TypeError, match=r"contour\(\) takes from 1 to 4"):
  662. ax.contour(Z=data)
  663. def test_contour_clip_path():
  664. fig, ax = plt.subplots()
  665. data = [[0, 1], [1, 0]]
  666. circle = mpatches.Circle([0.5, 0.5], 0.5, transform=ax.transAxes)
  667. cs = ax.contour(data, clip_path=circle)
  668. assert cs.get_clip_path() is not None
  669. def test_bool_autolevel():
  670. x, y = np.random.rand(2, 9)
  671. z = (np.arange(9) % 2).reshape((3, 3)).astype(bool)
  672. m = [[False, False, False], [False, True, False], [False, False, False]]
  673. assert plt.contour(z.tolist()).levels.tolist() == [.5]
  674. assert plt.contour(z).levels.tolist() == [.5]
  675. assert plt.contour(np.ma.array(z, mask=m)).levels.tolist() == [.5]
  676. assert plt.contourf(z.tolist()).levels.tolist() == [0, .5, 1]
  677. assert plt.contourf(z).levels.tolist() == [0, .5, 1]
  678. assert plt.contourf(np.ma.array(z, mask=m)).levels.tolist() == [0, .5, 1]
  679. z = z.ravel()
  680. assert plt.tricontour(x, y, z.tolist()).levels.tolist() == [.5]
  681. assert plt.tricontour(x, y, z).levels.tolist() == [.5]
  682. assert plt.tricontourf(x, y, z.tolist()).levels.tolist() == [0, .5, 1]
  683. assert plt.tricontourf(x, y, z).levels.tolist() == [0, .5, 1]
  684. def test_all_nan():
  685. x = np.array([[np.nan, np.nan], [np.nan, np.nan]])
  686. assert_array_almost_equal(plt.contour(x).levels,
  687. [-1e-13, -7.5e-14, -5e-14, -2.4e-14, 0.0,
  688. 2.4e-14, 5e-14, 7.5e-14, 1e-13])
  689. def test_allsegs_allkinds():
  690. x, y = np.meshgrid(np.arange(0, 10, 2), np.arange(0, 10, 2))
  691. z = np.sin(x) * np.cos(y)
  692. cs = plt.contour(x, y, z, levels=[0, 0.5])
  693. # Expect two levels, the first with 5 segments and the second with 4.
  694. for result in [cs.allsegs, cs.allkinds]:
  695. assert len(result) == 2
  696. assert len(result[0]) == 5
  697. assert len(result[1]) == 4
  698. def test_deprecated_apis():
  699. cs = plt.contour(np.arange(16).reshape((4, 4)))
  700. with pytest.warns(mpl.MatplotlibDeprecationWarning, match="collections"):
  701. colls = cs.collections
  702. with pytest.warns(mpl.MatplotlibDeprecationWarning, match="tcolors"):
  703. assert_array_equal(cs.tcolors, [c.get_edgecolor() for c in colls])
  704. with pytest.warns(mpl.MatplotlibDeprecationWarning, match="tlinewidths"):
  705. assert cs.tlinewidths == [c.get_linewidth() for c in colls]
  706. with pytest.warns(mpl.MatplotlibDeprecationWarning, match="antialiased"):
  707. assert cs.antialiased
  708. with pytest.warns(mpl.MatplotlibDeprecationWarning, match="antialiased"):
  709. cs.antialiased = False
  710. with pytest.warns(mpl.MatplotlibDeprecationWarning, match="antialiased"):
  711. assert not cs.antialiased