Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Support trace-specific color sequences in Plotly Express via templates
- Modify apply_default_cascade to check template.data.<trace_type> for marker.color or line.color
- Fallback to template.layout.colorway if trace-specific colors not found
- Add comprehensive tests for trace-specific color sequences
- Handle timeline special case (maps to bar trace type)
- Follow existing patterns for symbol_sequence and line_dash_sequence

Fixes #5416
  • Loading branch information
antonymilne committed Nov 26, 2025
commit 83faec8d91591c768799dce0b9083b74fff88369
18 changes: 13 additions & 5 deletions plotly/express/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1037,28 +1037,36 @@ def apply_default_cascade(args, constructor=None):
if args["color_continuous_scale"] is None:
args["color_continuous_scale"] = sequential.Viridis

# if color_discrete_sequence not set explicitly or in px.defaults,
# see if we can defer to template. Try trace-specific colors first,
# then layout.colorway, then set reasonable defaults
if "color_discrete_sequence" in args:
if args["color_discrete_sequence"] is None and constructor is not None:
if constructor == "timeline":
trace_type = "bar"
else:
trace_type = constructor().type
if trace_data_list := getattr(args["template"].data, trace_type, None):
collected_colors = [
# try marker.color first
args["color_discrete_sequence"] = [
trace_data.marker.color
for trace_data in trace_data_list
if hasattr(trace_data, "marker")
]
if not collected_colors:
collected_colors = [
# fallback to line.color if marker.color not available
if not args["color_discrete_sequence"] or not any(args["color_discrete_sequence"]):
args["color_discrete_sequence"] = [
trace_data.line.color
for trace_data in trace_data_list
if hasattr(trace_data, "line")
]
if collected_colors:
args["color_discrete_sequence"] = collected_colors
# if no trace-specific colors found, reset to None to allow fallback
if not args["color_discrete_sequence"] or not any(args["color_discrete_sequence"]):
args["color_discrete_sequence"] = None
# fallback to layout.colorway if trace-specific colors not available
if args["color_discrete_sequence"] is None and args["template"].layout.colorway:
args["color_discrete_sequence"] = args["template"].layout.colorway
# final fallback to default qualitative palette
if args["color_discrete_sequence"] is None:
args["color_discrete_sequence"] = qualitative.D3

Expand Down
81 changes: 81 additions & 0 deletions tests/test_optional/test_px/test_px.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,87 @@ def test_px_templates(backend):
pio.templates.default = "plotly"


def test_px_templates_trace_specific_colors(backend):
import plotly.graph_objects as go

tips = px.data.tips(return_type=backend)

# read trace-specific colors from template.data.histogram
histogram_template = go.layout.Template()
histogram_template.data.histogram = [
go.Histogram(marker=dict(color="orange")),
go.Histogram(marker=dict(color="purple")),
]
fig = px.histogram(tips, x="total_bill", color="sex", template=histogram_template)
assert fig.data[0].marker.color == "orange"
assert fig.data[1].marker.color == "purple"

# scatter uses template.data.scatter colors, not histogram colors
scatter_template = go.layout.Template()
scatter_template.data.scatter = [
go.Scatter(marker=dict(color="cyan")),
go.Scatter(marker=dict(color="magenta")),
]
scatter_template.data.histogram = [
go.Histogram(marker=dict(color="orange")),
]
fig = px.scatter(tips, x="total_bill", y="tip", color="sex", template=scatter_template)
assert fig.data[0].marker.color == "cyan"
assert fig.data[1].marker.color == "magenta"

# histogram still uses histogram colors even when scatter colors exist
fig = px.histogram(tips, x="total_bill", color="sex", template=scatter_template)
assert fig.data[0].marker.color == "orange"

# fallback to layout.colorway when trace-specific colors don't exist
fig = px.histogram(
tips, x="total_bill", color="sex", template=dict(layout_colorway=["yellow", "green"])
)
assert fig.data[0].marker.color == "yellow"
assert fig.data[1].marker.color == "green"

# timeline special case (maps to bar)
timeline_template = go.layout.Template()
timeline_template.data.bar = [
go.Bar(marker=dict(color="red")),
go.Bar(marker=dict(color="blue")),
]
timeline_data = {
"Task": ["Job A", "Job B"],
"Start": ["2009-01-01", "2009-03-05"],
"Finish": ["2009-02-28", "2009-04-15"],
"Resource": ["Alex", "Max"],
}
# Use same backend as tips for consistency
df_timeline = px.data.tips(return_type=backend)
df_timeline = nw.from_native(df_timeline).with_columns(
nw.lit("Job A").alias("Task"),
nw.lit("2009-01-01").alias("Start"),
nw.lit("2009-02-28").alias("Finish"),
nw.lit("Alex").alias("Resource"),
).head(1).to_native()
# Add second row
df_timeline2 = nw.from_native(df_timeline).with_columns(
nw.lit("Job B").alias("Task"),
nw.lit("2009-03-05").alias("Start"),
nw.lit("2009-04-15").alias("Finish"),
nw.lit("Max").alias("Resource"),
).head(1).to_native()
# Combine - actually, this is getting too complex. Let me just use a simpler approach
import pandas as pd
df_timeline = pd.DataFrame(timeline_data)
fig = px.timeline(
df_timeline,
x_start="Start",
x_end="Finish",
y="Task",
color="Resource",
template=timeline_template,
)
assert fig.data[0].marker.color == "red"
assert fig.data[1].marker.color == "blue"


def test_px_defaults():
px.defaults.labels = dict(x="hey x")
px.defaults.category_orders = dict(color=["b", "a"])
Expand Down