Skip to content

Commit 2cd2a1c

Browse files
lucasjamarclaude
andcommitted
Narrow marginal_x/marginal_y="heatmap" to density_heatmap only
density_contour's colorbar (discrete, labeled at contour boundaries) isn't compatible with the continuous colorbar used by the marginal heatmap strip, producing misleading visuals. Per maintainer feedback on #5707, drop density_contour from the feature's scope and keep it for density_heatmap, which has no such conflict. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 4394c32 commit 2cd2a1c

5 files changed

Lines changed: 28 additions & 56 deletions

File tree

‎CHANGELOG.md‎

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

77
### Added
8-
- Support `marginal_x`/`marginal_y="heatmap"` in `density_heatmap`/`density_contour`, drawing a single-row/column heatmap strip in the margin colored by the same `z`/`histfunc` aggregate as the main plot; `text_auto` now also applies to the marginal heatmap strip [[#5706](https://github.com/plotly/plotly.py/issues/5706)]
8+
- 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; `text_auto` now also applies to the marginal heatmap strip [[#5706](https://github.com/plotly/plotly.py/issues/5706)]
99

1010
### Fixed
1111
- Fix `hex_to_rgb` parsing of 3-digit shorthand hexadecimal colors such as `#FFF` [[#5662](https://github.com/plotly/plotly.py/pull/5662)], with thanks to @genrichez for the contribution!

‎doc/python/marginal-plots.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Marginal distribution plots are small subplots above or to the right of a main p
3939

4040
### Scatter Plot Marginals
4141

42-
The `marginal_x` and `marginal_y` arguments accept one of `"histogram"`, `"rug"`, `"box"`, or `"violin"` (see also how to create [histograms](/python/histograms/), [box plots](/python/box-plots/) and [violin plots](/python/violin-plots/) as the main figure), plus `"heatmap"` for `density_heatmap` and `density_contour` (see below).
42+
The `marginal_x` and `marginal_y` arguments accept one of `"histogram"`, `"rug"`, `"box"`, or `"violin"` (see also how to create [histograms](/python/histograms/), [box plots](/python/box-plots/) and [violin plots](/python/violin-plots/) as the main figure), plus `"heatmap"` for `density_heatmap` (see below).
4343

4444
Marginal plots are linked to the main plot: try zooming or panning on the main plot.
4545

@@ -59,9 +59,9 @@ fig = px.density_heatmap(df, x="sepal_length", y="sepal_width", marginal_x="box"
5959
fig.show()
6060
```
6161

62-
### Marginal Heatmaps on Density Heatmaps and Contours
62+
### Marginal Heatmaps on Density Heatmaps
6363

64-
`marginal_x` and `marginal_y` also accept `"heatmap"` for [`density_heatmap`](/python/2D-Histogram/) and [`density_contour`](/python/2d-histogram-contour/). This draws a single-row or single-column heatmap strip, colored by the same aggregate (`histfunc` of `z`, or count by default) as the main plot, and sharing its color scale.
64+
`marginal_x` and `marginal_y` also accept `"heatmap"` for [`density_heatmap`](/python/2D-Histogram/). This draws a single-row or single-column heatmap strip, colored by the same aggregate (`histfunc` of `z`, or count by default) as the main plot, and sharing its color scale. This is not supported for `density_contour`, since a contour plot's colorbar is discrete and not compatible with the continuous colorbar used by the marginal heatmap.
6565

6666
```python
6767
import plotly.express as px

‎plotly/express/_chart_types.py‎

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -142,17 +142,6 @@ def density_contour(
142142
"For `density_heatmap` and `density_contour` these values are used as the inputs to `histfunc`.",
143143
],
144144
histfunc=["The arguments to this function are the values of `z`."],
145-
marginal_x=[
146-
"Also supports `'heatmap'`, showing a single-row heatmap colored by the aggregate value. "
147-
"Uses a default colorscale since `density_contour` has no `color_continuous_scale` argument.",
148-
],
149-
marginal_y=[
150-
"Also supports `'heatmap'`, showing a single-column heatmap colored by the aggregate value. "
151-
"Uses a default colorscale since `density_contour` has no `color_continuous_scale` argument.",
152-
],
153-
text_auto=[
154-
"Also applies to `marginal_x`/`marginal_y='heatmap'`, in which case the z values are always displayed.",
155-
],
156145
),
157146
)
158147

‎plotly/express/_core.py‎

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -972,10 +972,10 @@ def make_trace_spec(args, constructor, attrs, trace_patch):
972972
marginal=letter,
973973
)
974974
elif args["marginal_" + letter] == "heatmap":
975-
if constructor not in [go.Histogram2d, go.Histogram2dContour]:
975+
if constructor != go.Histogram2d:
976976
raise ValueError(
977977
"`marginal_x`/`marginal_y` value `'heatmap'` is only supported "
978-
"for `density_heatmap` and `density_contour`."
978+
"for `density_heatmap`."
979979
)
980980
other_letter = "y" if letter == "x" else "x"
981981
heatmap_trace_patch = dict(
@@ -2380,10 +2380,6 @@ def infer_config(args, constructor, trace_patch, layout_patch):
23802380
if constructor in [go.Histogram2d, go.Densitymap, go.Densitymapbox]:
23812381
show_colorbar = True
23822382
trace_patch["coloraxis"] = "coloraxis1"
2383-
elif constructor == go.Histogram2dContour and (
2384-
args.get("marginal_x") == "heatmap" or args.get("marginal_y") == "heatmap"
2385-
):
2386-
show_colorbar = True
23872383

23882384
if "opacity" in args:
23892385
if args["opacity"] is None:
@@ -2677,10 +2673,6 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None):
26772673
trace_spec.constructor in [go.Histogram]
26782674
and m.variable in ["symbol", "dash"]
26792675
)
2680-
or (
2681-
trace_spec.constructor == go.Histogram2d
2682-
and m.variable in ["symbol", "pattern", "dash", "color"]
2683-
)
26842676
):
26852677
pass
26862678
elif (
@@ -2778,18 +2770,17 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None):
27782770
if show_colorbar:
27792771
colorvar = (
27802772
"z"
2781-
if constructor
2782-
in [go.Histogram2d, go.Histogram2dContour, go.Densitymap, go.Densitymapbox]
2773+
if constructor in [go.Histogram2d, go.Densitymap, go.Densitymapbox]
27832774
else "color"
27842775
)
2785-
range_color = args.get("range_color") or [None, None]
2776+
range_color = args["range_color"] or [None, None]
27862777

27872778
colorscale_validator = ColorscaleValidator("colorscale", "make_figure")
27882779
coloraxis_dict = dict(
27892780
colorscale=colorscale_validator.validate_coerce(
2790-
args.get("color_continuous_scale")
2781+
args["color_continuous_scale"]
27912782
),
2792-
cmid=args.get("color_continuous_midpoint"),
2783+
cmid=args["color_continuous_midpoint"],
27932784
cmin=range_color[0],
27942785
cmax=range_color[1],
27952786
colorbar=dict(

‎tests/test_optional/test_px/test_marginals.py‎

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,13 @@ def test_single_marginals(backend, px_fn, marginal, orientation):
2626
assert len(fig.data) == 1 + (marginal is not None)
2727

2828

29-
@pytest.mark.parametrize("px_fn", [px.density_heatmap, px.density_contour])
30-
def test_marginal_heatmap_uses_z_and_histfunc(backend, px_fn):
29+
def test_marginal_heatmap_uses_z_and_histfunc(backend):
3130
df = px.data.tips(return_type=backend)
3231
# backend-independent reference for min/max, since e.g. pyarrow columns don't
3332
# support .min()/.max() directly
3433
pdf = px.data.tips()
3534

36-
fig = px_fn(
35+
fig = px.density_heatmap(
3736
df,
3837
x="total_bill",
3938
y="tip",
@@ -66,11 +65,12 @@ def test_marginal_heatmap_uses_z_and_histfunc(backend, px_fn):
6665
assert fig.layout.coloraxis.colorbar.title.text == "sum of size"
6766

6867

69-
@pytest.mark.parametrize("px_fn", [px.density_heatmap, px.density_contour])
70-
def test_marginal_heatmap_without_z(backend, px_fn):
68+
def test_marginal_heatmap_without_z(backend):
7169
df = px.data.tips(return_type=backend)
7270

73-
fig = px_fn(df, x="total_bill", y="tip", marginal_x="heatmap", marginal_y="heatmap")
71+
fig = px.density_heatmap(
72+
df, x="total_bill", y="tip", marginal_x="heatmap", marginal_y="heatmap"
73+
)
7474
marginal_x_trace, marginal_y_trace = fig.data[1], fig.data[2]
7575

7676
assert marginal_x_trace.type == "histogram2d"
@@ -84,12 +84,11 @@ def test_marginal_heatmap_without_z(backend, px_fn):
8484
assert fig.layout.coloraxis.colorbar.title.text == "count"
8585

8686

87-
@pytest.mark.parametrize("px_fn", [px.density_heatmap, px.density_contour])
8887
@pytest.mark.parametrize("text_auto", [True, ".1f"])
89-
def test_marginal_heatmap_text_auto(backend, px_fn, text_auto):
88+
def test_marginal_heatmap_text_auto(backend, text_auto):
9089
df = px.data.tips(return_type=backend)
9190

92-
fig = px_fn(
91+
fig = px.density_heatmap(
9392
df,
9493
x="total_bill",
9594
y="tip",
@@ -102,11 +101,12 @@ def test_marginal_heatmap_text_auto(backend, px_fn, text_auto):
102101
assert trace.texttemplate == expected
103102

104103

105-
@pytest.mark.parametrize("px_fn", [px.density_heatmap, px.density_contour])
106-
def test_marginal_heatmap_no_text_auto(backend, px_fn):
104+
def test_marginal_heatmap_no_text_auto(backend):
107105
df = px.data.tips(return_type=backend)
108106

109-
fig = px_fn(df, x="total_bill", y="tip", marginal_x="heatmap", marginal_y="heatmap")
107+
fig = px.density_heatmap(
108+
df, x="total_bill", y="tip", marginal_x="heatmap", marginal_y="heatmap"
109+
)
110110
for trace in fig.data:
111111
assert trace.texttemplate is None
112112

@@ -118,20 +118,12 @@ def test_marginal_heatmap_unsupported_chart_type_raises():
118118
px.scatter(x=[1, 2, 3], y=[2, 3, 4], marginal_y="heatmap")
119119
with pytest.raises(ValueError, match="only supported for `density_heatmap`"):
120120
px.histogram(x=[1, 2, 3], marginal="heatmap")
121-
122-
123-
def test_marginal_heatmap_with_discrete_color(backend): # density_contour + line.color
124-
df = px.data.tips(return_type=backend)
125-
fig = px.density_contour(
126-
df, x="total_bill", y="tip", color="sex", marginal_x="heatmap"
127-
)
128-
assert len(fig.data) == 4
129-
assert [t.type for t in fig.data] == [
130-
"histogram2dcontour",
131-
"histogram2d",
132-
"histogram2dcontour",
133-
"histogram2d",
134-
]
121+
# density_contour's discrete-looking colorbar isn't compatible with the
122+
# continuous marginal heatmap colorbar, so it's intentionally unsupported
123+
with pytest.raises(ValueError, match="only supported for `density_heatmap`"):
124+
px.density_contour(x=[1, 2, 3], y=[2, 3, 4], marginal_x="heatmap")
125+
with pytest.raises(ValueError, match="only supported for `density_heatmap`"):
126+
px.density_contour(x=[1, 2, 3], y=[2, 3, 4], marginal_y="heatmap")
135127

136128

137129
def test_unsupported_marginal_raises_clear_error(): # issue 4654

0 commit comments

Comments
 (0)