From 776e82dcd49c899ce47b2c1d8d00ccb1578f4271 Mon Sep 17 00:00:00 2001 From: Marc Garcia Date: Sun, 9 Jun 2019 12:18:04 +0100 Subject: [PATCH 01/10] WIP/PLOT: Add option to specify the plotting backend --- pandas/core/config_init.py | 11 ++++++++++ pandas/plotting/_core.py | 43 +++++++++++++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index 7eb2b413822d9..0bddcfbf164b8 100644 --- a/pandas/core/config_init.py +++ b/pandas/core/config_init.py @@ -460,6 +460,17 @@ def use_inf_as_na_cb(key): # Plotting # --------- +plotting_backend_doc = """ +: str + The plotting backend to use. The default value is "matplotlib", the + backend provided with pandas. Other backends can be specified by + prodiving the name of the module that implements the backend. +""" + +with cf.config_prefix('plotting'): + cf.register_option('backend', defval='matplotlib', + doc=plotting_backend_doc) + register_converter_doc = """ : bool Whether to register converters with matplotlib's units registry for diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index 81f5b5cb0f74c..ab3cdefa777d2 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -1,3 +1,4 @@ +import importlib from typing import List, Type # noqa from pandas.util._decorators import Appender @@ -625,11 +626,43 @@ def _get_plot_backend(): The backend is imported lazily, as matplotlib is a soft dependency, and pandas can be used without it being installed. """ - try: - import pandas.plotting._matplotlib as plot_backend - except ImportError: - raise ImportError("matplotlib is required for plotting.") - return plot_backend + backend_str = pandas.get_option('plotting.backend') + if backend_str == 'matplotlib': + try: + import pandas.plotting._matplotlib as backend_mod + except ImportError: + raise ImportError('matplotlib is required for plotting when the ' + 'default backend is selected.') + else: + try: + mod = importlib.import_module(backend_str) + except ImportError: + raise ValueError('"{}" does not seem to be an installed module.' + 'A pandas plotting backend must be a module that ' + 'can be imported'.format(backend_str)) + + required_objs = ['LinePlot', 'BarPlot', 'BarhPlot', 'HistPlot', + 'BoxPlot', 'KdePlot', 'AreaPlot', 'PiePlot', + 'ScatterPlot', 'HexBinPlot', 'hist_series', + 'hist_frame', 'boxplot', 'boxplot_frame', + 'boxplot_frame_groupby', 'tsplot', 'table', + 'andrews_curves', 'autocorrelation_plot', + 'bootstrap_plot', 'lag_plot', 'parallel_coordinates', + 'radviz', 'scatter_matrix', 'register', 'deregister'] + missing_objs = set(required_objs) - set(dir(mod)) + if len(missing_objs) == len(required_objs): + raise ValueError( + '"{}" does not seem to be a valid backend. Valid backends are ' + 'modules that implement the next objects:\n{}'.format( + backend_str, '\n-'.join(required_objs))) + elif missing_objs: + raise ValueError( + '"{}" does not seem to be a complete backend. Valid backends ' + 'must implement the next objects:\n{}'.format( + backend_str, '\n-'.join(missing_objs))) + else: + backend_mod = importlib.import_module(backend_str) + return backend_mod def _plot_classes(): From fd36e1ae0c95409eaf52f1a3c6540dc8b4cf2a96 Mon Sep 17 00:00:00 2001 From: Marc Garcia Date: Mon, 10 Jun 2019 13:16:13 +0100 Subject: [PATCH 02/10] Moving the validation of the backend to when the backend is selected --- pandas/core/config_init.py | 47 +++++++++++++++++++++++++++++++++++++- pandas/plotting/_core.py | 37 ++---------------------------- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index 0bddcfbf164b8..e36a5d6fbfc8e 100644 --- a/pandas/core/config_init.py +++ b/pandas/core/config_init.py @@ -467,9 +467,54 @@ def use_inf_as_na_cb(key): prodiving the name of the module that implements the backend. """ + +def register_plotting_backend_cb(key): + import importlib + + backend_str = cf.get_option(key) + if backend_str == 'matplotlib': + try: + import pandas.plotting._matplotlib # noqa + except ImportError: + raise ImportError('matplotlib is required for plotting when the ' + 'default backend "matplotlib" is selected.') + else: + return + + try: + backend_mod = importlib.import_module(backend_str) + except ImportError: + raise ValueError('"{}" does not seem to be an installed module.' + 'A pandas plotting backend must be a module that ' + 'can be imported'.format(backend_str)) + + required_objs = ['LinePlot', 'BarPlot', 'BarhPlot', 'HistPlot', + 'BoxPlot', 'KdePlot', 'AreaPlot', 'PiePlot', + 'ScatterPlot', 'HexBinPlot', 'hist_series', + 'hist_frame', 'boxplot', 'boxplot_frame', + 'boxplot_frame_groupby', 'tsplot', 'table', + 'andrews_curves', 'autocorrelation_plot', + 'bootstrap_plot', 'lag_plot', 'parallel_coordinates', + 'radviz', 'scatter_matrix', 'register', 'deregister'] + missing_objs = set(required_objs) - set(dir(backend_mod)) + if len(missing_objs) == len(required_objs): + raise ValueError( + '"{}" does not seem to be a valid backend. Valid backends are ' + 'modules that implement the next objects:\n{}'.format( + backend_str, '\n-'.join(required_objs))) + elif missing_objs: + raise ValueError( + '"{}" does not seem to be a complete backend. Valid backends ' + 'must implement the next objects:\n{}'.format( + backend_str, '\n-'.join(missing_objs))) + + with cf.config_prefix('plotting'): cf.register_option('backend', defval='matplotlib', - doc=plotting_backend_doc) + doc=plotting_backend_doc, + validator=str, + cb=register_plotting_backend_cb) + register_converter_doc = """ : bool diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index ab3cdefa777d2..c354222d3a13c 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -628,41 +628,8 @@ def _get_plot_backend(): """ backend_str = pandas.get_option('plotting.backend') if backend_str == 'matplotlib': - try: - import pandas.plotting._matplotlib as backend_mod - except ImportError: - raise ImportError('matplotlib is required for plotting when the ' - 'default backend is selected.') - else: - try: - mod = importlib.import_module(backend_str) - except ImportError: - raise ValueError('"{}" does not seem to be an installed module.' - 'A pandas plotting backend must be a module that ' - 'can be imported'.format(backend_str)) - - required_objs = ['LinePlot', 'BarPlot', 'BarhPlot', 'HistPlot', - 'BoxPlot', 'KdePlot', 'AreaPlot', 'PiePlot', - 'ScatterPlot', 'HexBinPlot', 'hist_series', - 'hist_frame', 'boxplot', 'boxplot_frame', - 'boxplot_frame_groupby', 'tsplot', 'table', - 'andrews_curves', 'autocorrelation_plot', - 'bootstrap_plot', 'lag_plot', 'parallel_coordinates', - 'radviz', 'scatter_matrix', 'register', 'deregister'] - missing_objs = set(required_objs) - set(dir(mod)) - if len(missing_objs) == len(required_objs): - raise ValueError( - '"{}" does not seem to be a valid backend. Valid backends are ' - 'modules that implement the next objects:\n{}'.format( - backend_str, '\n-'.join(required_objs))) - elif missing_objs: - raise ValueError( - '"{}" does not seem to be a complete backend. Valid backends ' - 'must implement the next objects:\n{}'.format( - backend_str, '\n-'.join(missing_objs))) - else: - backend_mod = importlib.import_module(backend_str) - return backend_mod + backend_str = 'pandas.plotting._matplotlib' + return importlib.import_module(backend_str) def _plot_classes(): From 3ae0662081603be53f7d95cc64db9ddedda239bf Mon Sep 17 00:00:00 2001 From: Marc Garcia Date: Mon, 10 Jun 2019 15:44:42 +0100 Subject: [PATCH 03/10] Adding tests, doc and whatsnew --- doc/source/user_guide/options.rst | 6 ++ doc/source/whatsnew/v0.25.0.rst | 2 +- pandas/core/config_init.py | 15 ++--- pandas/tests/plotting/test_backend.py | 92 +++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 pandas/tests/plotting/test_backend.py diff --git a/doc/source/user_guide/options.rst b/doc/source/user_guide/options.rst index 4b466c2c44d49..4d0def435cb1e 100644 --- a/doc/source/user_guide/options.rst +++ b/doc/source/user_guide/options.rst @@ -431,6 +431,12 @@ compute.use_bottleneck True Use the bottleneck library computation if it is installed. compute.use_numexpr True Use the numexpr library to accelerate computation if it is installed. +plotting.backend matplotlib Change the plotting backend to a different + backend than the current matplotlib one. + Backends can be implemented as third-party + libraries implementing the pandas plotting + API. They can use other plotting libraries + like Bokeh, Altair, etc. plotting.matplotlib.register_converters True Register custom converters with matplotlib. Set to False to de-register. ======================================= ============ ================================== diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index df22a21196dab..842d84a2067c2 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -82,7 +82,7 @@ Other Enhancements - :meth:`DataFrame.query` and :meth:`DataFrame.eval` now supports quoting column names with backticks to refer to names with spaces (:issue:`6508`) - :func:`merge_asof` now gives a more clear error message when merge keys are categoricals that are not equal (:issue:`26136`) - :meth:`pandas.core.window.Rolling` supports exponential (or Poisson) window type (:issue:`21303`) -- +- Added new option ``plotting.backend`` to be able to select a plotting backend different that the existing ``matplotlib`` one. Use ``pandas.set_option('plotting.backend', '')`` where `` Date: Mon, 10 Jun 2019 17:04:32 +0100 Subject: [PATCH 04/10] Restoring plotting backend after tests, to not affect other tests --- pandas/tests/plotting/test_backend.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pandas/tests/plotting/test_backend.py b/pandas/tests/plotting/test_backend.py index fbe25fcfd82f0..a71b74ef87d23 100644 --- a/pandas/tests/plotting/test_backend.py +++ b/pandas/tests/plotting/test_backend.py @@ -58,6 +58,7 @@ def test_backend_is_not_module(): 'A pandas plotting backend must be a module that can be imported') with pytest.raises(ValueError, match=msg): pandas.set_option('plotting.backend', 'not_an_existing_module') + pandas.set_option('plotting.backend', 'matplotlib') def test_backend_not_a_backend_module(monkeypatch): @@ -73,6 +74,7 @@ def test_backend_not_a_backend_module(monkeypatch): _mocked_import_module) with pytest.raises(ValueError, match=msg): pandas.set_option('plotting.backend', 'module_not_a_backend') + pandas.set_option('plotting.backend', 'matplotlib') def test_backend_has_missing_objects(monkeypatch): @@ -83,6 +85,7 @@ def test_backend_has_missing_objects(monkeypatch): _mocked_import_module) with pytest.raises(ValueError, match=msg): pandas.set_option('plotting.backend', 'backend_missing_area_plot') + pandas.set_option('plotting.backend', 'matplotlib') def test_backend_is_correct(monkeypatch): @@ -90,3 +93,4 @@ def test_backend_is_correct(monkeypatch): _mocked_import_module) pandas.set_option('plotting.backend', 'correct_backend') assert pandas.get_option('plotting.backend') == 'correct_backend' + pandas.set_option('plotting.backend', 'matplotlib') From 06e829c473100c45cdc76df8d9e04bebfb1e057f Mon Sep 17 00:00:00 2001 From: Marc Garcia Date: Mon, 10 Jun 2019 17:43:43 +0100 Subject: [PATCH 05/10] avoid failing tests when matplotlib is not installed --- pandas/tests/plotting/test_backend.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pandas/tests/plotting/test_backend.py b/pandas/tests/plotting/test_backend.py index a71b74ef87d23..eeee7276f243e 100644 --- a/pandas/tests/plotting/test_backend.py +++ b/pandas/tests/plotting/test_backend.py @@ -58,7 +58,6 @@ def test_backend_is_not_module(): 'A pandas plotting backend must be a module that can be imported') with pytest.raises(ValueError, match=msg): pandas.set_option('plotting.backend', 'not_an_existing_module') - pandas.set_option('plotting.backend', 'matplotlib') def test_backend_not_a_backend_module(monkeypatch): @@ -74,7 +73,6 @@ def test_backend_not_a_backend_module(monkeypatch): _mocked_import_module) with pytest.raises(ValueError, match=msg): pandas.set_option('plotting.backend', 'module_not_a_backend') - pandas.set_option('plotting.backend', 'matplotlib') def test_backend_has_missing_objects(monkeypatch): @@ -85,7 +83,6 @@ def test_backend_has_missing_objects(monkeypatch): _mocked_import_module) with pytest.raises(ValueError, match=msg): pandas.set_option('plotting.backend', 'backend_missing_area_plot') - pandas.set_option('plotting.backend', 'matplotlib') def test_backend_is_correct(monkeypatch): @@ -93,4 +90,9 @@ def test_backend_is_correct(monkeypatch): _mocked_import_module) pandas.set_option('plotting.backend', 'correct_backend') assert pandas.get_option('plotting.backend') == 'correct_backend' - pandas.set_option('plotting.backend', 'matplotlib') + + # Restore backend for other tests (matplotlib can be not installed) + try: + pandas.set_option('plotting.backend', 'matplotlib') + except ImportError: + pass From f7c6e33c04b3f90a9bdb662063f5a8497cbef79b Mon Sep 17 00:00:00 2001 From: Marc Garcia Date: Tue, 11 Jun 2019 11:57:34 +0100 Subject: [PATCH 06/10] Removing checks to see if plotting backens implement the API --- pandas/core/config_init.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index d7262f29f5abc..12ed87a558443 100644 --- a/pandas/core/config_init.py +++ b/pandas/core/config_init.py @@ -488,23 +488,6 @@ def register_plotting_backend_cb(key): 'A pandas plotting backend must be a module that ' 'can be imported'.format(backend_str)) - required_objs = ['LinePlot', 'BarPlot', 'BarhPlot', 'HistPlot', - 'BoxPlot', 'KdePlot', 'AreaPlot', 'PiePlot', - 'ScatterPlot', 'HexBinPlot', 'hist_series', - 'hist_frame', 'boxplot', 'boxplot_frame', - 'boxplot_frame_groupby'] - missing_objs = set(required_objs) - set(dir(backend_mod)) - if len(missing_objs) == len(required_objs): - raise ValueError( - '"{}" does not seem to be a valid backend. Valid backends are ' - 'modules that implement the next objects:\n{}'.format( - backend_str, '\n'.join(required_objs))) - elif missing_objs: - raise ValueError( - '"{}" does not seem to be a complete backend. Valid backends ' - 'must implement the next objects:\n{}'.format( - backend_str, '\n'.join(missing_objs))) - with cf.config_prefix('plotting'): cf.register_option('backend', defval='matplotlib', From 10953449653dd06abf3639c63dbeda06d7ee255b Mon Sep 17 00:00:00 2001 From: Marc Garcia Date: Tue, 11 Jun 2019 13:31:52 +0100 Subject: [PATCH 07/10] Removing tests related to previous commit --- pandas/core/config_init.py | 2 +- pandas/tests/plotting/test_backend.py | 67 +-------------------------- 2 files changed, 2 insertions(+), 67 deletions(-) diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index 12ed87a558443..4409267147b65 100644 --- a/pandas/core/config_init.py +++ b/pandas/core/config_init.py @@ -482,7 +482,7 @@ def register_plotting_backend_cb(key): return try: - backend_mod = importlib.import_module(backend_str) + importlib.import_module(backend_str) except ImportError: raise ValueError('"{}" does not seem to be an installed module. ' 'A pandas plotting backend must be a module that ' diff --git a/pandas/tests/plotting/test_backend.py b/pandas/tests/plotting/test_backend.py index eeee7276f243e..65e1d690d5f8f 100644 --- a/pandas/tests/plotting/test_backend.py +++ b/pandas/tests/plotting/test_backend.py @@ -3,46 +3,6 @@ import pandas -def _mocked_import_module(name): - """ - Mock of ``importlib.import_module``. Depending of the name of the module - received will return: - - - 'sample_backend': A mock of a valid plotting backend - - 'module_not_a_backend': A backend (object) with none of the backend - methods - - 'backend_missing_area_plot': A backend (object) with all backend - attributes except ``AreaPlot`` - """ - class PlottingBackendModuleMock: - LinePlot = None - BarPlot = None - BarhPlot = None - HistPlot = None - BoxPlot = None - KdePlot = None - AreaPlot = None - PiePlot = None - ScatterPlot = None - HexBinPlot = None - hist_series = None - hist_frame = None - boxplot = None - boxplot_frame = None - boxplot_frame_groupby = None - - if name == 'correct_backend': - return PlottingBackendModuleMock - elif name == 'module_not_a_backend': - return object() - elif name == 'backend_missing_area_plot': - mod = PlottingBackendModuleMock - del mod.AreaPlot - return mod - - raise ValueError('Unknown mocked backend: {}'.format(name)) - - def test_matplotlib_backend_error(): msg = ('matplotlib is required for plotting when the default backend ' '"matplotlib" is selected.') @@ -60,34 +20,9 @@ def test_backend_is_not_module(): pandas.set_option('plotting.backend', 'not_an_existing_module') -def test_backend_not_a_backend_module(monkeypatch): - required_objs = ['LinePlot', 'BarPlot', 'BarhPlot', 'HistPlot', - 'BoxPlot', 'KdePlot', 'AreaPlot', 'PiePlot', - 'ScatterPlot', 'HexBinPlot', 'hist_series', - 'hist_frame', 'boxplot', 'boxplot_frame', - 'boxplot_frame_groupby'] - msg = ('"module_not_a_backend" does not seem to be a valid backend. ' - 'Valid backends are modules that implement the next ' - 'objects:\n{}'.format('\n'.join(required_objs))) - monkeypatch.setattr('pandas.core.config_init.importlib.import_module', - _mocked_import_module) - with pytest.raises(ValueError, match=msg): - pandas.set_option('plotting.backend', 'module_not_a_backend') - - -def test_backend_has_missing_objects(monkeypatch): - msg = ('"backend_missing_area_plot" does not seem to be a complete ' - 'backend. Valid backends must implement the next objects:\n' - 'AreaPlot') - monkeypatch.setattr('pandas.core.config_init.importlib.import_module', - _mocked_import_module) - with pytest.raises(ValueError, match=msg): - pandas.set_option('plotting.backend', 'backend_missing_area_plot') - - def test_backend_is_correct(monkeypatch): monkeypatch.setattr('pandas.core.config_init.importlib.import_module', - _mocked_import_module) + lambda name: None) pandas.set_option('plotting.backend', 'correct_backend') assert pandas.get_option('plotting.backend') == 'correct_backend' From b13a74b386ad004b23e2553febd615d4037ee680 Mon Sep 17 00:00:00 2001 From: Marc Garcia Date: Thu, 20 Jun 2019 11:31:23 +0100 Subject: [PATCH 08/10] Fixing failing test, and unifying get_plot_backend code --- pandas/plotting/_misc.py | 9 +-------- pandas/tests/plotting/test_misc.py | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/pandas/plotting/_misc.py b/pandas/plotting/_misc.py index a3f3f354690df..f240faf45dfce 100644 --- a/pandas/plotting/_misc.py +++ b/pandas/plotting/_misc.py @@ -3,14 +3,7 @@ from pandas.util._decorators import deprecate_kwarg - -def _get_plot_backend(): - # TODO unify with the same function in `_core.py` - try: - import pandas.plotting._matplotlib as plot_backend - except ImportError: - raise ImportError("matplotlib is required for plotting.") - return plot_backend +from pandas.plotting._core import _get_plot_backend def table(ax, data, rowLabels=None, colLabels=None, **kwargs): diff --git a/pandas/tests/plotting/test_misc.py b/pandas/tests/plotting/test_misc.py index a1c12fc73362a..b58854743a42d 100644 --- a/pandas/tests/plotting/test_misc.py +++ b/pandas/tests/plotting/test_misc.py @@ -21,7 +21,7 @@ def test_import_error_message(): # GH-19810 df = DataFrame({"A": [1, 2]}) - with pytest.raises(ImportError, match='matplotlib is required'): + with pytest.raises(ImportError, match="No module named 'matplotlib'"): df.plot() From 001c57b112a402d6b733a822fd45a2f16f3ff6a7 Mon Sep 17 00:00:00 2001 From: Marc Garcia Date: Thu, 20 Jun 2019 13:57:26 +0100 Subject: [PATCH 09/10] Fixing typo in whatsnew --- doc/source/whatsnew/v0.25.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index 40cd4cc4b8dbd..683b29f194bca 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -132,7 +132,7 @@ Other Enhancements - :class:`DatetimeIndex` and :class:`TimedeltaIndex` now have a ``mean`` method (:issue:`24757`) - :meth:`DataFrame.describe` now formats integer percentiles without decimal point (:issue:`26660`) - Added support for reading SPSS .sav files using :func:`read_spss` (:issue:`26537`) -- Added new option ``plotting.backend`` to be able to select a plotting backend different that the existing ``matplotlib`` one. Use ``pandas.set_option('plotting.backend', '')`` where ``')`` where `` Date: Fri, 21 Jun 2019 09:40:02 +0100 Subject: [PATCH 10/10] Adding import lost in the merge --- pandas/plotting/_core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index ceda6a9e764b2..b0e928fa8022b 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -6,6 +6,7 @@ from pandas.core.dtypes.common import is_integer, is_list_like from pandas.core.dtypes.generic import ABCDataFrame, ABCSeries +import pandas from pandas.core.base import PandasObject from pandas.core.generic import _shared_doc_kwargs, _shared_docs