diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 259f742ac072..f7a20b59fde7 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -1,3 +1,4 @@ +import datetime import functools import itertools import logging @@ -9049,6 +9050,20 @@ def violin(self, vpstats, positions=None, vert=None, elif len(widths) != N: raise ValueError(datashape_message.format("widths")) + # For usability / better error message: + # Validate that datetime-like positions have timedelta-like widths. + # Checking only the first element is good enough for standard misuse cases + pos0 = positions[0] + width0 = widths if np.isscalar(widths) else widths[0] + if (isinstance(pos0, (datetime.datetime, datetime.date)) + and not isinstance(width0, datetime.timedelta)): + raise TypeError( + "datetime/date 'position' values, require timedelta 'widths'") + elif (isinstance(pos0, np.datetime64) + and not isinstance(width0, np.timedelta64)): + raise TypeError( + "np.datetime64 'position' values, require np.timedelta64 'widths'") + # Validate side _api.check_in_list(["both", "low", "high"], side=side) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 304ba0413686..e0c869af52ce 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4121,6 +4121,81 @@ def test_violinplot_sides(): showextrema=True, showmedians=True, side=side) +def violin_plot_stats(): + datetimes = [ + datetime.datetime(2023, 2, 10), + datetime.datetime(2023, 5, 18), + datetime.datetime(2023, 6, 6) + ] + return [{ + 'coords': datetimes, + 'vals': [0.1, 0.5, 0.2], + 'mean': datetimes[1], + 'median': datetimes[1], + 'min': datetimes[0], + 'max': datetimes[-1], + 'quantiles': datetimes + }, { + 'coords': datetimes, + 'vals': [0.2, 0.3, 0.4], + 'mean': datetimes[2], + 'median': datetimes[2], + 'min': datetimes[0], + 'max': datetimes[-1], + 'quantiles': datetimes + }] + + +def test_datetime_positions_with_datetime64(): + """Test that datetime positions with float widths raise TypeError.""" + fig, ax = plt.subplots() + positions = [np.datetime64('2020-01-01'), np.datetime64('2021-01-01')] + widths = [0.5, 1.0] + with pytest.raises(TypeError, + match="np.datetime64 'position' values, require np.timedelta64 'widths'"): + ax.violin(violin_plot_stats(), positions=positions, widths=widths) + + +def test_datetime_positions_with_float_widths_raises(): + """Test that datetime positions with float widths raise TypeError.""" + fig, ax = plt.subplots() + positions = [datetime.datetime(2020, 1, 1), datetime.datetime(2021, 1, 1)] + widths = [0.5, 1.0] + with pytest.raises(TypeError, + match="datetime/date 'position' values, require timedelta 'widths'"): + ax.violin(violin_plot_stats(), positions=positions, widths=widths) + + +def test_datetime_positions_with_scalar_float_width_raises(): + """Test that datetime positions with scalar float width raise TypeError.""" + fig, ax = plt.subplots() + positions = [datetime.datetime(2020, 1, 1), datetime.datetime(2021, 1, 1)] + widths = 0.75 + with pytest.raises(TypeError, + match="datetime/date 'position' values, require timedelta 'widths'"): + ax.violin(violin_plot_stats(), positions=positions, widths=widths) + + +def test_numeric_positions_with_float_widths_ok(): + """Test that numeric positions with float widths work.""" + fig, ax = plt.subplots() + positions = [1.0, 2.0] + widths = [0.5, 1.0] + ax.violin(violin_plot_stats(), positions=positions, widths=widths) + + +def test_mixed_positions_datetime_and_numeric_behaves(): + """Test that mixed datetime and numeric positions + with float widths raise TypeError. + """ + fig, ax = plt.subplots() + positions = [datetime.datetime(2020, 1, 1), 2.0] + widths = [0.5, 1.0] + with pytest.raises(TypeError, + match="datetime/date 'position' values, require timedelta 'widths'"): + ax.violin(violin_plot_stats(), positions=positions, widths=widths) + + def test_violinplot_bad_positions(): ax = plt.axes() # First 9 digits of frac(sqrt(47))