Skip to content

Commit d3105d4

Browse files
authored
Merge pull request #5262 from robertoffmoura/rm/support-non-ap-xticks
Add support for overriding x tick with non arithmetic progression values
2 parents 2abf6fd + c5787d1 commit d3105d4

3 files changed

Lines changed: 128 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
66

77
### Added
88
- 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!
9+
- 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!
910

1011
### Fixed
1112
- 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!

plotly/matplotlylib/mpltools.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -436,18 +436,20 @@ def prep_ticks(ax, index, ax_type, props):
436436
tick0 = tickvalues[0]
437437
dticks = [
438438
round(tickvalues[i] - tickvalues[i - 1], 12)
439-
for i in range(1, len(tickvalues) - 1)
439+
for i in range(1, len(tickvalues))
440440
]
441-
if all([dticks[i] == dticks[i - 1] for i in range(1, len(dticks) - 1)]):
441+
if all([dticks[i] == dticks[i - 1] for i in range(1, len(dticks))]):
442442
dtick = tickvalues[1] - tickvalues[0]
443443
else:
444444
warnings.warn(
445445
"'linear' {0}-axis tick spacing not even, "
446-
"ignoring mpl tick formatting.".format(ax_type)
446+
"exporting explicit tick values instead of dtick.".format(ax_type)
447447
)
448448
raise TypeError
449449
except (IndexError, TypeError):
450450
axis_dict["nticks"] = props["axes"][index]["nticks"]
451+
if props["axes"][index]["tickvalues"] is not None:
452+
axis_dict["tickvals"] = props["axes"][index]["tickvalues"]
451453
else:
452454
axis_dict["tick0"] = tick0
453455
axis_dict["dtick"] = dtick
@@ -485,17 +487,26 @@ def prep_ticks(ax, index, ax_type, props):
485487
formatter = axis.get_major_formatter().__class__.__name__
486488
if ax_type == "x" and "DateFormatter" in formatter:
487489
axis_dict["type"] = "date"
488-
try:
489-
axis_dict["tick0"] = mpl_dates_to_datestrings(axis_dict["tick0"], formatter)
490-
except KeyError:
491-
pass
492-
finally:
490+
tickvalues = props["axes"][index]["tickvalues"]
491+
if tickvalues is not None:
492+
# custom ticks: export the exact locations and drop the
493+
# arithmetic tick0/dtick spec, which plotly would use instead
494+
axis_dict["tickvals"] = mpl_dates_to_datestrings(tickvalues, formatter)
495+
axis_dict.pop("tick0", None)
493496
axis_dict.pop("dtick", None)
494497
axis_dict.pop("tickmode", None)
495-
axis_dict["range"] = mpl_dates_to_datestrings(props["xlim"], formatter)
498+
axis_dict["range"] = mpl_dates_to_datestrings(props["xlim"], formatter)
496499

497500
if formatter == "LogFormatterMathtext":
498501
axis_dict["exponentformat"] = "e"
502+
elif (
503+
formatter in ("FuncFormatter", "FixedFormatter")
504+
and props["axes"][index]["tickformat"] is not None
505+
):
506+
axis_dict.pop("dtick", None)
507+
axis_dict.pop("tickmode", None)
508+
axis_dict["ticktext"] = props["axes"][index]["tickformat"]
509+
axis_dict["tickvals"] = props["axes"][index]["tickvalues"]
499510
return axis_dict
500511

501512

plotly/matplotlylib/tests/test_renderer.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,110 @@ def test_line_color_is_valid_plotly_color():
244244
plotly_fig = tls.mpl_to_plotly(fig)
245245

