Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Fix Plotly Express mutating lists passed to the `x` or `y` arguments in wide mode [[#5727](https://github.com/plotly/plotly.py/pull/5727)], with thanks to @cpruijsen for the contribution!
- Fix `px.sunburst`, `px.treemap` and `px.icicle` listing sectors in a different order on every run when `path` is used with a Polars DataFrame; sectors now follow their order of first appearance for all dataframe backends [[#5766](https://github.com/plotly/plotly.py/pull/5766)], with thanks to @Irahan2 for the contribution!
- Fix `mpl_to_plotly` conversion of matplotlib contour lines and line collections: close contour rings ending with `Z` codes, support dash styles, convert date x-axes, separate disjoint subpaths with `None` separators, hide line collection traces from the legend by default, and group consecutive same-style lines into single traces [[#5770](https://github.com/plotly/plotly.py/pull/5770)], with thanks to @robertoffmoura for the contribution!
- Fix Plotly Express misaligning per-point `opacity` values with their markers when the data is split into several traces (e.g. by `color`, `symbol` or facets); each trace now gets the opacities of its own points, and `opacity` also accepts a column name [[#5825](https://github.com/plotly/plotly.py/pull/5825)], with thanks to @shivenaggarwal for the contribution!

## [7.1.0] - 2026-09-15

Expand Down
17 changes: 16 additions & 1 deletion plotly/express/_core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import plotly.graph_objs as go
import plotly.io as pio
from collections import namedtuple, OrderedDict
from numbers import Number
from ._special_inputs import IdentityMap, Constant, Range
from .trendline_functions import ols, lowess, rolling, expanding, ewm

Expand Down Expand Up @@ -44,7 +45,11 @@
"pattern_shape", # renamed to marker.pattern.shape in infer_config
]
all_attrables = (
direct_attrables + array_attrables + group_attrables + renameable_group_attrables
direct_attrables
+ array_attrables
+ group_attrables
+ renameable_group_attrables
+ ["opacity"] # only refers to data when it isn't a number, see infer_config
)

cartesians = [go.Scatter, go.Scattergl, go.Bar, go.Funnel, go.Box, go.Violin]
Expand Down Expand Up @@ -352,6 +357,10 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref):
trace_patch["marker"]["sizemode"] = "area"
trace_patch["marker"]["sizeref"] = sizeref
mapping_labels[attr_label] = "%{marker.size}"
elif attr_name == "opacity":
if "marker" not in trace_patch:
trace_patch["marker"] = dict()
trace_patch["marker"]["opacity"] = trace_data.get_column(attr_value)
elif attr_name == "marginal_x":
if trace_spec.constructor == go.Histogram and args.get("z") is None:
mapping_labels["count"] = "%{y}"
Expand Down Expand Up @@ -1313,6 +1322,8 @@ def process_args_into_dataframe(
length = len(df_output[next(iter(df_output))]) if len(df_output) else 0
if argument is None:
continue
if field_name == "opacity" and isinstance(argument, Number):
continue # a number is one opacity for all marks, not a column
col_name = None
# Case of multiindex
if is_pd_like and isinstance(argument, native_namespace.MultiIndex):
Expand Down Expand Up @@ -2373,6 +2384,10 @@ def infer_config(args, constructor, trace_patch, layout_patch):
go.Funnelarea,
]:
trace_patch["opacity"] = args["opacity"]
elif isinstance(args["opacity"], str):
# values from a column are split along with the rest of the data,
# so that each trace gets the opacities of its own marks
attrs.append("opacity")
else:
trace_patch["marker"] = dict(opacity=args["opacity"])
if (
Expand Down
55 changes: 55 additions & 0 deletions tests/test_optional/test_px/test_px.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,61 @@ def test_marginal_ranges(backend):
assert fig.layout.yaxis3.range is None


@pytest.mark.parametrize(
"fn,kwargs",
[
(px.scatter, dict(x="x", y="y")),
(px.bar, dict(x="x", y="y")),
(px.scatter_matrix, dict(dimensions=["x", "y"])),
],
)
def test_opacity_values_split_by_color(constructor, fn, kwargs):
# https://github.com/plotly/plotly.py/issues/4580
data = dict(
x=[1, 2, 3, 4, 5, 6],
y=[2, 3, 1, 5, 4, 6],
color=["a", "b", "a", "b", "a", "b"],
)
opacity = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
fig = fn(constructor(data), color="color", opacity=opacity, **kwargs)
assert len(fig.data) == 2
for trace in fig.data:
expected = [o for o, c in zip(opacity, data["color"]) if c == trace.name]
assert list(trace.marker.opacity) == expected


def test_opacity_column_split_by_facets(constructor):
data = dict(
x=[1, 2, 3, 4, 5, 6],
y=[2, 3, 1, 5, 4, 6],
row=["p", "p", "q", "q", "p", "q"],
col=["u", "v", "v", "u", "u", "v"],
alpha=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6],
)
fig = px.scatter(
constructor(data),
x="x",
y="y",
facet_row="row",
facet_col="col",
opacity="alpha",
)
assert len(fig.data) == 4
alpha_by_x = dict(zip(data["x"], data["alpha"]))
for trace in fig.data:
assert list(trace.marker.opacity) == [alpha_by_x[x] for x in trace.x]


@pytest.mark.parametrize("opacity", [0.5, 1, np.float64(0.25)])
def test_opacity_scalar(opacity):
# a number applies to every trace, even if a column has the same name
df = pd.DataFrame({0: [1, 2, 3], 1: [3, 1, 2], "color": ["a", "b", "a"]})
fig = px.scatter(df, x=0, y=1, color="color", opacity=opacity)
assert len(fig.data) == 2
for trace in fig.data:
assert trace.marker.opacity == opacity


def test_render_mode(backend):
df = nw.from_native(px.data.gapminder(return_type=backend))
df2007 = df.filter(nw.col("year") == 2007)
Expand Down