From 61beb343d98369b2fb85eb5ca5e02e2b93119abc Mon Sep 17 00:00:00 2001 From: Ninja-lgtm Date: Tue, 24 Feb 2026 16:02:50 +0530 Subject: [PATCH 1/4] Improve default time axis formatting using ConciseDateFormatter --- radiospectra/mixins.py | 43 +++++++++++------ .../spectrogram/tests/test_spectrogrambase.py | 46 +++++++++++++++++-- 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/radiospectra/mixins.py b/radiospectra/mixins.py index 488c7cf2..1351a290 100644 --- a/radiospectra/mixins.py +++ b/radiospectra/mixins.py @@ -1,4 +1,5 @@ from matplotlib import pyplot as plt +from matplotlib.dates import AutoDateLocator, ConciseDateFormatter, date2num from matplotlib.image import NonUniformImage from astropy.visualization import quantity_support, time_support @@ -37,7 +38,19 @@ def _set_axis_converter(axis, converter): axis.converter = converter -class PcolormeshPlotMixin: +class _TimeAxisMixin: + def _setup_time_axis(self, axes): + """ + Apply `~matplotlib.dates.ConciseDateFormatter` to the x-axis. + """ + locator = AutoDateLocator() + formatter = ConciseDateFormatter(locator) + axes.xaxis.set_major_locator(locator) + axes.xaxis.set_major_formatter(formatter) + axes.set_xlabel(f"Time ({self.times.scale})") + + +class PcolormeshPlotMixin(_TimeAxisMixin): """ Class provides plotting functions using `~pcolormesh`. """ @@ -59,9 +72,7 @@ def plot(self, axes=None, **kwargs): """ if axes is None: - fig, axes = plt.subplots() - else: - fig = axes.get_figure() + _, axes = plt.subplots() if hasattr(self.data, "value"): data = self.data.value @@ -84,13 +95,15 @@ def plot(self, axes=None, **kwargs): if converter_x is not None and not getattr(axes.xaxis, "_converter_is_explicit", False): _set_axis_converter(axes.xaxis, converter_x) - axes.plot(self.times[[0, -1]], self.frequencies[[0, -1]], linestyle="None", marker="None") + times_datetime = self.times.datetime + axes.plot(times_datetime[[0, -1]], self.frequencies[[0, -1]], linestyle="None", marker="None") if self.times.shape[0] == self.data.shape[0] and self.frequencies.shape[0] == self.data.shape[1]: - ret = axes.pcolormesh(self.times, self.frequencies, data, shading="auto", **kwargs) + ret = axes.pcolormesh(times_datetime, self.frequencies, data, shading="auto", **kwargs) else: - ret = axes.pcolormesh(self.times, self.frequencies, data[:-1, :-1], shading="auto", **kwargs) - axes.set_xlim(self.times[0], self.times[-1]) - fig.autofmt_xdate() + ret = axes.pcolormesh(times_datetime, self.frequencies, data[:-1, :-1], shading="auto", **kwargs) + axes.set_xlim(times_datetime[0], times_datetime[-1]) + + self._setup_time_axis(axes) # Set current axes/image if pyplot is being used (makes colorbar work) for i in plt.get_fignums(): @@ -100,7 +113,7 @@ def plot(self, axes=None, **kwargs): return ret -class NonUniformImagePlotMixin: +class NonUniformImagePlotMixin(_TimeAxisMixin): """ Class provides plotting functions using `NonUniformImage`. """ @@ -123,9 +136,13 @@ def plotim(self, fig=None, axes=None, **kwargs): axes.yaxis.update_units(self.frequencies) frequencies = axes.yaxis.convert_units(self.frequencies) - axes.plot(self.times[[0, -1]], self.frequencies[[0, -1]], linestyle="None", marker="None") + times_datetime = self.times.datetime + times_num = date2num(times_datetime) + axes.plot(times_datetime[[0, -1]], self.frequencies[[0, -1]], linestyle="None", marker="None") im = NonUniformImage(axes, interpolation="none", **kwargs) - im.set_data(axes.convert_xunits(self.times), frequencies, self.data) + im.set_data(times_num, frequencies, self.data) axes.add_image(im) - axes.set_xlim(self.times[0], self.times[-1]) + axes.set_xlim(times_datetime[0], times_datetime[-1]) axes.set_ylim(frequencies[0], frequencies[-1]) + + self._setup_time_axis(axes) diff --git a/radiospectra/spectrogram/tests/test_spectrogrambase.py b/radiospectra/spectrogram/tests/test_spectrogrambase.py index ddd3a36b..8aa19c31 100644 --- a/radiospectra/spectrogram/tests/test_spectrogrambase.py +++ b/radiospectra/spectrogram/tests/test_spectrogrambase.py @@ -2,6 +2,7 @@ import matplotlib.pyplot as plt import numpy as np +from matplotlib.dates import ConciseDateFormatter, date2num import astropy.units as u @@ -53,7 +54,9 @@ def test_plotim(make_spectrogram): plt.close("all") _, x_values, y_values, image = set_data.call_args.args + expected_times = date2num(rad_im.times.datetime) assert len(x_values) == len(rad_im.times) + np.testing.assert_allclose(x_values, expected_times) np.testing.assert_allclose(y_values, rad_im.frequencies.value) np.testing.assert_allclose(image, rad_im.data) @@ -104,12 +107,12 @@ def test_plot_instrument_detector_differ(make_spectrogram): def test_plot_uses_time_support_for_datetime_conversion(make_spectrogram): - """Plotting with non-UTC time scale should use time_support.""" + """Plotting with non-UTC time scale should use datetime conversion.""" spec = make_spectrogram(np.linspace(10, 40, 4) * u.MHz, scale="tt") mesh = spec.plot() x_limits = np.array(mesh.axes.get_xlim()) - expected_tt_limits = mesh.axes.convert_xunits(spec.times[[0, -1]]) + expected_tt_limits = date2num(spec.times.datetime[[0, -1]]) plt.close(mesh.axes.figure) @@ -117,7 +120,7 @@ def test_plot_uses_time_support_for_datetime_conversion(make_spectrogram): def test_plotim_uses_time_support_for_datetime_conversion(make_spectrogram): - """plotim with non-UTC time scale should use time_support.""" + """plotim with non-UTC time scale should use datetime conversion.""" spec = make_spectrogram(np.linspace(10, 40, 4) * u.MHz, scale="tt") fig, axes = plt.subplots() @@ -130,8 +133,43 @@ def test_plotim_uses_time_support_for_datetime_conversion(make_spectrogram): plt.close(fig) _, x_values, y_values, image = set_data.call_args.args - expected_tt = axes.convert_xunits(spec.times) + expected_tt = date2num(spec.times.datetime) np.testing.assert_allclose(x_values, expected_tt) np.testing.assert_allclose(y_values, spec.frequencies.value) np.testing.assert_allclose(image, spec.data) + + +def test_plot_uses_concise_date_formatter(make_spectrogram): + """plot() should apply ConciseDateFormatter to the x-axis.""" + rad = make_spectrogram(np.array([10, 20, 30, 40]) * u.kHz) + mesh = rad.plot() + formatter = mesh.axes.xaxis.get_major_formatter() + plt.close("all") + + assert isinstance(formatter, ConciseDateFormatter) + + +def test_plotim_uses_concise_date_formatter(make_spectrogram): + """plotim() should apply ConciseDateFormatter to the x-axis.""" + rad = make_spectrogram(np.array([10, 20, 30, 40]) * u.kHz) + fig, axes = plt.subplots() + with ( + mock.patch("matplotlib.image.NonUniformImage.set_interpolation", autospec=True), + mock.patch("matplotlib.image.NonUniformImage.set_data", autospec=True), + ): + rad.plotim(axes=axes) + formatter = axes.xaxis.get_major_formatter() + plt.close("all") + + assert isinstance(formatter, ConciseDateFormatter) + + +def test_plot_xlabel_includes_time_scale(make_spectrogram): + """plot() should set the x-label to include the time scale.""" + rad = make_spectrogram(np.array([10, 20, 30, 40]) * u.kHz) + mesh = rad.plot() + xlabel = mesh.axes.get_xlabel() + plt.close("all") + + assert "utc" in xlabel.lower() From 2daac091bf0b4ac22f61e70e5bd8d470694d02e7 Mon Sep 17 00:00:00 2001 From: Ninja-lgtm Date: Tue, 24 Feb 2026 16:37:02 +0530 Subject: [PATCH 2/4] Add changelog entry for PR #163 --- changelog/163.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/163.feature.rst diff --git a/changelog/163.feature.rst b/changelog/163.feature.rst new file mode 100644 index 00000000..6f6c54c1 --- /dev/null +++ b/changelog/163.feature.rst @@ -0,0 +1 @@ +Improved default time axis formatting on spectrogram plots by using `matplotlib.dates.ConciseDateFormatter`. This also changes the internal time axis representation to native Python `datetime` objects; users should now use `datetime` objects when manually setting axis limits (e.g., via `~matplotlib.axes.Axes.set_xlim`). From 895d327cea956224640107b8aac506bde9433f73 Mon Sep 17 00:00:00 2001 From: Ninja-lgtm Date: Thu, 26 Feb 2026 18:53:39 +0530 Subject: [PATCH 3/4] Implement matplotlib.units adapter for astropy.time.Time to support ConciseDateFormatter --- changelog/163.feature.rst | 2 +- radiospectra/mixins.py | 80 +++++++++++++----- .../spectrogram/tests/test_spectrogrambase.py | 82 +++++++++++++++++-- 3 files changed, 135 insertions(+), 29 deletions(-) diff --git a/changelog/163.feature.rst b/changelog/163.feature.rst index 6f6c54c1..778321f1 100644 --- a/changelog/163.feature.rst +++ b/changelog/163.feature.rst @@ -1 +1 @@ -Improved default time axis formatting on spectrogram plots by using `matplotlib.dates.ConciseDateFormatter`. This also changes the internal time axis representation to native Python `datetime` objects; users should now use `datetime` objects when manually setting axis limits (e.g., via `~matplotlib.axes.Axes.set_xlim`). +Improved default time axis formatting on spectrogram plots by using `matplotlib.dates.ConciseDateFormatter` with a custom matplotlib units converter for `astropy.time.Time`, preserving support for both `Time` and `datetime` values when setting axis limits (e.g., via `~matplotlib.axes.Axes.set_xlim`). diff --git a/radiospectra/mixins.py b/radiospectra/mixins.py index 1351a290..7dddcfea 100644 --- a/radiospectra/mixins.py +++ b/radiospectra/mixins.py @@ -1,8 +1,10 @@ +import numpy as np from matplotlib import pyplot as plt -from matplotlib.dates import AutoDateLocator, ConciseDateFormatter, date2num +from matplotlib.dates import AutoDateLocator, ConciseDateFormatter, DateConverter from matplotlib.image import NonUniformImage -from astropy.visualization import quantity_support, time_support +from astropy.time import Time +from astropy.visualization import quantity_support def _get_axis_converter(axis): @@ -38,7 +40,50 @@ def _set_axis_converter(axis, converter): axis.converter = converter +class _TimeDateConverter(DateConverter): + """ + Converter that supports both matplotlib date values and astropy Time. + """ + + @staticmethod + def _is_time_sequence(value): + return ( + isinstance(value, (list, tuple, np.ndarray)) + and np.size(value) + and all(isinstance(v, Time) for v in np.asarray(value, dtype=object).flat) + ) + + @staticmethod + def default_units(x, axis): + if isinstance(x, Time) or _TimeDateConverter._is_time_sequence(x): + return None + return DateConverter.default_units(x, axis) + + @staticmethod + def convert(value, unit, axis): + if isinstance(value, Time): + return value.plot_date + + if _TimeDateConverter._is_time_sequence(value): + converted = [v.plot_date for v in np.asarray(value, dtype=object).flat] + if isinstance(value, np.ndarray): + return np.asarray(converted, dtype=float).reshape(value.shape) + return converted + + return DateConverter.convert(value, unit, axis) + + +_TIME_DATE_CONVERTER = _TimeDateConverter() + + class _TimeAxisMixin: + def _set_time_converter(self, axes): + """ + Ensure the x-axis supports both `~astropy.time.Time` and datetime inputs. + """ + if not getattr(axes.xaxis, "_converter_is_explicit", False): + _set_axis_converter(axes.xaxis, _TIME_DATE_CONVERTER) + def _setup_time_axis(self, axes): """ Apply `~matplotlib.dates.ConciseDateFormatter` to the x-axis. @@ -85,23 +130,21 @@ def plot(self, axes=None, **kwargs): axes.set_title(title) - with time_support(), quantity_support(): + with quantity_support(): # Pin existing converters to avoid warnings when re-plotting on shared axes. converter_y = _get_axis_converter(axes.yaxis) if converter_y is not None and not getattr(axes.yaxis, "_converter_is_explicit", False): _set_axis_converter(axes.yaxis, converter_y) - converter_x = _get_axis_converter(axes.xaxis) - if converter_x is not None and not getattr(axes.xaxis, "_converter_is_explicit", False): - _set_axis_converter(axes.xaxis, converter_x) + self._set_time_converter(axes) - times_datetime = self.times.datetime - axes.plot(times_datetime[[0, -1]], self.frequencies[[0, -1]], linestyle="None", marker="None") + times_plot_date = self.times.plot_date + axes.plot(times_plot_date[[0, -1]], self.frequencies[[0, -1]], linestyle="None", marker="None") if self.times.shape[0] == self.data.shape[0] and self.frequencies.shape[0] == self.data.shape[1]: - ret = axes.pcolormesh(times_datetime, self.frequencies, data, shading="auto", **kwargs) + ret = axes.pcolormesh(times_plot_date, self.frequencies, data, shading="auto", **kwargs) else: - ret = axes.pcolormesh(times_datetime, self.frequencies, data[:-1, :-1], shading="auto", **kwargs) - axes.set_xlim(times_datetime[0], times_datetime[-1]) + ret = axes.pcolormesh(times_plot_date, self.frequencies, data[:-1, :-1], shading="auto", **kwargs) + axes.set_xlim(times_plot_date[0], times_plot_date[-1]) self._setup_time_axis(axes) @@ -123,26 +166,23 @@ def plotim(self, fig=None, axes=None, **kwargs): if axes is None: fig, axes = plt.subplots() - with time_support(), quantity_support(): + with quantity_support(): # Pin existing converters to avoid warnings when re-plotting on shared axes. converter_y = _get_axis_converter(axes.yaxis) if converter_y is not None and not getattr(axes.yaxis, "_converter_is_explicit", False): _set_axis_converter(axes.yaxis, converter_y) - converter_x = _get_axis_converter(axes.xaxis) - if converter_x is not None and not getattr(axes.xaxis, "_converter_is_explicit", False): - _set_axis_converter(axes.xaxis, converter_x) + self._set_time_converter(axes) axes.yaxis.update_units(self.frequencies) frequencies = axes.yaxis.convert_units(self.frequencies) - times_datetime = self.times.datetime - times_num = date2num(times_datetime) - axes.plot(times_datetime[[0, -1]], self.frequencies[[0, -1]], linestyle="None", marker="None") + times_plot_date = self.times.plot_date + axes.plot(times_plot_date[[0, -1]], self.frequencies[[0, -1]], linestyle="None", marker="None") im = NonUniformImage(axes, interpolation="none", **kwargs) - im.set_data(times_num, frequencies, self.data) + im.set_data(times_plot_date, frequencies, self.data) axes.add_image(im) - axes.set_xlim(times_datetime[0], times_datetime[-1]) + axes.set_xlim(times_plot_date[0], times_plot_date[-1]) axes.set_ylim(frequencies[0], frequencies[-1]) self._setup_time_axis(axes) diff --git a/radiospectra/spectrogram/tests/test_spectrogrambase.py b/radiospectra/spectrogram/tests/test_spectrogrambase.py index 8aa19c31..de692891 100644 --- a/radiospectra/spectrogram/tests/test_spectrogrambase.py +++ b/radiospectra/spectrogram/tests/test_spectrogrambase.py @@ -2,9 +2,10 @@ import matplotlib.pyplot as plt import numpy as np -from matplotlib.dates import ConciseDateFormatter, date2num +from matplotlib.dates import ConciseDateFormatter import astropy.units as u +from astropy.time import Time def test_plot_mixed_frequency_units_on_same_axes(make_spectrogram): @@ -54,7 +55,7 @@ def test_plotim(make_spectrogram): plt.close("all") _, x_values, y_values, image = set_data.call_args.args - expected_times = date2num(rad_im.times.datetime) + expected_times = rad_im.times.plot_date assert len(x_values) == len(rad_im.times) np.testing.assert_allclose(x_values, expected_times) np.testing.assert_allclose(y_values, rad_im.frequencies.value) @@ -106,21 +107,21 @@ def test_plot_instrument_detector_differ(make_spectrogram): plt.close("all") -def test_plot_uses_time_support_for_datetime_conversion(make_spectrogram): - """Plotting with non-UTC time scale should use datetime conversion.""" +def test_plot_uses_plot_date_conversion(make_spectrogram): + """Plotting with non-UTC time scale should use matplotlib plot-date conversion.""" spec = make_spectrogram(np.linspace(10, 40, 4) * u.MHz, scale="tt") mesh = spec.plot() x_limits = np.array(mesh.axes.get_xlim()) - expected_tt_limits = date2num(spec.times.datetime[[0, -1]]) + expected_tt_limits = spec.times.plot_date[[0, -1]] plt.close(mesh.axes.figure) np.testing.assert_allclose(x_limits, expected_tt_limits) -def test_plotim_uses_time_support_for_datetime_conversion(make_spectrogram): - """plotim with non-UTC time scale should use datetime conversion.""" +def test_plotim_uses_plot_date_conversion(make_spectrogram): + """plotim with non-UTC time scale should use matplotlib plot-date conversion.""" spec = make_spectrogram(np.linspace(10, 40, 4) * u.MHz, scale="tt") fig, axes = plt.subplots() @@ -133,13 +134,78 @@ def test_plotim_uses_time_support_for_datetime_conversion(make_spectrogram): plt.close(fig) _, x_values, y_values, image = set_data.call_args.args - expected_tt = date2num(spec.times.datetime) + expected_tt = spec.times.plot_date np.testing.assert_allclose(x_values, expected_tt) np.testing.assert_allclose(y_values, spec.frequencies.value) np.testing.assert_allclose(image, spec.data) +def test_plot_accepts_astropy_time_xlim(make_spectrogram): + """plot() should still accept astropy Time values in set_xlim().""" + rad = make_spectrogram(np.array([10, 20, 30, 40]) * u.kHz) + mesh = rad.plot() + mesh.axes.set_xlim(rad.times[0], rad.times[-1]) + x_limits = np.array(mesh.axes.get_xlim()) + plt.close(mesh.axes.figure) + + np.testing.assert_allclose(x_limits, rad.times.plot_date[[0, -1]]) + + +def test_plot_accepts_datetime_xlim(make_spectrogram): + """plot() should accept datetime values in set_xlim().""" + rad = make_spectrogram(np.array([10, 20, 30, 40]) * u.kHz) + mesh = rad.plot() + mesh.axes.set_xlim(rad.times.datetime[0], rad.times.datetime[-1]) + x_limits = np.array(mesh.axes.get_xlim()) + plt.close(mesh.axes.figure) + + np.testing.assert_allclose(x_limits, rad.times.plot_date[[0, -1]]) + + +def test_plotim_accepts_astropy_time_xlim(make_spectrogram): + """plotim() should still accept astropy Time values in set_xlim().""" + rad = make_spectrogram(np.array([10, 20, 30, 40]) * u.kHz) + fig, axes = plt.subplots() + with ( + mock.patch("matplotlib.image.NonUniformImage.set_interpolation", autospec=True), + mock.patch("matplotlib.image.NonUniformImage.set_data", autospec=True), + ): + rad.plotim(axes=axes) + axes.set_xlim(rad.times[0], rad.times[-1]) + x_limits = np.array(axes.get_xlim()) + plt.close(fig) + + np.testing.assert_allclose(x_limits, rad.times.plot_date[[0, -1]]) + + +def test_plotim_accepts_datetime_xlim(make_spectrogram): + """plotim() should accept datetime values in set_xlim().""" + rad = make_spectrogram(np.array([10, 20, 30, 40]) * u.kHz) + fig, axes = plt.subplots() + with ( + mock.patch("matplotlib.image.NonUniformImage.set_interpolation", autospec=True), + mock.patch("matplotlib.image.NonUniformImage.set_data", autospec=True), + ): + rad.plotim(axes=axes) + axes.set_xlim(rad.times.datetime[0], rad.times.datetime[-1]) + x_limits = np.array(axes.get_xlim()) + plt.close(fig) + + np.testing.assert_allclose(x_limits, rad.times.plot_date[[0, -1]]) + + +def test_plot_handles_leap_seconds(make_spectrogram): + """plot() should not fail for times that include a leap second.""" + leap_times = Time("2016-12-31T23:59:58", scale="utc") + np.arange(4) * u.s + rad = make_spectrogram(np.array([10, 20, 30, 40]) * u.kHz, times=leap_times) + mesh = rad.plot() + x_limits = np.array(mesh.axes.get_xlim()) + plt.close(mesh.axes.figure) + + np.testing.assert_allclose(x_limits, leap_times.plot_date[[0, -1]]) + + def test_plot_uses_concise_date_formatter(make_spectrogram): """plot() should apply ConciseDateFormatter to the x-axis.""" rad = make_spectrogram(np.array([10, 20, 30, 40]) * u.kHz) From 31aa96a9d497dd2a243de2a38c97ce331b0578f4 Mon Sep 17 00:00:00 2001 From: Amityush-lgtm Date: Fri, 27 Mar 2026 18:07:40 +0530 Subject: [PATCH 4/4] Implemented ConciseAstropyConverter and context manager for native time formatting --- changelog/163.feature.rst | 2 +- radiospectra/mixins.py | 117 +++++++++++++++++++------------------- 2 files changed, 59 insertions(+), 60 deletions(-) diff --git a/changelog/163.feature.rst b/changelog/163.feature.rst index 778321f1..8ff3d286 100644 --- a/changelog/163.feature.rst +++ b/changelog/163.feature.rst @@ -1 +1 @@ -Improved default time axis formatting on spectrogram plots by using `matplotlib.dates.ConciseDateFormatter` with a custom matplotlib units converter for `astropy.time.Time`, preserving support for both `Time` and `datetime` values when setting axis limits (e.g., via `~matplotlib.axes.Axes.set_xlim`). +Improved default time axis formatting on spectrogram plots by using `matplotlib.dates.ConciseDateFormatter`. The time axis natively supports `astropy.time.Time` objects through a custom Matplotlib units converter (`ConciseAstropyConverter`), allowing use of `~matplotlib.axes.Axes.set_xlim` with `Time` objects. diff --git a/radiospectra/mixins.py b/radiospectra/mixins.py index 7dddcfea..337af2c4 100644 --- a/radiospectra/mixins.py +++ b/radiospectra/mixins.py @@ -1,3 +1,6 @@ +from contextlib import contextmanager + +import matplotlib.units as munits import numpy as np from matplotlib import pyplot as plt from matplotlib.dates import AutoDateLocator, ConciseDateFormatter, DateConverter @@ -40,62 +43,65 @@ def _set_axis_converter(axis, converter): axis.converter = converter -class _TimeDateConverter(DateConverter): - """ - Converter that supports both matplotlib date values and astropy Time. +class ConciseAstropyConverter(munits.ConversionInterface): """ + Matplotlib unit converter for `astropy.time.Time` that uses concise date formatting. - @staticmethod - def _is_time_sequence(value): - return ( - isinstance(value, (list, tuple, np.ndarray)) - and np.size(value) - and all(isinstance(v, Time) for v in np.asarray(value, dtype=object).flat) - ) + Notes + ----- + This converter turns times into Matplotlib internal date floats. It does + not handle leap seconds or nanosecond precision for the entire age of + the universe. + """ @staticmethod - def default_units(x, axis): - if isinstance(x, Time) or _TimeDateConverter._is_time_sequence(x): - return None - return DateConverter.default_units(x, axis) + def axisinfo(unit, axis): + locator = AutoDateLocator() + formatter = ConciseDateFormatter(locator) + return munits.AxisInfo(majloc=locator, majfmt=formatter, label=f"Time ({unit})") @staticmethod def convert(value, unit, axis): if isinstance(value, Time): return value.plot_date - if _TimeDateConverter._is_time_sequence(value): - converted = [v.plot_date for v in np.asarray(value, dtype=object).flat] - if isinstance(value, np.ndarray): - return np.asarray(converted, dtype=float).reshape(value.shape) - return converted + if isinstance(value, (list, tuple, np.ndarray)): + # If it's already a Time object (could be array-valued), use its plot_date + if isinstance(value, Time): + return value.plot_date + # Otherwise iterate over the sequence + converted = [v.plot_date if isinstance(v, Time) else v for v in np.asarray(value, dtype=object).flat] - return DateConverter.convert(value, unit, axis) + # Matplotlib dates are floats; let DateConverter handle any remaining datetimes + converted_val = np.asarray(converted).reshape(np.shape(value)) + return DateConverter.convert(converted_val, unit, axis) + return DateConverter.convert(value, unit, axis) -_TIME_DATE_CONVERTER = _TimeDateConverter() + @staticmethod + def default_units(x, axis): + if isinstance(x, Time): + return x.scale.upper() + return None -class _TimeAxisMixin: - def _set_time_converter(self, axes): - """ - Ensure the x-axis supports both `~astropy.time.Time` and datetime inputs. - """ - if not getattr(axes.xaxis, "_converter_is_explicit", False): - _set_axis_converter(axes.xaxis, _TIME_DATE_CONVERTER) - - def _setup_time_axis(self, axes): - """ - Apply `~matplotlib.dates.ConciseDateFormatter` to the x-axis. - """ - locator = AutoDateLocator() - formatter = ConciseDateFormatter(locator) - axes.xaxis.set_major_locator(locator) - axes.xaxis.set_major_formatter(formatter) - axes.set_xlabel(f"Time ({self.times.scale})") +@contextmanager +def concise_time_support(): + """ + Context manager to enable concise time formatting for `astropy.time.Time` objects. + """ + original_converter = munits.registry.get(Time) + munits.registry[Time] = ConciseAstropyConverter() + try: + yield + finally: + if original_converter is None: + del munits.registry[Time] + else: + munits.registry[Time] = original_converter -class PcolormeshPlotMixin(_TimeAxisMixin): +class PcolormeshPlotMixin: """ Class provides plotting functions using `~pcolormesh`. """ @@ -130,23 +136,18 @@ def plot(self, axes=None, **kwargs): axes.set_title(title) - with quantity_support(): + with concise_time_support(), quantity_support(): # Pin existing converters to avoid warnings when re-plotting on shared axes. converter_y = _get_axis_converter(axes.yaxis) if converter_y is not None and not getattr(axes.yaxis, "_converter_is_explicit", False): _set_axis_converter(axes.yaxis, converter_y) - self._set_time_converter(axes) - - times_plot_date = self.times.plot_date - axes.plot(times_plot_date[[0, -1]], self.frequencies[[0, -1]], linestyle="None", marker="None") + axes.plot(self.times[[0, -1]], self.frequencies[[0, -1]], linestyle="None", marker="None") if self.times.shape[0] == self.data.shape[0] and self.frequencies.shape[0] == self.data.shape[1]: - ret = axes.pcolormesh(times_plot_date, self.frequencies, data, shading="auto", **kwargs) + ret = axes.pcolormesh(self.times, self.frequencies, data, shading="auto", **kwargs) else: - ret = axes.pcolormesh(times_plot_date, self.frequencies, data[:-1, :-1], shading="auto", **kwargs) - axes.set_xlim(times_plot_date[0], times_plot_date[-1]) - - self._setup_time_axis(axes) + ret = axes.pcolormesh(self.times, self.frequencies, data[:-1, :-1], shading="auto", **kwargs) + axes.set_xlim(self.times[0], self.times[-1]) # Set current axes/image if pyplot is being used (makes colorbar work) for i in plt.get_fignums(): @@ -156,7 +157,7 @@ def plot(self, axes=None, **kwargs): return ret -class NonUniformImagePlotMixin(_TimeAxisMixin): +class NonUniformImagePlotMixin: """ Class provides plotting functions using `NonUniformImage`. """ @@ -166,23 +167,21 @@ def plotim(self, fig=None, axes=None, **kwargs): if axes is None: fig, axes = plt.subplots() - with quantity_support(): + with concise_time_support(), quantity_support(): # Pin existing converters to avoid warnings when re-plotting on shared axes. converter_y = _get_axis_converter(axes.yaxis) if converter_y is not None and not getattr(axes.yaxis, "_converter_is_explicit", False): _set_axis_converter(axes.yaxis, converter_y) - self._set_time_converter(axes) - axes.yaxis.update_units(self.frequencies) frequencies = axes.yaxis.convert_units(self.frequencies) - times_plot_date = self.times.plot_date - axes.plot(times_plot_date[[0, -1]], self.frequencies[[0, -1]], linestyle="None", marker="None") + axes.plot(self.times[[0, -1]], self.frequencies[[0, -1]], linestyle="None", marker="None") im = NonUniformImage(axes, interpolation="none", **kwargs) - im.set_data(times_plot_date, frequencies, self.data) + # NonUniformImage does not use the axis converter itself, + # so we manually convert the explicit input times down to floats. + times_numeric = axes.xaxis.convert_units(self.times) + im.set_data(times_numeric, frequencies, self.data) axes.add_image(im) - axes.set_xlim(times_plot_date[0], times_plot_date[-1]) + axes.set_xlim(self.times[0], self.times[-1]) axes.set_ylim(frequencies[0], frequencies[-1]) - - self._setup_time_axis(axes)