diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index ccf5c43280765..be208a434f77b 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -23,7 +23,7 @@ including other versions of pandas. Other Enhancements ^^^^^^^^^^^^^^^^^^ - +- :func:`DataFrame.plot` keywords ``logy``, ``logx`` and ``loglog`` can now accept the value ``'sym'`` for symlog scaling. (:issue:`24867`) - Added support for ISO week year format ('%G-%V-%u') when parsing datetimes using :meth: `to_datetime` (:issue:`16607`) - Indexing of ``DataFrame`` and ``Series`` now accepts zerodim ``np.ndarray`` (:issue:`24919`) - :meth:`Timestamp.replace` now supports the ``fold`` argument to disambiguate DST transition times (:issue:`25017`) diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index 5ed6c2f4e14b6..e75e8bb4f8821 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -288,8 +288,10 @@ def _maybe_right_yaxis(self, ax, axes_num): if not self._has_plotted_object(orig_ax): # no data on left y orig_ax.get_yaxis().set_visible(False) - if self.logy or self.loglog: + if self.logy is True or self.loglog is True: new_ax.set_yscale('log') + elif self.logy == 'sym' or self.loglog == 'sym': + new_ax.set_yscale('symlog') return new_ax def _setup_subplots(self): @@ -311,10 +313,24 @@ def _setup_subplots(self): axes = _flatten(axes) - if self.logx or self.loglog: + valid_log = {False, True, 'sym', None} + input_log = {self.logx, self.logy, self.loglog} + if input_log - valid_log: + invalid_log = next(iter((input_log - valid_log))) + raise ValueError( + "Boolean, None and 'sym' are valid options," + " '{}' is given.".format(invalid_log) + ) + + if self.logx is True or self.loglog is True: [a.set_xscale('log') for a in axes] - if self.logy or self.loglog: + elif self.logx == 'sym' or self.loglog == 'sym': + [a.set_xscale('symlog') for a in axes] + + if self.logy is True or self.loglog is True: [a.set_yscale('log') for a in axes] + elif self.logy == 'sym' or self.loglog == 'sym': + [a.set_yscale('symlog') for a in axes] self.fig = fig self.axes = axes @@ -1909,12 +1925,18 @@ def _plot(data, x=None, y=None, subplots=False, Place legend on axis subplots style : list or dict matplotlib line style per column - logx : bool, default False - Use log scaling on x axis - logy : bool, default False - Use log scaling on y axis - loglog : bool, default False - Use log scaling on both x and y axes + logx : bool or 'sym', default False + Use log scaling or symlog scaling on x axis + .. versionchanged:: 0.25.0 + + logy : bool or 'sym' default False + Use log scaling or symlog scaling on y axis + .. versionchanged:: 0.25.0 + + loglog : bool or 'sym', default False + Use log scaling or symlog scaling on both x and y axes + .. versionchanged:: 0.25.0 + xticks : sequence Values to use for the xticks yticks : sequence diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index 2b17377c7b9bc..768fb7519c1ce 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -248,16 +248,34 @@ def test_plot_xy(self): # TODO add MultiIndex test @pytest.mark.slow - def test_logscales(self): + @pytest.mark.parametrize("input_log, expected_log", [ + (True, 'log'), + ('sym', 'symlog') + ]) + def test_logscales(self, input_log, expected_log): df = DataFrame({'a': np.arange(100)}, index=np.arange(100)) - ax = df.plot(logy=True) - self._check_ax_scales(ax, yaxis='log') - ax = df.plot(logx=True) - self._check_ax_scales(ax, xaxis='log') + ax = df.plot(logy=input_log) + self._check_ax_scales(ax, yaxis=expected_log) + assert ax.get_yscale() == expected_log + + ax = df.plot(logx=input_log) + self._check_ax_scales(ax, xaxis=expected_log) + assert ax.get_xscale() == expected_log + + ax = df.plot(loglog=input_log) + self._check_ax_scales(ax, xaxis=expected_log, yaxis=expected_log) + assert ax.get_xscale() == expected_log + assert ax.get_yscale() == expected_log + + @pytest.mark.parametrize("input_param", ["logx", "logy", "loglog"]) + def test_invalid_logscale(self, input_param): + # GH: 24867 + df = DataFrame({'a': np.arange(100)}, index=np.arange(100)) - ax = df.plot(loglog=True) - self._check_ax_scales(ax, xaxis='log', yaxis='log') + msg = "Boolean, None and 'sym' are valid options, 'sm' is given." + with pytest.raises(ValueError, match=msg): + df.plot(**{input_param: "sm"}) @pytest.mark.slow def test_xcompat(self): diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index e384c578aa446..daa17c173ca08 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -571,16 +571,21 @@ def test_df_series_secondary_legend(self): tm.close() @pytest.mark.slow - def test_secondary_logy(self): + @pytest.mark.parametrize("input_logy, expected_scale", [ + (True, 'log'), + ('sym', 'symlog') + ]) + def test_secondary_logy(self, input_logy, expected_scale): # GH 25545 s1 = Series(np.random.randn(30)) s2 = Series(np.random.randn(30)) - ax1 = s1.plot(logy=True) - ax2 = s2.plot(secondary_y=True, logy=True) + # GH 24980 + ax1 = s1.plot(logy=input_logy) + ax2 = s2.plot(secondary_y=True, logy=input_logy) - assert ax1.get_yscale() == 'log' - assert ax2.get_yscale() == 'log' + assert ax1.get_yscale() == expected_scale + assert ax2.get_yscale() == expected_scale @pytest.mark.slow def test_plot_fails_with_dupe_color_and_style(self):