From 7b44fcb97f0f5e83c0c9002f3b6bf92e9fa8e530 Mon Sep 17 00:00:00 2001 From: Hasan Rashid Date: Sat, 15 Nov 2025 21:33:53 -0500 Subject: [PATCH 1/5] Improving error message for width and position type mismatch in violinplot --- lib/matplotlib/axes/_axes.py | 14 ++++++ lib/matplotlib/tests/test_axes.py | 75 +++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 778c72aaedf9..cc4e8364d186 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2,6 +2,7 @@ import itertools import logging import math +import datetime from numbers import Integral, Number, Real import re @@ -9057,6 +9058,19 @@ 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 b5c965d0eb4d..c288e15a4272 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)) From 4d9c8cdc82e256bc7945b0faedc8050a08fe6fb7 Mon Sep 17 00:00:00 2001 From: Hasan Rashid Date: Sat, 15 Nov 2025 21:33:53 -0500 Subject: [PATCH 2/5] Improving error message for width and position type mismatch in violinplot --- lib/matplotlib/axes/_axes.py | 14 ++++++ lib/matplotlib/tests/test_axes.py | 75 +++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 778c72aaedf9..cc4e8364d186 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2,6 +2,7 @@ import itertools import logging import math +import datetime from numbers import Integral, Number, Real import re @@ -9057,6 +9058,19 @@ 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 b5c965d0eb4d..0c5f658776f8 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': 0.5, + 'median': 0.5, + 'min': 0.1, + 'max': 0.2, + 'quantiles': [0.1, 0.5, 0.2] + }, { + 'coords': datetimes, + 'vals': [0.2, 0.3, 0.4], + 'mean': 0.3, + 'median': 0.3, + 'min': 0.2, + 'max': 0.4, + 'quantiles': [0.2, 0.3, 0.4] + }] + + +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)) From c40820b7c0357ad68f6f6ad0a2db702cc9c8e381 Mon Sep 17 00:00:00 2001 From: Hasan Rashid Date: Sat, 29 Nov 2025 01:41:19 -0500 Subject: [PATCH 3/5] Fix violin plot statistics in test data --- lib/matplotlib/tests/test_axes.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 0c5f658776f8..df5c576e2a6c 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4129,20 +4129,20 @@ def violin_plot_stats(): ] return [{ 'coords': datetimes, - 'vals': [0.1, 0.5, 0.2], - 'mean': 0.5, - 'median': 0.5, - 'min': 0.1, - 'max': 0.2, - 'quantiles': [0.1, 0.5, 0.2] + 'vals': [1.2, 2.8, 1.5, 3.1, 2.0, 1.8, 2.5], + 'mean': 2.1285714285714286, + 'median': 2.0, + 'min': 1.2, + 'max': 3.1, + 'quantiles': [1.5, 2.0, 2.5] }, { 'coords': datetimes, - 'vals': [0.2, 0.3, 0.4], - 'mean': 0.3, - 'median': 0.3, - 'min': 0.2, - 'max': 0.4, - 'quantiles': [0.2, 0.3, 0.4] + 'vals': [0.8, 1.1, 0.9, 1.4, 1.0, 1.3, 1.2], + 'mean': 1.1, + 'median': 1.1, + 'min': 0.8, + 'max': 1.4, + 'quantiles': [0.95, 1.1, 1.25] }] From 2cb8abaebe8d73d3d29912042385606dde35ba8a Mon Sep 17 00:00:00 2001 From: Hasan Rashid Date: Sun, 7 Dec 2025 15:19:54 -0500 Subject: [PATCH 4/5] Trigger CI pipeline From 8ff4f125325188e47482d939a33e919028ac6b71 Mon Sep 17 00:00:00 2001 From: Hasan Rashid Date: Sun, 7 Dec 2025 15:19:54 -0500 Subject: [PATCH 5/5] Trigger CI pipeline --- lib/matplotlib/tests/test_axes.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index df5c576e2a6c..3fe0c410bff4 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4129,20 +4129,20 @@ def violin_plot_stats(): ] return [{ 'coords': datetimes, - 'vals': [1.2, 2.8, 1.5, 3.1, 2.0, 1.8, 2.5], - 'mean': 2.1285714285714286, - 'median': 2.0, + 'vals': [1.2, 2.8, 1.5], + 'mean': 1.84, + 'median': 1.5, 'min': 1.2, - 'max': 3.1, - 'quantiles': [1.5, 2.0, 2.5] + 'max': 2.8, + 'quantiles': [1.2, 1.5, 2.8] }, { 'coords': datetimes, - 'vals': [0.8, 1.1, 0.9, 1.4, 1.0, 1.3, 1.2], - 'mean': 1.1, - 'median': 1.1, + 'vals': [0.8, 1.1, 0.9], + 'mean': 0.94, + 'median': 0.9, 'min': 0.8, - 'max': 1.4, - 'quantiles': [0.95, 1.1, 1.25] + 'max': 1.1, + 'quantiles': [0.8, 0.9, 1.1] }]