Skip to content

Commit 2f44517

Browse files
Merge pull request #2897 from plotly/make_subplots-impossible-spacing
Descriptive error when subplot spacing impossible
2 parents 259fe11 + 92d13cb commit 2f44517

File tree

4 files changed

+180
-15
lines changed

4 files changed

+180
-15
lines changed

Diff for: packages/python/plotly/plotly/express/_core.py

+34-15
Original file line numberDiff line numberDiff line change
@@ -2120,22 +2120,41 @@ def init_figure(args, subplot_type, frame_list, nrows, ncols, col_labels, row_la
21202120
for j in range(ncols):
21212121
subplot_labels[i * ncols + j] = col_labels[(nrows - 1 - i) * ncols + j]
21222122

2123+
def _spacing_error_translator(e, direction, facet_arg):
2124+
"""
2125+
Translates the spacing errors thrown by the underlying make_subplots
2126+
routine into one that describes an argument adjustable through px.
2127+
"""
2128+
if ("%s spacing" % (direction,)) in e.args[0]:
2129+
e.args = (
2130+
e.args[0]
2131+
+ """
2132+
Use the {facet_arg} argument to adjust this spacing.""".format(
2133+
facet_arg=facet_arg
2134+
),
2135+
)
2136+
raise e
2137+
21232138
# Create figure with subplots
2124-
fig = make_subplots(
2125-
rows=nrows,
2126-
cols=ncols,
2127-
specs=specs,
2128-
shared_xaxes="all",
2129-
shared_yaxes="all",
2130-
row_titles=[] if facet_col_wrap else list(reversed(row_labels)),
2131-
column_titles=[] if facet_col_wrap else col_labels,
2132-
subplot_titles=subplot_labels if facet_col_wrap else [],
2133-
horizontal_spacing=horizontal_spacing,
2134-
vertical_spacing=vertical_spacing,
2135-
row_heights=row_heights,
2136-
column_widths=column_widths,
2137-
start_cell="bottom-left",
2138-
)
2139+
try:
2140+
fig = make_subplots(
2141+
rows=nrows,
2142+
cols=ncols,
2143+
specs=specs,
2144+
shared_xaxes="all",
2145+
shared_yaxes="all",
2146+
row_titles=[] if facet_col_wrap else list(reversed(row_labels)),
2147+
column_titles=[] if facet_col_wrap else col_labels,
2148+
subplot_titles=subplot_labels if facet_col_wrap else [],
2149+
horizontal_spacing=horizontal_spacing,
2150+
vertical_spacing=vertical_spacing,
2151+
row_heights=row_heights,
2152+
column_widths=column_widths,
2153+
start_cell="bottom-left",
2154+
)
2155+
except ValueError as e:
2156+
_spacing_error_translator(e, "Horizontal", "facet_col_spacing")
2157+
_spacing_error_translator(e, "Vertical", "facet_row_spacing")
21392158

21402159
# Remove explicit font size of row/col titles so template can take over
21412160
for annot in fig.layout.annotations:

Diff for: packages/python/plotly/plotly/subplots.py

+22
Original file line numberDiff line numberDiff line change
@@ -530,19 +530,41 @@ def _checks(item, defaults):
530530
)
531531
)
532532

533+
def _check_hv_spacing(dimsize, spacing, name, dimvarname, dimname):
534+
if spacing < 0 or spacing > 1:
535+
raise ValueError("%s spacing must be between 0 and 1." % (name,))
536+
if dimsize <= 1:
537+
return
538+
max_spacing = 1.0 / float(dimsize - 1)
539+
if spacing > max_spacing:
540+
raise ValueError(
541+
"""{name} spacing cannot be greater than (1 / ({dimvarname} - 1)) = {max_spacing:f}.
542+
The resulting plot would have {dimsize} {dimname} ({dimvarname}={dimsize}).""".format(
543+
dimvarname=dimvarname,
544+
name=name,
545+
dimname=dimname,
546+
max_spacing=max_spacing,
547+
dimsize=dimsize,
548+
)
549+
)
550+
533551
# ### horizontal_spacing ###
534552
if horizontal_spacing is None:
535553
if has_secondary_y:
536554
horizontal_spacing = 0.4 / cols
537555
else:
538556
horizontal_spacing = 0.2 / cols
557+
# check horizontal_spacing can be satisfied:
558+
_check_hv_spacing(cols, horizontal_spacing, "Horizontal", "cols", "columns")
539559

540560
# ### vertical_spacing ###
541561
if vertical_spacing is None:
542562
if subplot_titles is not None:
543563
vertical_spacing = 0.5 / rows
544564
else:
545565
vertical_spacing = 0.3 / rows
566+
# check vertical_spacing can be satisfied:
567+
_check_hv_spacing(rows, vertical_spacing, "Vertical", "rows", "rows")
546568

547569
# ### subplot titles ###
548570
if subplot_titles is None:

Diff for: packages/python/plotly/plotly/tests/test_core/test_px/test_facets.py

+46
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import plotly
2+
import pandas as pd
13
import plotly.express as px
24
from pytest import approx
5+
import pytest
6+
import random
37

48

