Skip to content
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added
- Support `marginal_x`/`marginal_y="heatmap"` in `density_heatmap`, drawing a single-row/column heatmap strip in the margin colored by the same `z`/`histfunc` aggregate as the main plot and sharing its color scale [[#5706](https://github.com/plotly/plotly.py/issues/5706)], with thanks to @lucasjamar for the contribution!
- Add support for custom tick values in `mpl_to_plotly` when the matplotlib tick positions don't follow an arithmetic progression, including custom tick labels and tick values on date axes [[#5262](https://github.com/plotly/plotly.py/pull/5262)], with thanks to @robertoffmoura for the contribution!

### Fixed
- Fix `mpl_to_plotly` not setting `paper_bgcolor` and `plot_bgcolor` from the matplotlib figure and axes backgrounds, so converted figures match the source figure's background colors [[#5285](https://github.com/plotly/plotly.py/pull/5285)], with thanks to @robertoffmoura for the contribution!
Expand Down
29 changes: 20 additions & 9 deletions plotly/matplotlylib/mpltools.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,18 +436,20 @@ def prep_ticks(ax, index, ax_type, props):
tick0 = tickvalues[0]
dticks = [
round(tickvalues[i] - tickvalues[i - 1], 12)
for i in range(1, len(tickvalues) - 1)
for i in range(1, len(tickvalues))
]
if all([dticks[i] == dticks[i - 1] for i in range(1, len(dticks) - 1)]):
if all([dticks[i] == dticks[i - 1] for i in range(1, len(dticks))]):
dtick = tickvalues[1] - tickvalues[0]
else:
warnings.warn(
"'linear' {0}-axis tick spacing not even, "
"ignoring mpl tick formatting.".format(ax_type)
"exporting explicit tick values instead of dtick.".format(ax_type)
)
raise TypeError
except (IndexError, TypeError):
axis_dict["nticks"] = props["axes"][index]["nticks"]
if props["axes"][index]["tickvalues"] is not None:
axis_dict["tickvals"] = props["axes"][index]["tickvalues"]
else:
axis_dict["tick0"] = tick0
axis_dict["dtick"] = dtick
Expand Down Expand Up @@ -485,17 +487,26 @@ def prep_ticks(ax, index, ax_type, props):
formatter = axis.get_major_formatter().__class__.__name__
if ax_type == "x" and "DateFormatter" in formatter:
axis_dict["type"] = "date"
try:
axis_dict["tick0"] = mpl_dates_to_datestrings(axis_dict["tick0"], formatter)
except KeyError:
pass
finally:
tickvalues = props["axes"][index]["tickvalues"]
if tickvalues is not None:
# custom ticks: export the exact locations and drop the
# arithmetic tick0/dtick spec, which plotly would use instead
axis_dict["tickvals"] = mpl_dates_to_datestrings(tickvalues, formatter)
axis_dict.pop("tick0", None)
axis_dict.pop("dtick", None)
axis_dict.pop("tickmode", None)
axis_dict["range"] = mpl_dates_to_datestrings(props["xlim"], formatter)
axis_dict["range"] = mpl_dates_to_datestrings(props["xlim"], formatter)

if formatter == "LogFormatterMathtext":
axis_dict["exponentformat"] = "e"
elif (
formatter in ("FuncFormatter", "FixedFormatter")
and props["axes"][index]["tickformat"] is not None
):
axis_dict.pop("dtick", None)
axis_dict.pop("tickmode", None)
axis_dict["ticktext"] = props["axes"][index]["tickformat"]
axis_dict["tickvals"] = props["axes"][index]["tickvalues"]
return axis_dict


Expand Down
107 changes: 107 additions & 0 deletions plotly/matplotlylib/tests/test_renderer.py
Comment thread
robertoffmoura marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,110 @@ def test_semitransparent_axes_background_preserved():
plotly_fig = tls.mpl_to_plotly(fig)

assert plotly_fig.layout.plot_bgcolor == "rgba(26, 51, 76, 0.4)"


def test_non_arithmetic_progression_xtickvals():
Comment thread
robertoffmoura marked this conversation as resolved.
xticks = [0.01, 0.53, 0.75]
fig, ax = plt.subplots()
ax.plot([0, 1], [0, 1])
ax.set_xticks(xticks)

plotly_fig = tls.mpl_to_plotly(fig)

assert plotly_fig.layout.xaxis.tickvals == tuple(xticks)


def test_non_arithmetic_progression_yticks():
yticks = [0.01, 0.53, 0.75]
fig, ax = plt.subplots()
ax.plot([0, 1], [0, 1])
ax.set_yticks(yticks)

plotly_fig = tls.mpl_to_plotly(fig)

assert plotly_fig.layout.yaxis.tickvals == tuple(yticks)


def test_non_arithmetic_progression_xticktext():
xtickvals = [0.01, 0.53, 0.75]
xticktext = ["Baseline", "param = 1", "param = 2"]
fig, ax = plt.subplots()
ax.plot([0, 1], [0, 1])
ax.set_xticks(xtickvals, xticktext)

plotly_fig = tls.mpl_to_plotly(fig)

assert plotly_fig.layout.xaxis.tickvals == tuple(xtickvals)
assert plotly_fig.layout.xaxis.ticktext == tuple(xticktext)


def test_fixed_formatter_ticktext():
import matplotlib.ticker as ticker

fig, ax = plt.subplots()
ax.plot([0, 1], [0, 1])
ax.xaxis.set_major_locator(ticker.FixedLocator([0.01, 0.53, 0.75]))
ax.xaxis.set_major_formatter(
ticker.FixedFormatter(["Baseline", "param = 1", "param = 2"])
)

plotly_fig = tls.mpl_to_plotly(fig)

assert plotly_fig.layout.xaxis.tickvals == (0.01, 0.53, 0.75)
assert plotly_fig.layout.xaxis.ticktext == ("Baseline", "param = 1", "param = 2")


def test_custom_date_xtickvals_are_converted():
"""Custom tick values on a date axis must be converted to date strings,
not left as raw matplotlib date numbers or datetime objects."""
dates = [datetime.datetime(2023, 1, i) for i in range(1, 11)]
fig, ax = plt.subplots()
ax.plot(dates, np.random.rand(10))
ax.set_xticks(dates[::3])

plotly_fig = tls.mpl_to_plotly(fig)

assert plotly_fig.layout.xaxis.tickvals == (
"2023-01-01 00:00:00",
"2023-01-04 00:00:00",
"2023-01-07 00:00:00",
"2023-01-10 00:00:00",
)


def test_uneven_custom_date_xtickvals_are_converted():
"""Unevenly spaced custom date ticks must be converted to date strings."""
dates = [datetime.datetime(2023, 1, i) for i in range(1, 11)]
ticks = [datetime.datetime(2023, 1, i) for i in [1, 3, 6, 10]]
fig, ax = plt.subplots()
ax.plot(dates, np.random.rand(10))
ax.set_xticks(ticks)

plotly_fig = tls.mpl_to_plotly(fig)

assert plotly_fig.layout.xaxis.tickvals == (
"2023-01-01 00:00:00",
"2023-01-03 00:00:00",
"2023-01-06 00:00:00",
"2023-01-10 00:00:00",
)


def test_custom_date_xtickvals_given_as_numbers_are_converted():
"""Custom date ticks given as matplotlib date numbers must be converted
to date strings."""
import matplotlib.dates as mdates

dates = [datetime.datetime(2023, 1, i) for i in range(1, 11)]
fig, ax = plt.subplots()
ax.plot(dates, np.random.rand(10))
ax.set_xticks([mdates.date2num(d) for d in dates[::3]])

plotly_fig = tls.mpl_to_plotly(fig)

assert plotly_fig.layout.xaxis.tickvals == (
"2023-01-01 00:00:00",
"2023-01-04 00:00:00",
"2023-01-07 00:00:00",
"2023-01-10 00:00:00",
)