246246
assert plotly_fig.data[0].line.color == "rgba(255, 0, 0, 1)"
247+
248+
249+
def test_non_arithmetic_progression_xtickvals():
250+
xticks = [0.01, 0.53, 0.75]
251+
fig, ax = plt.subplots()
252+
ax.plot([0, 1], [0, 1])
253+
ax.set_xticks(xticks)
254+
255+
plotly_fig = tls.mpl_to_plotly(fig)
256+
257+
assert plotly_fig.layout.xaxis.tickvals == tuple(xticks)
258+
259+
260+
def test_non_arithmetic_progression_yticks():
261+
yticks = [0.01, 0.53, 0.75]
262+
fig, ax = plt.subplots()
263+
ax.plot([0, 1], [0, 1])
264+
ax.set_yticks(yticks)
265+
266+
plotly_fig = tls.mpl_to_plotly(fig)
267+
268+
assert plotly_fig.layout.yaxis.tickvals == tuple(yticks)
269+
270+
271+
def test_non_arithmetic_progression_xticktext():
272+
xtickvals = [0.01, 0.53, 0.75]
273+
xticktext = ["Baseline", "param = 1", "param = 2"]
274+
fig, ax = plt.subplots()
275+
ax.plot([0, 1], [0, 1])
276+
ax.set_xticks(xtickvals, xticktext)
277+
278+
plotly_fig = tls.mpl_to_plotly(fig)
279+
280+
assert plotly_fig.layout.xaxis.tickvals == tuple(xtickvals)
281+
assert plotly_fig.layout.xaxis.ticktext == tuple(xticktext)
282+
283+
284+
def test_fixed_formatter_ticktext():
285+
import matplotlib.ticker as ticker
286+
287+
fig, ax = plt.subplots()
288+
ax.plot([0, 1], [0, 1])
289+
ax.xaxis.set_major_locator(ticker.FixedLocator([0.01, 0.53, 0.75]))
290+
ax.xaxis.set_major_formatter(
291+
ticker.FixedFormatter(["Baseline", "param = 1", "param = 2"])
292+
)
293+
294+
plotly_fig = tls.mpl_to_plotly(fig)
295+
296+
assert plotly_fig.layout.xaxis.tickvals == (0.01, 0.53, 0.75)
297+
assert plotly_fig.layout.xaxis.ticktext == ("Baseline", "param = 1", "param = 2")
298+
299+
300+
def test_custom_date_xtickvals_are_converted():
301+
"""Custom tick values on a date axis must be converted to date strings,
302+
not left as raw matplotlib date numbers or datetime objects."""
303+
dates = [datetime.datetime(2023, 1, i) for i in range(1, 11)]
304+
fig, ax = plt.subplots()
305+
ax.plot(dates, np.random.rand(10))
306+
ax.set_xticks(dates[::3])
307+
308+
plotly_fig = tls.mpl_to_plotly(fig)
309+
310+
assert plotly_fig.layout.xaxis.tickvals == (
311+
"2023-01-01 00:00:00",
312+
"2023-01-04 00:00:00",
313+
"2023-01-07 00:00:00",
314+
"2023-01-10 00:00:00",
315+
)
316+
317+
318+
def test_uneven_custom_date_xtickvals_are_converted():
319+
"""Unevenly spaced custom date ticks must be converted to date strings."""
320+
dates = [datetime.datetime(2023, 1, i) for i in range(1, 11)]
321+
ticks = [datetime.datetime(2023, 1, i) for i in [1, 3, 6, 10]]
322+
fig, ax = plt.subplots()
323+
ax.plot(dates, np.random.rand(10))
324+
ax.set_xticks(ticks)
325+
326+
plotly_fig = tls.mpl_to_plotly(fig)
327+
328+
assert plotly_fig.layout.xaxis.tickvals == (
329+
"2023-01-01 00:00:00",
330+
"2023-01-03 00:00:00",
331+
"2023-01-06 00:00:00",
332+
"2023-01-10 00:00:00",
333+
)
334+
335+
336+
def test_custom_date_xtickvals_given_as_numbers_are_converted():
337+
"""Custom date ticks given as matplotlib date numbers must be converted
338+
to date strings."""
339+
import matplotlib.dates as mdates
340+
341+
dates = [datetime.datetime(2023, 1, i) for i in range(1, 11)]
342+
fig, ax = plt.subplots()
343+
ax.plot(dates, np.random.rand(10))
344+
ax.set_xticks([mdates.date2num(d) for d in dates[::3]])
345+
346+
plotly_fig = tls.mpl_to_plotly(fig)
347+
348+
assert plotly_fig.layout.xaxis.tickvals == (
349+
"2023-01-01 00:00:00",
350+
"2023-01-04 00:00:00",
351+
"2023-01-07 00:00:00",
352+
"2023-01-10 00:00:00",
353+
)

0 commit comments

Comments
 (0)