From df1a759ecf7f30862e9cb2ffdadefbdb865557a1 Mon Sep 17 00:00:00 2001 From: robertoffmoura Date: Thu, 7 Aug 2025 16:09:58 +0100 Subject: [PATCH 1/2] Fix tick marker mirroring --- plotly/matplotlylib/mpltools.py | 13 ++-- plotly/matplotlylib/renderer.py | 12 +++- plotly/matplotlylib/tests/__init__.py | 4 ++ plotly/matplotlylib/tests/test_renderer.py | 76 ++++++++++++++++++++++ 4 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 plotly/matplotlylib/tests/__init__.py create mode 100644 plotly/matplotlylib/tests/test_renderer.py diff --git a/plotly/matplotlylib/mpltools.py b/plotly/matplotlylib/mpltools.py index 42681360030..5303e910cfa 100644 --- a/plotly/matplotlylib/mpltools.py +++ b/plotly/matplotlylib/mpltools.py @@ -265,15 +265,12 @@ def get_axes_bounds(fig): return (x_min, x_max), (y_min, y_max) -def get_axis_mirror(main_spine, mirror_spine): - if main_spine and mirror_spine: +def get_axis_mirror(main_spine, mirror_spine, main_tick_markers, mirror_tick_markers): + if main_spine and mirror_spine and main_tick_markers and mirror_tick_markers: return "ticks" - elif main_spine and not mirror_spine: - return False - elif not main_spine and mirror_spine: - return False # can't handle this case yet! - else: - return False # nuttin'! + if main_spine and mirror_spine: + return True + return False def get_bar_gap(bar_starts, bar_ends, tol=1e-10): diff --git a/plotly/matplotlylib/renderer.py b/plotly/matplotlylib/renderer.py index c95de522477..1d8e62caa9e 100644 --- a/plotly/matplotlylib/renderer.py +++ b/plotly/matplotlylib/renderer.py @@ -168,8 +168,16 @@ def open_axes(self, ax, props): top_spine = mpltools.get_spine_visible(ax, "top") left_spine = mpltools.get_spine_visible(ax, "left") right_spine = mpltools.get_spine_visible(ax, "right") - xaxis["mirror"] = mpltools.get_axis_mirror(bottom_spine, top_spine) - yaxis["mirror"] = mpltools.get_axis_mirror(left_spine, right_spine) + bottom_tick_markers = ax.xaxis.get_tick_params()["bottom"] + top_tick_markers = ax.xaxis.get_tick_params()["top"] + left_tick_markers = ax.yaxis.get_tick_params()["left"] + right_tick_markers = ax.yaxis.get_tick_params()["right"] + xaxis["mirror"] = mpltools.get_axis_mirror( + bottom_spine, top_spine, bottom_tick_markers, top_tick_markers + ) + yaxis["mirror"] = mpltools.get_axis_mirror( + left_spine, right_spine, left_tick_markers, right_tick_markers + ) xaxis["showline"] = bottom_spine yaxis["showline"] = top_spine diff --git a/plotly/matplotlylib/tests/__init__.py b/plotly/matplotlylib/tests/__init__.py new file mode 100644 index 00000000000..c29e9896d61 --- /dev/null +++ b/plotly/matplotlylib/tests/__init__.py @@ -0,0 +1,4 @@ +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt diff --git a/plotly/matplotlylib/tests/test_renderer.py b/plotly/matplotlylib/tests/test_renderer.py new file mode 100644 index 00000000000..47d2bb6a9f8 --- /dev/null +++ b/plotly/matplotlylib/tests/test_renderer.py @@ -0,0 +1,76 @@ +import plotly.tools as tls + +from . import plt + +def test_axis_mirror_with_spines_and_ticks(): + """Test that mirror=True when both spines and ticks are visible on both sides.""" + fig, ax = plt.subplots() + ax.plot([0, 1], [0, 1]) + + # Show all spines + ax.spines['top'].set_visible(True) + ax.spines['bottom'].set_visible(True) + ax.spines['left'].set_visible(True) + ax.spines['right'].set_visible(True) + + # Show ticks on all sides + ax.tick_params(top=True, bottom=True, left=True, right=True) + + plotly_fig = tls.mpl_to_plotly(fig) + + assert plotly_fig.layout.xaxis.mirror == "ticks" + assert plotly_fig.layout.yaxis.mirror == "ticks" + + +def test_axis_mirror_with_ticks_only(): + """Test that mirror=False when spines are not visible on both sides.""" + fig, ax = plt.subplots() + ax.plot([0, 1], [0, 1]) + + # Hide opposite spines + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + + # Show ticks on all sides + ax.tick_params(top=True, bottom=True, left=True, right=True) + + plotly_fig = tls.mpl_to_plotly(fig) + + assert plotly_fig.layout.xaxis.mirror == False + assert plotly_fig.layout.yaxis.mirror == False + + +def test_axis_mirror_false_with_one_sided_ticks(): + """Test that mirror=True when ticks are only on one side but spines are + visible on both sides.""" + fig, ax = plt.subplots() + ax.plot([0, 1], [0, 1]) + + # Default matplotlib behavior - ticks only on bottom and left + ax.tick_params(top=False, bottom=True, left=True, right=False) + + plotly_fig = tls.mpl_to_plotly(fig) + + assert plotly_fig.layout.xaxis.mirror == True + assert plotly_fig.layout.yaxis.mirror == True + + +def test_axis_mirror_mixed_configurations(): + """Test different configurations for x and y axes.""" + fig, ax = plt.subplots() + ax.plot([0, 1], [0, 1]) + + # X-axis: spines and ticks on both sides (mirror="ticks") + ax.spines['top'].set_visible(True) + ax.spines['bottom'].set_visible(True) + ax.tick_params(top=True, bottom=True) + + # Y-axis: spine only on one side (mirror=False) + ax.spines['right'].set_visible(False) + ax.spines['left'].set_visible(True) + ax.tick_params(left=True, right=True) + + plotly_fig = tls.mpl_to_plotly(fig) + + assert plotly_fig.layout.xaxis.mirror == "ticks" + assert plotly_fig.layout.yaxis.mirror == False From 001ff5347c36bfd0d077234ac29f37d52c3ed77d Mon Sep 17 00:00:00 2001 From: robertoffmoura Date: Tue, 12 Aug 2025 09:59:22 +0100 Subject: [PATCH 2/2] Run ruff format --- plotly/matplotlylib/renderer.py | 4 ++-- plotly/matplotlylib/tests/test_renderer.py | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/plotly/matplotlylib/renderer.py b/plotly/matplotlylib/renderer.py index 1d8e62caa9e..5760fbb3923 100644 --- a/plotly/matplotlylib/renderer.py +++ b/plotly/matplotlylib/renderer.py @@ -307,7 +307,7 @@ def draw_bar(self, coll): ) # TODO ditto if len(bar["x"]) > 1: self.msg += " Heck yeah, I drew that bar chart\n" - (self.plotly_fig.add_trace(bar),) + self.plotly_fig.add_trace(bar) if bar_gap is not None: self.plotly_fig["layout"]["bargap"] = bar_gap else: @@ -505,7 +505,7 @@ def draw_marked_line(self, **props): marked_line["x"] = mpltools.mpl_dates_to_datestrings( marked_line["x"], formatter ) - (self.plotly_fig.add_trace(marked_line),) + self.plotly_fig.add_trace(marked_line) self.msg += " Heck yeah, I drew that line\n" elif props["coordinates"] == "axes": # dealing with legend graphical elements diff --git a/plotly/matplotlylib/tests/test_renderer.py b/plotly/matplotlylib/tests/test_renderer.py index 47d2bb6a9f8..23a3a62f729 100644 --- a/plotly/matplotlylib/tests/test_renderer.py +++ b/plotly/matplotlylib/tests/test_renderer.py @@ -2,16 +2,17 @@ from . import plt + def test_axis_mirror_with_spines_and_ticks(): """Test that mirror=True when both spines and ticks are visible on both sides.""" fig, ax = plt.subplots() ax.plot([0, 1], [0, 1]) # Show all spines - ax.spines['top'].set_visible(True) - ax.spines['bottom'].set_visible(True) - ax.spines['left'].set_visible(True) - ax.spines['right'].set_visible(True) + ax.spines["top"].set_visible(True) + ax.spines["bottom"].set_visible(True) + ax.spines["left"].set_visible(True) + ax.spines["right"].set_visible(True) # Show ticks on all sides ax.tick_params(top=True, bottom=True, left=True, right=True) @@ -28,8 +29,8 @@ def test_axis_mirror_with_ticks_only(): ax.plot([0, 1], [0, 1]) # Hide opposite spines - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) # Show ticks on all sides ax.tick_params(top=True, bottom=True, left=True, right=True) @@ -61,13 +62,13 @@ def test_axis_mirror_mixed_configurations(): ax.plot([0, 1], [0, 1]) # X-axis: spines and ticks on both sides (mirror="ticks") - ax.spines['top'].set_visible(True) - ax.spines['bottom'].set_visible(True) + ax.spines["top"].set_visible(True) + ax.spines["bottom"].set_visible(True) ax.tick_params(top=True, bottom=True) # Y-axis: spine only on one side (mirror=False) - ax.spines['right'].set_visible(False) - ax.spines['left'].set_visible(True) + ax.spines["right"].set_visible(False) + ax.spines["left"].set_visible(True) ax.tick_params(left=True, right=True) plotly_fig = tls.mpl_to_plotly(fig)