common.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. """
  2. Module consolidating common testing functions for checking plotting.
  3. """
  4. from __future__ import annotations
  5. from typing import TYPE_CHECKING
  6. import numpy as np
  7. from pandas.core.dtypes.api import is_list_like
  8. import pandas as pd
  9. from pandas import Series
  10. import pandas._testing as tm
  11. if TYPE_CHECKING:
  12. from collections.abc import Sequence
  13. from matplotlib.axes import Axes
  14. def _check_legend_labels(axes, labels=None, visible=True):
  15. """
  16. Check each axes has expected legend labels
  17. Parameters
  18. ----------
  19. axes : matplotlib Axes object, or its list-like
  20. labels : list-like
  21. expected legend labels
  22. visible : bool
  23. expected legend visibility. labels are checked only when visible is
  24. True
  25. """
  26. if visible and (labels is None):
  27. raise ValueError("labels must be specified when visible is True")
  28. axes = _flatten_visible(axes)
  29. for ax in axes:
  30. if visible:
  31. assert ax.get_legend() is not None
  32. _check_text_labels(ax.get_legend().get_texts(), labels)
  33. else:
  34. assert ax.get_legend() is None
  35. def _check_legend_marker(ax, expected_markers=None, visible=True):
  36. """
  37. Check ax has expected legend markers
  38. Parameters
  39. ----------
  40. ax : matplotlib Axes object
  41. expected_markers : list-like
  42. expected legend markers
  43. visible : bool
  44. expected legend visibility. labels are checked only when visible is
  45. True
  46. """
  47. if visible and (expected_markers is None):
  48. raise ValueError("Markers must be specified when visible is True")
  49. if visible:
  50. handles, _ = ax.get_legend_handles_labels()
  51. markers = [handle.get_marker() for handle in handles]
  52. assert markers == expected_markers
  53. else:
  54. assert ax.get_legend() is None
  55. def _check_data(xp, rs):
  56. """
  57. Check each axes has identical lines
  58. Parameters
  59. ----------
  60. xp : matplotlib Axes object
  61. rs : matplotlib Axes object
  62. """
  63. import matplotlib.pyplot as plt
  64. xp_lines = xp.get_lines()
  65. rs_lines = rs.get_lines()
  66. assert len(xp_lines) == len(rs_lines)
  67. for xpl, rsl in zip(xp_lines, rs_lines):
  68. xpdata = xpl.get_xydata()
  69. rsdata = rsl.get_xydata()
  70. tm.assert_almost_equal(xpdata, rsdata)
  71. plt.close("all")
  72. def _check_visible(collections, visible=True):
  73. """
  74. Check each artist is visible or not
  75. Parameters
  76. ----------
  77. collections : matplotlib Artist or its list-like
  78. target Artist or its list or collection
  79. visible : bool
  80. expected visibility
  81. """
  82. from matplotlib.collections import Collection
  83. if not isinstance(collections, Collection) and not is_list_like(collections):
  84. collections = [collections]
  85. for patch in collections:
  86. assert patch.get_visible() == visible
  87. def _check_patches_all_filled(axes: Axes | Sequence[Axes], filled: bool = True) -> None:
  88. """
  89. Check for each artist whether it is filled or not
  90. Parameters
  91. ----------
  92. axes : matplotlib Axes object, or its list-like
  93. filled : bool
  94. expected filling
  95. """
  96. axes = _flatten_visible(axes)
  97. for ax in axes:
  98. for patch in ax.patches:
  99. assert patch.fill == filled
  100. def _get_colors_mapped(series, colors):
  101. unique = series.unique()
  102. # unique and colors length can be differed
  103. # depending on slice value
  104. mapped = dict(zip(unique, colors))
  105. return [mapped[v] for v in series.values]
  106. def _check_colors(collections, linecolors=None, facecolors=None, mapping=None):
  107. """
  108. Check each artist has expected line colors and face colors
  109. Parameters
  110. ----------
  111. collections : list-like
  112. list or collection of target artist
  113. linecolors : list-like which has the same length as collections
  114. list of expected line colors
  115. facecolors : list-like which has the same length as collections
  116. list of expected face colors
  117. mapping : Series
  118. Series used for color grouping key
  119. used for andrew_curves, parallel_coordinates, radviz test
  120. """
  121. from matplotlib import colors
  122. from matplotlib.collections import (
  123. Collection,
  124. LineCollection,
  125. PolyCollection,
  126. )
  127. from matplotlib.lines import Line2D
  128. conv = colors.ColorConverter
  129. if linecolors is not None:
  130. if mapping is not None:
  131. linecolors = _get_colors_mapped(mapping, linecolors)
  132. linecolors = linecolors[: len(collections)]
  133. assert len(collections) == len(linecolors)
  134. for patch, color in zip(collections, linecolors):
  135. if isinstance(patch, Line2D):
  136. result = patch.get_color()
  137. # Line2D may contains string color expression
  138. result = conv.to_rgba(result)
  139. elif isinstance(patch, (PolyCollection, LineCollection)):
  140. result = tuple(patch.get_edgecolor()[0])
  141. else:
  142. result = patch.get_edgecolor()
  143. expected = conv.to_rgba(color)
  144. assert result == expected
  145. if facecolors is not None:
  146. if mapping is not None:
  147. facecolors = _get_colors_mapped(mapping, facecolors)
  148. facecolors = facecolors[: len(collections)]
  149. assert len(collections) == len(facecolors)
  150. for patch, color in zip(collections, facecolors):
  151. if isinstance(patch, Collection):
  152. # returned as list of np.array
  153. result = patch.get_facecolor()[0]
  154. else:
  155. result = patch.get_facecolor()
  156. if isinstance(result, np.ndarray):
  157. result = tuple(result)
  158. expected = conv.to_rgba(color)
  159. assert result == expected
  160. def _check_text_labels(texts, expected):
  161. """
  162. Check each text has expected labels
  163. Parameters
  164. ----------
  165. texts : matplotlib Text object, or its list-like
  166. target text, or its list
  167. expected : str or list-like which has the same length as texts
  168. expected text label, or its list
  169. """
  170. if not is_list_like(texts):
  171. assert texts.get_text() == expected
  172. else:
  173. labels = [t.get_text() for t in texts]
  174. assert len(labels) == len(expected)
  175. for label, e in zip(labels, expected):
  176. assert label == e
  177. def _check_ticks_props(axes, xlabelsize=None, xrot=None, ylabelsize=None, yrot=None):
  178. """
  179. Check each axes has expected tick properties
  180. Parameters
  181. ----------
  182. axes : matplotlib Axes object, or its list-like
  183. xlabelsize : number
  184. expected xticks font size
  185. xrot : number
  186. expected xticks rotation
  187. ylabelsize : number
  188. expected yticks font size
  189. yrot : number
  190. expected yticks rotation
  191. """
  192. from matplotlib.ticker import NullFormatter
  193. axes = _flatten_visible(axes)
  194. for ax in axes:
  195. if xlabelsize is not None or xrot is not None:
  196. if isinstance(ax.xaxis.get_minor_formatter(), NullFormatter):
  197. # If minor ticks has NullFormatter, rot / fontsize are not
  198. # retained
  199. labels = ax.get_xticklabels()
  200. else:
  201. labels = ax.get_xticklabels() + ax.get_xticklabels(minor=True)
  202. for label in labels:
  203. if xlabelsize is not None:
  204. tm.assert_almost_equal(label.get_fontsize(), xlabelsize)
  205. if xrot is not None:
  206. tm.assert_almost_equal(label.get_rotation(), xrot)
  207. if ylabelsize is not None or yrot is not None:
  208. if isinstance(ax.yaxis.get_minor_formatter(), NullFormatter):
  209. labels = ax.get_yticklabels()
  210. else:
  211. labels = ax.get_yticklabels() + ax.get_yticklabels(minor=True)
  212. for label in labels:
  213. if ylabelsize is not None:
  214. tm.assert_almost_equal(label.get_fontsize(), ylabelsize)
  215. if yrot is not None:
  216. tm.assert_almost_equal(label.get_rotation(), yrot)
  217. def _check_ax_scales(axes, xaxis="linear", yaxis="linear"):
  218. """
  219. Check each axes has expected scales
  220. Parameters
  221. ----------
  222. axes : matplotlib Axes object, or its list-like
  223. xaxis : {'linear', 'log'}
  224. expected xaxis scale
  225. yaxis : {'linear', 'log'}
  226. expected yaxis scale
  227. """
  228. axes = _flatten_visible(axes)
  229. for ax in axes:
  230. assert ax.xaxis.get_scale() == xaxis
  231. assert ax.yaxis.get_scale() == yaxis
  232. def _check_axes_shape(axes, axes_num=None, layout=None, figsize=None):
  233. """
  234. Check expected number of axes is drawn in expected layout
  235. Parameters
  236. ----------
  237. axes : matplotlib Axes object, or its list-like
  238. axes_num : number
  239. expected number of axes. Unnecessary axes should be set to
  240. invisible.
  241. layout : tuple
  242. expected layout, (expected number of rows , columns)
  243. figsize : tuple
  244. expected figsize. default is matplotlib default
  245. """
  246. from pandas.plotting._matplotlib.tools import flatten_axes
  247. if figsize is None:
  248. figsize = (6.4, 4.8)
  249. visible_axes = _flatten_visible(axes)
  250. if axes_num is not None:
  251. assert len(visible_axes) == axes_num
  252. for ax in visible_axes:
  253. # check something drawn on visible axes
  254. assert len(ax.get_children()) > 0
  255. if layout is not None:
  256. x_set = set()
  257. y_set = set()
  258. for ax in flatten_axes(axes):
  259. # check axes coordinates to estimate layout
  260. points = ax.get_position().get_points()
  261. x_set.add(points[0][0])
  262. y_set.add(points[0][1])
  263. result = (len(y_set), len(x_set))
  264. assert result == layout
  265. tm.assert_numpy_array_equal(
  266. visible_axes[0].figure.get_size_inches(),
  267. np.array(figsize, dtype=np.float64),
  268. )
  269. def _flatten_visible(axes: Axes | Sequence[Axes]) -> Sequence[Axes]:
  270. """
  271. Flatten axes, and filter only visible
  272. Parameters
  273. ----------
  274. axes : matplotlib Axes object, or its list-like
  275. """
  276. from pandas.plotting._matplotlib.tools import flatten_axes
  277. axes_ndarray = flatten_axes(axes)
  278. axes = [ax for ax in axes_ndarray if ax.get_visible()]
  279. return axes
  280. def _check_has_errorbars(axes, xerr=0, yerr=0):
  281. """
  282. Check axes has expected number of errorbars
  283. Parameters
  284. ----------
  285. axes : matplotlib Axes object, or its list-like
  286. xerr : number
  287. expected number of x errorbar
  288. yerr : number
  289. expected number of y errorbar
  290. """
  291. axes = _flatten_visible(axes)
  292. for ax in axes:
  293. containers = ax.containers
  294. xerr_count = 0
  295. yerr_count = 0
  296. for c in containers:
  297. has_xerr = getattr(c, "has_xerr", False)
  298. has_yerr = getattr(c, "has_yerr", False)
  299. if has_xerr:
  300. xerr_count += 1
  301. if has_yerr:
  302. yerr_count += 1
  303. assert xerr == xerr_count
  304. assert yerr == yerr_count
  305. def _check_box_return_type(
  306. returned, return_type, expected_keys=None, check_ax_title=True
  307. ):
  308. """
  309. Check box returned type is correct
  310. Parameters
  311. ----------
  312. returned : object to be tested, returned from boxplot
  313. return_type : str
  314. return_type passed to boxplot
  315. expected_keys : list-like, optional
  316. group labels in subplot case. If not passed,
  317. the function checks assuming boxplot uses single ax
  318. check_ax_title : bool
  319. Whether to check the ax.title is the same as expected_key
  320. Intended to be checked by calling from ``boxplot``.
  321. Normal ``plot`` doesn't attach ``ax.title``, it must be disabled.
  322. """
  323. from matplotlib.axes import Axes
  324. types = {"dict": dict, "axes": Axes, "both": tuple}
  325. if expected_keys is None:
  326. # should be fixed when the returning default is changed
  327. if return_type is None:
  328. return_type = "dict"
  329. assert isinstance(returned, types[return_type])
  330. if return_type == "both":
  331. assert isinstance(returned.ax, Axes)
  332. assert isinstance(returned.lines, dict)
  333. else:
  334. # should be fixed when the returning default is changed
  335. if return_type is None:
  336. for r in _flatten_visible(returned):
  337. assert isinstance(r, Axes)
  338. return
  339. assert isinstance(returned, Series)
  340. assert sorted(returned.keys()) == sorted(expected_keys)
  341. for key, value in returned.items():
  342. assert isinstance(value, types[return_type])
  343. # check returned dict has correct mapping
  344. if return_type == "axes":
  345. if check_ax_title:
  346. assert value.get_title() == key
  347. elif return_type == "both":
  348. if check_ax_title:
  349. assert value.ax.get_title() == key
  350. assert isinstance(value.ax, Axes)
  351. assert isinstance(value.lines, dict)
  352. elif return_type == "dict":
  353. line = value["medians"][0]
  354. axes = line.axes
  355. if check_ax_title:
  356. assert axes.get_title() == key
  357. else:
  358. raise AssertionError
  359. def _check_grid_settings(obj, kinds, kws={}):
  360. # Make sure plot defaults to rcParams['axes.grid'] setting, GH 9792
  361. import matplotlib as mpl
  362. def is_grid_on():
  363. xticks = mpl.pyplot.gca().xaxis.get_major_ticks()
  364. yticks = mpl.pyplot.gca().yaxis.get_major_ticks()
  365. xoff = all(not g.gridline.get_visible() for g in xticks)
  366. yoff = all(not g.gridline.get_visible() for g in yticks)
  367. return not (xoff and yoff)
  368. spndx = 1
  369. for kind in kinds:
  370. mpl.pyplot.subplot(1, 4 * len(kinds), spndx)
  371. spndx += 1
  372. mpl.rc("axes", grid=False)
  373. obj.plot(kind=kind, **kws)
  374. assert not is_grid_on()
  375. mpl.pyplot.clf()
  376. mpl.pyplot.subplot(1, 4 * len(kinds), spndx)
  377. spndx += 1
  378. mpl.rc("axes", grid=True)
  379. obj.plot(kind=kind, grid=False, **kws)
  380. assert not is_grid_on()
  381. mpl.pyplot.clf()
  382. if kind not in ["pie", "hexbin", "scatter"]:
  383. mpl.pyplot.subplot(1, 4 * len(kinds), spndx)
  384. spndx += 1
  385. mpl.rc("axes", grid=True)
  386. obj.plot(kind=kind, **kws)
  387. assert is_grid_on()
  388. mpl.pyplot.clf()
  389. mpl.pyplot.subplot(1, 4 * len(kinds), spndx)
  390. spndx += 1
  391. mpl.rc("axes", grid=False)
  392. obj.plot(kind=kind, grid=True, **kws)
  393. assert is_grid_on()
  394. mpl.pyplot.clf()
  395. def _unpack_cycler(rcParams, field="color"):
  396. """
  397. Auxiliary function for correctly unpacking cycler after MPL >= 1.5
  398. """
  399. return [v[field] for v in rcParams["axes.prop_cycle"]]
  400. def get_x_axis(ax):
  401. return ax._shared_axes["x"]
  402. def get_y_axis(ax):
  403. return ax._shared_axes["y"]
  404. def _check_plot_works(f, default_axes=False, **kwargs):
  405. """
  406. Create plot and ensure that plot return object is valid.
  407. Parameters
  408. ----------
  409. f : func
  410. Plotting function.
  411. default_axes : bool, optional
  412. If False (default):
  413. - If `ax` not in `kwargs`, then create subplot(211) and plot there
  414. - Create new subplot(212) and plot there as well
  415. - Mind special corner case for bootstrap_plot (see `_gen_two_subplots`)
  416. If True:
  417. - Simply run plotting function with kwargs provided
  418. - All required axes instances will be created automatically
  419. - It is recommended to use it when the plotting function
  420. creates multiple axes itself. It helps avoid warnings like
  421. 'UserWarning: To output multiple subplots,
  422. the figure containing the passed axes is being cleared'
  423. **kwargs
  424. Keyword arguments passed to the plotting function.
  425. Returns
  426. -------
  427. Plot object returned by the last plotting.
  428. """
  429. import matplotlib.pyplot as plt
  430. if default_axes:
  431. gen_plots = _gen_default_plot
  432. else:
  433. gen_plots = _gen_two_subplots
  434. ret = None
  435. try:
  436. fig = kwargs.get("figure", plt.gcf())
  437. plt.clf()
  438. for ret in gen_plots(f, fig, **kwargs):
  439. tm.assert_is_valid_plot_return_object(ret)
  440. finally:
  441. plt.close(fig)
  442. return ret
  443. def _gen_default_plot(f, fig, **kwargs):
  444. """
  445. Create plot in a default way.
  446. """
  447. yield f(**kwargs)
  448. def _gen_two_subplots(f, fig, **kwargs):
  449. """
  450. Create plot on two subplots forcefully created.
  451. """
  452. if "ax" not in kwargs:
  453. fig.add_subplot(211)
  454. yield f(**kwargs)
  455. if f is pd.plotting.bootstrap_plot:
  456. assert "ax" not in kwargs
  457. else:
  458. kwargs["ax"] = fig.add_subplot(212)
  459. yield f(**kwargs)