From 963ab32162955a3abe816225b8f1084b4f272fab Mon Sep 17 00:00:00 2001 From: Hasan Rashid Date: Tue, 2 Sep 2025 19:01:29 -0400 Subject: [PATCH 1/7] Added conversion from datetime to float for position and width parameters in violinplot --- lib/matplotlib/axes/_axes.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 6da0e925ab4f..1166eb8e13da 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -38,7 +38,8 @@ from matplotlib.axes._secondary_axes import SecondaryAxis from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer from matplotlib.transforms import _ScaledRotation - +import matplotlib.dates as mdates +import datetime _log = logging.getLogger(__name__) @@ -9051,6 +9052,22 @@ def violin(self, vpstats, positions=None, vert=None, elif len(positions) != N: raise ValueError(datashape_message.format("positions")) + #Checks if position is datetime; Converts to float if it is + if positions is not None: + positions = [ + mdates.date2num(pos) if isinstance(pos, (datetime.datetime, datetime.date)) else pos + for pos in positions + ] + + #Check if width is provided as time difference; convert to days if it is + if widths is not None: + if np.isscalar(widths): + widths = [widths] * N + widths = [ + w.days if isinstance(w, datetime.timedelta) else w + for w in widths + ] + # Validate widths if np.isscalar(widths): widths = [widths] * N From 0f553155f8b2d2c7341b48afed345bb0d5632af2 Mon Sep 17 00:00:00 2001 From: Hasan Rashid Date: Tue, 2 Sep 2025 20:52:19 -0400 Subject: [PATCH 2/7] Fix linting error by making lines shorter --- lib/matplotlib/axes/_axes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 1166eb8e13da..3c39d002b442 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -9055,7 +9055,8 @@ def violin(self, vpstats, positions=None, vert=None, #Checks if position is datetime; Converts to float if it is if positions is not None: positions = [ - mdates.date2num(pos) if isinstance(pos, (datetime.datetime, datetime.date)) else pos + mdates.date2num(pos) if isinstance(pos, + (datetime.datetime, datetime.date)) else pos for pos in positions ] From 7034a735a2a69372c5fe4a62908015d2e5e70023 Mon Sep 17 00:00:00 2001 From: Hasan Rashid Date: Tue, 2 Sep 2025 20:57:45 -0400 Subject: [PATCH 3/7] Fixing trailing whitespace --- lib/matplotlib/axes/_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 3c39d002b442..01906a495528 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -9055,7 +9055,7 @@ def violin(self, vpstats, positions=None, vert=None, #Checks if position is datetime; Converts to float if it is if positions is not None: positions = [ - mdates.date2num(pos) if isinstance(pos, + mdates.date2num(pos) if isinstance(pos, (datetime.datetime, datetime.date)) else pos for pos in positions ] From 2065fd21afffd43ffcf48b6c69e8ab55037e78d4 Mon Sep 17 00:00:00 2001 From: Hasan Rashid Date: Sat, 6 Sep 2025 02:56:36 -0400 Subject: [PATCH 4/7] Adding some tests for new code coverage --- .../tests/test_violinplot_datetime.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 lib/matplotlib/tests/test_violinplot_datetime.py diff --git a/lib/matplotlib/tests/test_violinplot_datetime.py b/lib/matplotlib/tests/test_violinplot_datetime.py new file mode 100644 index 000000000000..018817a8135c --- /dev/null +++ b/lib/matplotlib/tests/test_violinplot_datetime.py @@ -0,0 +1,63 @@ +import pytest +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +import datetime +import numpy as np + +from matplotlib.testing.decorators import image_comparison + +def violin_plot_stats(): + # Stats for violin plot + 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_violin_datetime_positions_timedelta_widths(): + fig, ax = plt.subplots() + vpstats = violin_plot_stats() + positions = [datetime.datetime(2020, 1, 1), datetime.datetime(2021, 1, 1)] + widths = [datetime.timedelta(days=10), datetime.timedelta(days=20)] + ax.violin(vpstats, positions=positions, widths=widths) + plt.close(fig) + +def test_violin_date_positions_float_widths(): + fig, ax = plt.subplots() + vpstats = violin_plot_stats() + positions = [datetime.date(2020, 1, 1), datetime.date(2021, 1, 1)] + widths = [0.5, 1.0] + ax.violin(vpstats, positions=positions, widths=widths) + plt.close(fig) + +def test_violin_mixed_positions_widths(): + fig, ax = plt.subplots() + vpstats = violin_plot_stats() + positions = [datetime.datetime(2020, 1, 1), mdates.date2num(datetime.datetime(2021, 1, 1))] + widths = [datetime.timedelta(days=3), 2.0] + ax.violin(vpstats, positions=positions, widths=widths) + plt.close(fig) + +def test_violin_default_positions_widths(): + fig, ax = plt.subplots() + vpstats = violin_plot_stats() + ax.violin(vpstats) + plt.close(fig) + From 2370aa23c781d3b0d2473b1cbb5efd8064e13b2f Mon Sep 17 00:00:00 2001 From: Hasan Rashid Date: Sat, 6 Sep 2025 03:15:05 -0400 Subject: [PATCH 5/7] pylint and ruff check errors fixed --- lib/matplotlib/tests/test_violinplot_datetime.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_violinplot_datetime.py b/lib/matplotlib/tests/test_violinplot_datetime.py index 018817a8135c..6b5598f26fd4 100644 --- a/lib/matplotlib/tests/test_violinplot_datetime.py +++ b/lib/matplotlib/tests/test_violinplot_datetime.py @@ -1,10 +1,7 @@ -import pytest import matplotlib.pyplot as plt import matplotlib.dates as mdates import datetime -import numpy as np -from matplotlib.testing.decorators import image_comparison def violin_plot_stats(): # Stats for violin plot @@ -31,6 +28,7 @@ def violin_plot_stats(): 'quantiles': datetimes }] + def test_violin_datetime_positions_timedelta_widths(): fig, ax = plt.subplots() vpstats = violin_plot_stats() @@ -39,6 +37,7 @@ def test_violin_datetime_positions_timedelta_widths(): ax.violin(vpstats, positions=positions, widths=widths) plt.close(fig) + def test_violin_date_positions_float_widths(): fig, ax = plt.subplots() vpstats = violin_plot_stats() @@ -47,17 +46,19 @@ def test_violin_date_positions_float_widths(): ax.violin(vpstats, positions=positions, widths=widths) plt.close(fig) + def test_violin_mixed_positions_widths(): fig, ax = plt.subplots() vpstats = violin_plot_stats() - positions = [datetime.datetime(2020, 1, 1), mdates.date2num(datetime.datetime(2021, 1, 1))] + positions = [datetime.datetime(2020, 1, 1), + mdates.date2num(datetime.datetime(2021, 1, 1))] widths = [datetime.timedelta(days=3), 2.0] ax.violin(vpstats, positions=positions, widths=widths) plt.close(fig) + def test_violin_default_positions_widths(): fig, ax = plt.subplots() vpstats = violin_plot_stats() ax.violin(vpstats) plt.close(fig) - From de64e194d0adb09cf0c92de1acd9015d23af1acc Mon Sep 17 00:00:00 2001 From: Hasan Rashid Date: Tue, 2 Sep 2025 19:01:29 -0400 Subject: [PATCH 6/7] Added conversion from datetime to float for position and width parameters in violinplot Resolve merge conflict --- lib/matplotlib/axes/_axes.py | 13 ++- .../tests/test_violinplot_datetime.py | 80 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 lib/matplotlib/tests/test_violinplot_datetime.py diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 6da0e925ab4f..53dfbc2a2e91 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -38,7 +38,7 @@ from matplotlib.axes._secondary_axes import SecondaryAxis from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer from matplotlib.transforms import _ScaledRotation - +import datetime _log = logging.getLogger(__name__) @@ -9051,6 +9051,17 @@ def violin(self, vpstats, positions=None, vert=None, elif len(positions) != N: raise ValueError(datashape_message.format("positions")) + #Checkif positions are datetime-like, widths must be timedelta-like. + if any(isinstance(p, (datetime.datetime, datetime.date)) for p in positions): + _widths = widths if not np.isscalar(widths) else [widths] * N + if any(not isinstance(w, (datetime.timedelta, np.timedelta64)) + for w in _widths): + raise TypeError( + "If positions are datetime/date values, pass widths as " + "datetime.timedelta (e.g., datetime.timedelta(days=10)) " + "or numpy.timedelta64." + ) + # Validate widths if np.isscalar(widths): widths = [widths] * N diff --git a/lib/matplotlib/tests/test_violinplot_datetime.py b/lib/matplotlib/tests/test_violinplot_datetime.py new file mode 100644 index 000000000000..8134e738edc5 --- /dev/null +++ b/lib/matplotlib/tests/test_violinplot_datetime.py @@ -0,0 +1,80 @@ +"""Tests for datetime and timedelta support in violinplot.""" + +import datetime +import pytest +import matplotlib.pyplot as plt + +def make_vpstats(): + """Create minimal valid stats for a violin plot.""" + 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_float_widths_raises(): + """Test that datetime positions with float widths raise TypeError.""" + fig, ax = plt.subplots() + try: + vpstats = make_vpstats() + positions = [datetime.datetime(2020, 1, 1), datetime.datetime(2021, 1, 1)] + widths = [0.5, 1.0] + with pytest.raises(TypeError, + match="positions are datetime/date.*widths as datetime\\.timedelta"): + ax.violin(vpstats, positions=positions, widths=widths) + finally: + plt.close(fig) + +def test_datetime_positions_with_scalar_float_width_raises(): + """Test that datetime positions with scalar float width raise TypeError.""" + fig, ax = plt.subplots() + try: + vpstats = make_vpstats() + positions = [datetime.datetime(2020, 1, 1), datetime.datetime(2021, 1, 1)] + widths = 0.75 + with pytest.raises(TypeError, + match="positions are datetime/date.*widths as datetime\\.timedelta"): + ax.violin(vpstats, positions=positions, widths=widths) + finally: + plt.close(fig) + +def test_numeric_positions_with_float_widths_ok(): + """Test that numeric positions with float widths work.""" + fig, ax = plt.subplots() + try: + vpstats = make_vpstats() + positions = [1.0, 2.0] + widths = [0.5, 1.0] + ax.violin(vpstats, positions=positions, widths=widths) + finally: + plt.close(fig) + +def test_mixed_positions_datetime_and_numeric_behaves(): + """Test that mixed datetime and numeric positions with float widths raise TypeError.""" + fig, ax = plt.subplots() + try: + vpstats = make_vpstats() + positions = [datetime.datetime(2020, 1, 1), 2.0] + widths = [0.5, 1.0] + with pytest.raises(TypeError, + match="positions are datetime/date.*widths as datetime\\.timedelta"): + ax.violin(vpstats, positions=positions, widths=widths) + finally: + plt.close(fig) \ No newline at end of file From 4d0398d2b4a3209631f504500f75ec9873d756ca Mon Sep 17 00:00:00 2001 From: Hasan Rashid Date: Wed, 10 Sep 2025 20:08:26 -0400 Subject: [PATCH 7/7] Improve error message for mismatched position and width types in violinplot --- .../tests/test_violinplot_datetime.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_violinplot_datetime.py b/lib/matplotlib/tests/test_violinplot_datetime.py index 8134e738edc5..0dadede7752a 100644 --- a/lib/matplotlib/tests/test_violinplot_datetime.py +++ b/lib/matplotlib/tests/test_violinplot_datetime.py @@ -4,6 +4,7 @@ import pytest import matplotlib.pyplot as plt + def make_vpstats(): """Create minimal valid stats for a violin plot.""" datetimes = [ @@ -29,8 +30,11 @@ def make_vpstats(): 'quantiles': datetimes }] + def test_datetime_positions_with_float_widths_raises(): - """Test that datetime positions with float widths raise TypeError.""" + """Test that datetime positions with + float widths raise TypeError. + """ fig, ax = plt.subplots() try: vpstats = make_vpstats() @@ -42,8 +46,11 @@ def test_datetime_positions_with_float_widths_raises(): finally: plt.close(fig) + def test_datetime_positions_with_scalar_float_width_raises(): - """Test that datetime positions with scalar float width raise TypeError.""" + """Test that datetime positions with scalar + float width raise TypeError. + """ fig, ax = plt.subplots() try: vpstats = make_vpstats() @@ -55,8 +62,11 @@ def test_datetime_positions_with_scalar_float_width_raises(): finally: plt.close(fig) + def test_numeric_positions_with_float_widths_ok(): - """Test that numeric positions with float widths work.""" + """Test that numeric positions with + float widths work. + """ fig, ax = plt.subplots() try: vpstats = make_vpstats() @@ -66,8 +76,11 @@ def test_numeric_positions_with_float_widths_ok(): finally: plt.close(fig) + def test_mixed_positions_datetime_and_numeric_behaves(): - """Test that mixed datetime and numeric positions with float widths raise TypeError.""" + """Test that mixed datetime and numeric positions with + float widths raise TypeError. + """ fig, ax = plt.subplots() try: vpstats = make_vpstats() @@ -77,4 +90,4 @@ def test_mixed_positions_datetime_and_numeric_behaves(): match="positions are datetime/date.*widths as datetime\\.timedelta"): ax.violin(vpstats, positions=positions, widths=widths) finally: - plt.close(fig) \ No newline at end of file + plt.close(fig)