Skip to content

Commit 45ddf75

Browse files
Merge pull request #3014 from plotly/pre_distfuncs
Extra tests, bugfix and finish histnorm labelling
2 parents 234ec0d + 0501de6 commit 45ddf75

File tree

4 files changed

+181
-23
lines changed

4 files changed

+181
-23
lines changed

packages/python/plotly/plotly/express/_core.py

+29-11
Original file line numberDiff line numberDiff line change
@@ -147,19 +147,36 @@ def _is_continuous(df, col_name):
147147

148148

149149
def get_decorated_label(args, column, role):
150-
label = get_label(args, column)
150+
original_label = label = get_label(args, column)
151151
if "histfunc" in args and (
152152
(role == "z")
153153
or (role == "x" and "orientation" in args and args["orientation"] == "h")
154154
or (role == "y" and "orientation" in args and args["orientation"] == "v")
155155
):
156-
if label:
157-
label = "%s of %s" % (args["histfunc"] or "count", label)
156+
histfunc = args["histfunc"] or "count"
157+
if histfunc != "count":
158+
label = "%s of %s" % (histfunc, label)
158159
else:
159160
label = "count"
160161

161162
if "histnorm" in args and args["histnorm"] is not None:
162-
label = "%s of %s" % (args["histnorm"], label)
163+
if label == "count":
164+
label = args["histnorm"]
165+
else:
166+
histnorm = args["histnorm"]
167+
if histfunc == "sum":
168+
if histnorm == "probability":
169+
label = "%s of %s" % ("fraction", label)
170+
elif histnorm == "percent":
171+
label = "%s of %s" % (histnorm, label)
172+
else:
173+
label = "%s weighted by %s" % (histnorm, original_label)
174+
elif histnorm == "probability":
175+
label = "%s of sum of %s" % ("fraction", label)
176+
elif histnorm == "percent":
177+
label = "%s of sum of %s" % ("percent", label)
178+
else:
179+
label = "%s of %s" % (histnorm, label)
163180

164181
if "barnorm" in args and args["barnorm"] is not None:
165182
label = "%s (normalized as %s)" % (label, args["barnorm"])
@@ -924,13 +941,6 @@ def apply_default_cascade(args):
924941
"longdashdot",
925942
]
926943

927-
# If both marginals and faceting are specified, faceting wins
928-
if args.get("facet_col", None) is not None and args.get("marginal_y", None):
929-
args["marginal_y"] = None
930-
931-
if args.get("facet_row", None) is not None and args.get("marginal_x", None):
932-
args["marginal_x"] = None
933-
934944

935945
def _check_name_not_reserved(field_name, reserved_names):
936946
if field_name not in reserved_names:
@@ -1765,6 +1775,14 @@ def infer_config(args, constructor, trace_patch, layout_patch):
17651775
args[position] = args["marginal"]
17661776
args[other_position] = None
17671777

