Skip to content

Commit 8eff93a

Browse files
authored
Merge branch 'main' into rm/fix-step-plot
2 parents f902129 + 1ccd51d commit 8eff93a

5 files changed

Lines changed: 353 additions & 35 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
55
## Unreleased
66

77
### Fixed
8+
- Fix `mpl_to_plotly` tick marker mirroring, axis positioning, and trace visibility for twinned and overlaid subplots (such as `twinx` and `twiny`) [[#5310](https://github.com/plotly/plotly.py/pull/5310)], with thanks to @robertoffmoura for the contribution!
89
- Fix concurrent first access to lazily initialized graph object properties, which could raise `ValueError("Invalid value")` [[#5691](https://github.com/plotly/plotly.py/pull/5691)], with thanks to @hb1915 for the contribution!
910
- Fix `mpl_to_plotly` crashing on touching bars (such as `plt.hist`) due to floating-point noise producing negative `bargap` values by clamping `bargap` to `[0, 1]` [[#5696](https://github.com/plotly/plotly.py/pull/5696)], with thanks to @robertoffmoura for the contribution!
1011
- Fix `mpl_to_plotly` drawing step plots with diagonal lines instead of vertical/horizontal step segments by mapping matplotlib step drawstyles (`steps-pre`, `steps-post`, `steps-mid`) to plotly line shapes [[#5697](https://github.com/plotly/plotly.py/pull/5697)], with thanks to @robertoffmoura for the contribution!
12+
- Export tick label colors to the plotly tickfont in `mpl_to_plotly` [[#5716](https://github.com/plotly/plotly.py/pull/5716)], with thanks to @robertoffmoura for the contribution!
1113
- Fix `px.sunburst`, `px.treemap` and `px.icicle` listing sectors in a different order on every run when `path` is used with a Polars DataFrame; sectors now follow their order of first appearance for all dataframe backends [[#5766](https://github.com/plotly/plotly.py/pull/5766)], with thanks to @Irahan2 for the contribution!
1214

1315
## [7.1.0] - 2026-09-15

‎plotly/matplotlylib/mplexporter/utils.py‎

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -200,18 +200,36 @@ def get_text_style(text):
200200
def get_axis_properties(axis):
201201
"""Return the property dictionary for a matplotlib.Axis instance"""
202202
props = {}
203-
label1On = axis.get_tick_params().get("label1On", True)
203+
tick_params = axis.get_tick_params()
204204

205205
if isinstance(axis, matplotlib.axis.XAxis):
206-
if label1On:
207-
props["position"] = "bottom"
208-
else:
206+
if (
207+
axis.get_ticks_position() == "top"
208+
or axis.get_label_position() == "top"
209+
or (
210+
tick_params.get("labeltop", False)
211+
and not tick_params.get("labelbottom", False)
212+
)
213+
or (tick_params.get("top", False) and not tick_params.get("bottom", False))
214+
or not tick_params.get("label1On", True)
215+
):
209216
props["position"] = "top"
210-
elif isinstance(axis, matplotlib.axis.YAxis):
211-
if label1On:
212-
props["position"] = "left"
213217
else:
218+
props["position"] = "bottom"
219+
elif isinstance(axis, matplotlib.axis.YAxis):
220+
if (
221+
axis.get_ticks_position() == "right"
222+
or axis.get_label_position() == "right"
223+
or (
224+
tick_params.get("labelright", False)
225+
and not tick_params.get("labelleft", False)
226+
)
227+
or (tick_params.get("right", False) and not tick_params.get("left", False))
228+
or not tick_params.get("label1On", True)
229+
):
214230
props["position"] = "right"
231+
else:
232+
props["position"] = "left"
215233
else:
216234
raise ValueError("{0} should be an Axis instance".format(axis))
217235

@@ -239,12 +257,15 @@ def get_axis_properties(axis):
239257
# Get axis scale
240258
props["scale"] = axis.get_scale()
241259

242-
# Get major tick label size (assumes that's all we really care about!)
260+
# Get major tick label size and color (assumes that's all we really
261+
# care about!)
243262
labels = axis.get_ticklabels()
244263
if labels:
245264
props["fontsize"] = labels[0].get_fontsize()
265+
props["fontcolor"] = export_color(labels[0].get_color())
246266
else:
247267
props["fontsize"] = None
268+
props["fontcolor"] = None
248269

249270
# Get associated grid
250271
props["grid"] = get_grid_style(axis)

‎plotly/matplotlylib/mpltools.py‎

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -250,15 +250,12 @@ def get_axes_bounds(fig):
250250
return (x_min, x_max), (y_min, y_max)
251251

252252

253-
def get_axis_mirror(main_spine, mirror_spine):
254-
if main_spine and mirror_spine:
253+
def get_axis_mirror(main_spine, mirror_spine, main_tick_markers, mirror_tick_markers):
254+
if main_spine and mirror_spine and main_tick_markers and mirror_tick_markers:
255255
return "ticks"
256-
elif main_spine and not mirror_spine:
257-
return False
258-
elif not main_spine and mirror_spine:
259-
return False # can't handle this case yet!
260-
else:
261-
return False # nuttin'!
256+
if main_spine and mirror_spine:
257+
return True
258+
return False
262259

263260

264261
def get_bar_gap(bar_starts, bar_ends, tol=1e-10):
@@ -533,14 +530,32 @@ def prep_ticks(ax, index, ax_type, props):
533530
return axis_dict
534531

535532

533+
def _export_color(color):
534+
"""Export a matplotlib color for use as a plotly color.
535+
536+
matplotlib uses "none" for fully transparent colors, which plotly does not
537+
accept, so transparent colors are exported as transparent black.
538+
Colors already exported by the mplexporter (hex or rgba strings) are
539+
passed through unchanged.
540+
"""
541+
if color is None:
542+
return None
543+
if isinstance(color, str):
544+
return color if color != "none" else "rgba(0,0,0,0)"
545+
return [_export_color(c) for c in color]
546+
547+
536548
def prep_xy_axis(ax, props, x_bounds, y_bounds):
537549
xaxis = dict(
538550
type=props["axes"][0]["scale"],
539551
range=list(props["xlim"]),
540552
showgrid=props["axes"][0]["grid"]["gridOn"],
541553
domain=convert_x_domain(props["bounds"], x_bounds),
542554
side=props["axes"][0]["position"],
543-
tickfont=dict(size=props["axes"][0]["fontsize"]),
555+
tickfont=dict(
556+
size=props["axes"][0]["fontsize"],
557+
color=_export_color(props["axes"][0]["fontcolor"]),
558+
),
544559
)
545560
xaxis.update(prep_ticks(ax, 0, "x", props))
546561
yaxis = dict(
@@ -549,7 +564,10 @@ def prep_xy_axis(ax, props, x_bounds, y_bounds):
549564
showgrid=props["axes"][1]["grid"]["gridOn"],
550565
domain=convert_y_domain(props["bounds"], y_bounds),
551566
side=props["axes"][1]["position"],
552-
tickfont=dict(size=props["axes"][1]["fontsize"]),
567+
tickfont=dict(
568+
size=props["axes"][1]["fontsize"],
569+
color=_export_color(props["axes"][1]["fontcolor"]),
570+
),
553571
)
554572
yaxis.update(prep_ticks(ax, 1, "y", props))
555573
return xaxis, yaxis

‎plotly/matplotlylib/renderer.py‎

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,7 @@
1414
from plotly.matplotlylib import mpltools
1515

1616

17-
def _export_color(color):
18-
"""Export a matplotlib color for use as a plotly color.
19-
20-
matplotlib uses "none" for fully transparent colors, which plotly does not
21-
accept, so transparent colors are exported as transparent black.
22-
Colors already exported by the mplexporter (hex or rgba strings) are
23-
passed through unchanged.
24-
"""
25-
if isinstance(color, str):
26-
return "rgba(0,0,0,0)" if color == "none" else color
27-
return [_export_color(c) for c in color]
17+
from plotly.matplotlylib.mpltools import _export_color
2818

2919

3020
class PlotlyRenderer(Renderer):
@@ -67,6 +57,7 @@ def __init__(self):
6757
self.msg = "Initialized PlotlyRenderer\n"
6858
self._processing_legend = False
6959
self._legend_visible = False
60+
self.axes_list = []
7061

7162
def _convert_x_dates(self, x):
7263
"""Convert x values to date strings when the x-axis is a date axis."""
@@ -167,8 +158,8 @@ def open_axes(self, ax, props):
167158
]
168159
self.current_bars = []
169160
self.axis_ct += 1
170-
# update plot background with the axes background from mpl
171-
self.plotly_fig["layout"].plot_bgcolor = _export_color(props["axesbg"])
161+
if props.get("patch_visible", True):
162+
self.plotly_fig["layout"].plot_bgcolor = _export_color(props["axesbg"])
172163
# set defaults in axes
173164
xaxis = go.layout.XAxis(
174165
anchor="y{0}".format(self.axis_ct), zeroline=False, ticks="inside"
@@ -186,10 +177,63 @@ def open_axes(self, ax, props):
186177
top_spine = mpltools.get_spine_visible(ax, "top")
187178
left_spine = mpltools.get_spine_visible(ax, "left")
188179
right_spine = mpltools.get_spine_visible(ax, "right")
189-
xaxis["mirror"] = mpltools.get_axis_mirror(bottom_spine, top_spine)
190-
yaxis["mirror"] = mpltools.get_axis_mirror(left_spine, right_spine)
191-
xaxis["showline"] = bottom_spine
192-
yaxis["showline"] = top_spine
180+
x_tick_params = ax.xaxis.get_tick_params()
181+
y_tick_params = ax.yaxis.get_tick_params()
182+
bottom_tick_markers = x_tick_params.get(
183+
"bottom", x_tick_params.get("left", True)
184+
)
185+
top_tick_markers = x_tick_params.get("top", x_tick_params.get("right", False))
186+
left_tick_markers = y_tick_params.get("left", True)
187+
right_tick_markers = y_tick_params.get("right", False)
188+
if xaxis["side"] == "top":
189+
x_main_spine, x_mirror_spine = top_spine, bottom_spine
190+
x_main_ticks, x_mirror_ticks = top_tick_markers, bottom_tick_markers
191+
else:
192+
x_main_spine, x_mirror_spine = bottom_spine, top_spine
193+
x_main_ticks, x_mirror_ticks = bottom_tick_markers, top_tick_markers
194+
195+
xaxis["mirror"] = mpltools.get_axis_mirror(
196+
x_main_spine, x_mirror_spine, x_main_ticks, x_mirror_ticks
197+
)
198+
xaxis["showline"] = x_main_spine
199+
# hide tick markers when the mpl main-side tick markers are hidden
200+
if not x_main_ticks:
201+
xaxis["ticks"] = ""
202+
203+
if yaxis["side"] == "right":
204+
y_main_spine, y_mirror_spine = right_spine, left_spine
205+
y_main_ticks, y_mirror_ticks = right_tick_markers, left_tick_markers
206+
else:
207+
y_main_spine, y_mirror_spine = left_spine, right_spine
208+
y_main_ticks, y_mirror_ticks = left_tick_markers, right_tick_markers
209+
210+
yaxis["mirror"] = mpltools.get_axis_mirror(
211+
y_main_spine, y_mirror_spine, y_main_ticks, y_mirror_ticks
212+
)
213+
yaxis["showline"] = y_main_spine
214+
if not y_main_ticks:
215+
yaxis["ticks"] = ""
216+
217+
overlay_ax_ct = None
218+
for prev_ax, prev_ct in self.axes_list:
219+
# Overlay only axes that cover the same area. Shared-axis subplots, such
220+
# as the ones from plt.subplots(sharex=True), sit in different places.
221+
if ax.get_position().bounds == prev_ax.get_position().bounds:
222+
overlay_ax_ct = prev_ct
223+
break
224+
225+
if overlay_ax_ct is not None:
226+
overlay_x = "x" if overlay_ax_ct == 1 else "x{0}".format(overlay_ax_ct)
227+
overlay_y = "y" if overlay_ax_ct == 1 else "y{0}".format(overlay_ax_ct)
228+
xaxis["overlaying"] = overlay_x
229+
yaxis["overlaying"] = overlay_y
230+
231+
if not props["axes"][0]["visible"]:
232+
xaxis["visible"] = False
233+
if not props["axes"][1]["visible"]:
234+
yaxis["visible"] = False
235+
236+
self.axes_list.append((ax, self.axis_ct))
193237

194238
# put axes in our figure
195239
self.plotly_fig["layout"]["xaxis{0}".format(self.axis_ct)] = xaxis

0 commit comments

Comments
 (0)