59
def test_facets():
@@ -41,3 +45,45 @@ def test_facets():
4145
)
4246
assert fig.layout.xaxis4.domain[0] - fig.layout.xaxis.domain[1] == approx(0.09)
4347
assert fig.layout.yaxis4.domain[0] - fig.layout.yaxis.domain[1] == approx(0.08)
48+
49+
50+
@pytest.fixture
51+
def bad_facet_spacing_df():
52+
NROWS = 101
53+
NDATA = 1000
54+
categories = [n % NROWS for n in range(NDATA)]
55+
df = pd.DataFrame(
56+
{
57+
"x": [random.random() for _ in range(NDATA)],
58+
"y": [random.random() for _ in range(NDATA)],
59+
"category": categories,
60+
}
61+
)
62+
return df
63+
64+
65+
def test_bad_facet_spacing_eror(bad_facet_spacing_df):
66+
df = bad_facet_spacing_df
67+
with pytest.raises(
68+
ValueError, match="Use the facet_row_spacing argument to adjust this spacing\."
69+
):
70+
fig = px.scatter(
71+
df, x="x", y="y", facet_row="category", facet_row_spacing=0.01001
72+
)
73+
with pytest.raises(
74+
ValueError, match="Use the facet_col_spacing argument to adjust this spacing\."
75+
):
76+
fig = px.scatter(
77+
df, x="x", y="y", facet_col="category", facet_col_spacing=0.01001
78+
)
79+
# Check error is not raised when the spacing is OK
80+
try:
81+
fig = px.scatter(df, x="x", y="y", facet_row="category", facet_row_spacing=0.01)
82+
except ValueError:
83+
# Error shouldn't be raised, so fail if it is
84+
assert False
85+
try:
86+
fig = px.scatter(df, x="x", y="y", facet_col="category", facet_col_spacing=0.01)
87+
except ValueError:
88+
# Error shouldn't be raised, so fail if it is
89+
assert False

Diff for: packages/python/plotly/plotly/tests/test_core/test_subplots/test_make_subplots.py

+78
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import absolute_import
22

33
from unittest import TestCase
4+
import pytest
45
from plotly.graph_objs import (
56
Annotation,
67
Annotations,
@@ -1934,3 +1935,80 @@ def test_if_passed_figure(self):
19341935
fig2sp = subplots.make_subplots(2, 2)
19351936
assert id(fig2sp) != id(figsp)
19361937
assert fig2sp.layout == figsp.layout
1938+
1939+
1940+
def test_make_subplots_spacing_error():
1941+
# check exception describing maximum value for horizontal_spacing or
1942+
# vertical_spacing is raised when spacing exceeds that value
1943+
for match in [
1944+
(
1945+
"^%s spacing cannot be greater than \(1 / \(%s - 1\)\) = %f."
1946+
% ("Vertical", "rows", 1.0 / 50.0)
1947+
).replace(".", "\."),
1948+
"The resulting plot would have 51 rows \(rows=51\)\.$",
1949+
]:
1950+
with pytest.raises(
1951+
ValueError, match=match,
1952+
):
1953+
fig = subplots.make_subplots(51, 1, vertical_spacing=0.0201)
1954+
for match in [
1955+
(
1956+
"^%s spacing cannot be greater than \(1 / \(%s - 1\)\) = %f."
1957+
% ("Horizontal", "cols", 1.0 / 50.0)
1958+
).replace(".", "\."),
1959+
"The resulting plot would have 51 columns \(cols=51\)\.$",
1960+
]:
1961+
with pytest.raises(
1962+
ValueError, match=match,
1963+
):
1964+
fig = subplots.make_subplots(1, 51, horizontal_spacing=0.0201)
1965+
# Check it's not raised when it's not beyond the maximum
1966+
try:
1967+
fig = subplots.make_subplots(51, 1, vertical_spacing=0.0200)
1968+
except ValueError:
1969+
# This shouldn't happen so we assert False to force failure
1970+
assert False
1971+
try:
1972+
fig = subplots.make_subplots(1, 51, horizontal_spacing=0.0200)
1973+
except ValueError:
1974+
# This shouldn't happen so we assert False to force failure
1975+
assert False
1976+
# make sure any value between 0 and 1 works for horizontal_spacing if cols is 1
1977+
try:
1978+
fig = subplots.make_subplots(1, 1, horizontal_spacing=0)
1979+
except ValueError:
1980+
# This shouldn't happen so we assert False to force failure
1981+
assert False
1982+
try:
1983+
fig = subplots.make_subplots(1, 1, horizontal_spacing=1)
1984+
except ValueError:
1985+
# This shouldn't happen so we assert False to force failure
1986+
assert False
1987+
# make sure any value between 0 and 1 works for horizontal_spacing if cols is 1
1988+
try:
1989+
fig = subplots.make_subplots(1, 1, horizontal_spacing=0)
1990+
except ValueError:
1991+
# This shouldn't happen so we assert False to force failure
1992+
assert False
1993+
# make sure any value between 0 and 1 works for horizontal_spacing if cols is 1
1994+
try:
1995+
fig = subplots.make_subplots(1, 1, horizontal_spacing=1)
1996+
except ValueError:
1997+
# This shouldn't happen so we assert False to force failure
1998+
assert False
1999+
with pytest.raises(
2000+
ValueError, match="^Horizontal spacing must be between 0 and 1\.$"
2001+
):
2002+
fig = subplots.make_subplots(1, 1, horizontal_spacing=-0.01)
2003+
with pytest.raises(
2004+
ValueError, match="^Horizontal spacing must be between 0 and 1\.$"
2005+
):
2006+
fig = subplots.make_subplots(1, 1, horizontal_spacing=1.01)
2007+
with pytest.raises(
2008+
ValueError, match="^Vertical spacing must be between 0 and 1\.$"
2009+
):
2010+
fig = subplots.make_subplots(1, 1, vertical_spacing=-0.01)
2011+
with pytest.raises(
2012+
ValueError, match="^Vertical spacing must be between 0 and 1\.$"
2013+
):
2014+
fig = subplots.make_subplots(1, 1, vertical_spacing=1.01)

0 commit comments

Comments
 (0)