1778+
# If both marginals and faceting are specified, faceting wins
1779+
if args.get("facet_col", None) is not None and args.get("marginal_y", None):
1780+
args["marginal_y"] = None
1781+
1782+
if args.get("facet_row", None) is not None and args.get("marginal_x", None):
1783+
args["marginal_x"] = None
1784+
1785+
# facet_col_wrap only works if no marginals or row faceting is used
17681786
if (
17691787
args.get("marginal_x", None) is not None
17701788
or args.get("marginal_y", None) is not None

packages/python/plotly/plotly/tests/test_core/test_px/test_facets.py

+47
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,53 @@ def test_facets():
4747
assert fig.layout.yaxis4.domain[0] - fig.layout.yaxis.domain[1] == approx(0.08)
4848

4949

50+
def test_facets_with_marginals():
51+
df = px.data.tips()
52+
53+
fig = px.histogram(df, x="total_bill", facet_col="sex", marginal="rug")
54+
assert len(fig.data) == 4
55+
fig = px.histogram(df, x="total_bill", facet_row="sex", marginal="rug")
56+
assert len(fig.data) == 2
57+
58+
fig = px.histogram(df, y="total_bill", facet_col="sex", marginal="rug")
59+
assert len(fig.data) == 2
60+
fig = px.histogram(df, y="total_bill", facet_row="sex", marginal="rug")
61+
assert len(fig.data) == 4
62+
63+
fig = px.scatter(df, x="total_bill", y="tip", facet_col="sex", marginal_x="rug")
64+
assert len(fig.data) == 4
65+
fig = px.scatter(
66+
df, x="total_bill", y="tip", facet_col="day", facet_col_wrap=2, marginal_x="rug"
67+
)
68+
assert len(fig.data) == 8 # ignore the wrap when marginal is used
69+
fig = px.scatter(df, x="total_bill", y="tip", facet_col="sex", marginal_y="rug")
70+
assert len(fig.data) == 2 # ignore the marginal in the facet direction
71+
72+
fig = px.scatter(df, x="total_bill", y="tip", facet_row="sex", marginal_x="rug")
73+
assert len(fig.data) == 2 # ignore the marginal in the facet direction
74+
fig = px.scatter(df, x="total_bill", y="tip", facet_row="sex", marginal_y="rug")
75+
assert len(fig.data) == 4
76+
77+
fig = px.scatter(
78+
df, x="total_bill", y="tip", facet_row="sex", marginal_y="rug", marginal_x="rug"
79+
)
80+
assert len(fig.data) == 4 # ignore the marginal in the facet direction
81+
fig = px.scatter(
82+
df, x="total_bill", y="tip", facet_col="sex", marginal_y="rug", marginal_x="rug"
83+
)
84+
assert len(fig.data) == 4 # ignore the marginal in the facet direction
85+
fig = px.scatter(
86+
df,
87+
x="total_bill",
88+
y="tip",
89+
facet_row="sex",
90+
facet_col="sex",
91+
marginal_y="rug",
92+
marginal_x="rug",
93+
)
94+
assert len(fig.data) == 2 # ignore all marginals
95+
96+
5097
@pytest.fixture
5198
def bad_facet_spacing_df():
5299
NROWS = 101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import plotly.express as px
2+
import pytest
3+
4+
5+
@pytest.mark.parametrize("px_fn", [px.scatter, px.density_heatmap, px.density_contour])
6+
@pytest.mark.parametrize("marginal_x", [None, "histogram", "box", "violin"])
7+
@pytest.mark.parametrize("marginal_y", [None, "rug"])
8+
def test_xy_marginals(px_fn, marginal_x, marginal_y):
9+
df = px.data.tips()
10+
11+
fig = px_fn(
12+
df, x="total_bill", y="tip", marginal_x=marginal_x, marginal_y=marginal_y
13+
)
14+
assert len(fig.data) == 1 + (marginal_x is not None) + (marginal_y is not None)
15+
16+
17+
@pytest.mark.parametrize("px_fn", [px.histogram])
18+
@pytest.mark.parametrize("marginal", [None, "rug", "histogram", "box", "violin"])
19+
@pytest.mark.parametrize("orientation", ["h", "v"])
20+
def test_single_marginals(px_fn, marginal, orientation):
21+
df = px.data.tips()
22+
23+
fig = px_fn(
24+
df, x="total_bill", y="total_bill", marginal=marginal, orientation=orientation
25+
)
26+
assert len(fig.data) == 1 + (marginal is not None)

packages/python/plotly/plotly/tests/test_core/test_px/test_px_functions.py

+79-12
Original file line numberDiff line numberDiff line change
@@ -379,18 +379,73 @@ def test_parcats_dimensions_max():
379379
assert [d.label for d in fig.data[0].dimensions] == ["sex", "smoker", "day", "size"]
380380

381381

382-
def test_histfunc_hoverlabels():
382+
@pytest.mark.parametrize("histfunc,y", [(None, None), ("count", "tip")])
383+
def test_histfunc_hoverlabels_univariate(histfunc, y):
384+
def check_label(label, fig):
385+
assert fig.layout.yaxis.title.text == label
386+
assert label + "=" in fig.data[0].hovertemplate
387+
383388
df = px.data.tips()
384-
fig = px.histogram(df, x="total_bill")
385-
label = "count"
386-
assert fig.layout.yaxis.title.text == label
387-
assert label + "=" in fig.data[0].hovertemplate
388389

389-
fig = px.histogram(df, x="total_bill", y="tip")
390-
label = "sum of tip"
391-
assert fig.layout.yaxis.title.text == label
392-
assert label + "=" in fig.data[0].hovertemplate
390+
# base case, just "count" (note count(tip) is same as count())
391+
fig = px.histogram(df, x="total_bill", y=y, histfunc=histfunc)
392+
check_label("count", fig)
393+
394+
# without y, label is just histnorm
395+
for histnorm in ["probability", "percent", "density", "probability density"]:
396+
fig = px.histogram(
397+
df, x="total_bill", y=y, histfunc=histfunc, histnorm=histnorm
398+
)
399+
check_label(histnorm, fig)
400+
401+
for histnorm in ["probability", "percent", "density", "probability density"]:
402+
for barnorm in ["percent", "fraction"]:
403+
fig = px.histogram(
404+
df,
405+
x="total_bill",
406+
y=y,
407+
histfunc=histfunc,
408+
histnorm=histnorm,
409+
barnorm=barnorm,
410+
)
411+
check_label("%s (normalized as %s)" % (histnorm, barnorm), fig)
412+
413+
414+
def test_histfunc_hoverlabels_bivariate():
415+
def check_label(label, fig):
416+
assert fig.layout.yaxis.title.text == label
417+
assert label + "=" in fig.data[0].hovertemplate
393418

419+
df = px.data.tips()
420+
421+
# with y, should be same as forcing histfunc to sum
422+
fig = px.histogram(df, x="total_bill", y="tip")
423+
check_label("sum of tip", fig)
424+
425+
# change probability to fraction when histfunc is sum
426+
fig = px.histogram(df, x="total_bill", y="tip", histnorm="probability")
427+
check_label("fraction of sum of tip", fig)
428+
429+
# percent is percent
430+
fig = px.histogram(df, x="total_bill", y="tip", histnorm="percent")
431+
check_label("percent of sum of tip", fig)
432+
433+
# the other two are "weighted by"
434+
for histnorm in ["density", "probability density"]:
435+
fig = px.histogram(df, x="total_bill", y="tip", histnorm=histnorm)
436+
check_label("%s weighted by tip" % histnorm, fig)
437+
438+
# check a few "normalized by"
439+
for histnorm in ["density", "probability density"]:
440+
for barnorm in ["fraction", "percent"]:
441+
fig = px.histogram(
442+
df, x="total_bill", y="tip", histnorm=histnorm, barnorm=barnorm
443+
)
444+
check_label(
445+
"%s weighted by tip (normalized as %s)" % (histnorm, barnorm), fig
446+
)
447+
448+
# these next two are weird but OK...
394449
fig = px.histogram(
395450
df,
396451
x="total_bill",
@@ -399,9 +454,21 @@ def test_histfunc_hoverlabels():
399454
histnorm="probability",
400455
barnorm="percent",
401456
)
402-
label = "probability of min of tip (normalized as percent)"
403-
assert fig.layout.yaxis.title.text == label
404-
assert label + "=" in fig.data[0].hovertemplate
457+
check_label("fraction of sum of min of tip (normalized as percent)", fig)
458+
459+
fig = px.histogram(
460+
df,
461+
x="total_bill",
462+
y="tip",
463+
histfunc="avg",
464+
histnorm="percent",
465+
barnorm="fraction",
466+
)
467+
check_label("percent of sum of avg of tip (normalized as fraction)", fig)
468+
469+
# this next one is basically "never do this" but needs a defined behaviour
470+
fig = px.histogram(df, x="total_bill", y="tip", histfunc="max", histnorm="density")
471+
check_label("density of max of tip", fig)
405472

406473

407474
def test_timeline():

0 commit comments

Comments
 (0)