Shared legend between subplots of different types

Hey all, I’ve been trying to figure out how to create a subplot that shows both a line plot and a scatter plot of different data, but the same categories. The problem I’m currently encountering is that the legend color doesn’t correspond to the scatter plot colors. I’m trying to figure out how to share the colors and legend names between the subplots.

To share legend, I’m currently using the approach in Legends in Python

If there’s an entirely better approach to doing a line/scatter subplot, please let me know. I couldn’t figure out how to do this with plotly.express

Here’s what I currently have:

speaker_df = DataFrame({
    'samples per speaker': num_samples,
    '# speakers': num_speakers,
    'f1-score': f1_scores,
})
threshold_df = DataFrame({
    'samples per speaker': num_samples,
    '# speakers': num_speakers,
    'f1-score': threshold_scores,
    'threshold': thresholds,
})

f = make_subplots(rows=2, cols=1)

for n_speakers in sorted(set(num_speakers)):
    df = speaker_df[speaker_df['# speakers'] == n_speakers]
    f.add_trace(Scatter(mode='lines', x=df['samples per speaker'], y=df['f1-score'],
                        name=n_speakers, legendgroup=n_speakers),
                row=1, col=1)

    df = threshold_df[threshold_df['# speakers'] == n_speakers]
    f.add_trace(Scatter(mode='markers', opacity=0.9, x=df['threshold'], y=df['f1-score'],
                        name=n_speakers, legendgroup=n_speakers, showlegend=False),
                row=2, col=1)

Hey @xaviersjs, welcome to the forums.

You could assign a color directly to each trace.

Related:

1 Like

Thanks for the suggestion. The way my data is structured, I don’t know how many categories I’ll have, so hard-coding colors feels tedious. Is there a plotly way to construct a discrete color sequence from a built-in color scale?

For example, Is there a way to do something like

speakers = set(num_speakers)
color_sequence = discrete_equence('viridis', len(speakers))

for n_speakers, color in zip(speakers, color_sequence):
    ...

Hi @xaviersjs, yes, that’s possible. Some information here:

Adapted to your needs:

from plotly.express.colors import sample_colorscale
import plotly.graph_objects as go
import numpy as np

DATAPOINTS = 50

#scaling function
def minmax_scale(y):
    return [(val - y.min()) / (y.max() - y.min()) for val in y]

# create data
x = np.ones(DATAPOINTS)
y = np.arange(0, DATAPOINTS)

discrete_viridis24 = sample_colorscale('Viridis', minmax_scale(y))


fig = go.Figure(
    go.Scatter(
        x=x, 
        y=y, 
        mode='markers', 
        marker={'size':10, 'color': discrete_viridis24}, 
        name='discrete viridis'
    )
)

fig.update_layout(
    width=600, 
    height=800
)

1 Like

Thanks @AIMPED. Here’s a snippet with what I ended up with. I slightly simplified the y variable by using np.linspace instead of np.arange. Also, sample_colorscale takes a min, and max argument, so passing y, and max=DATAPOINTS in your example would yield the same result.

Anyways here’s the snippet I ended up with

speakers = tuple(sorted(set(num_speakers)))
colors = sample_colorscale('Viridis', np.linspace(0.05, 0.95, len(speakers)))

for n_speakers, color in zip(speakers, colors):
    marker = dict(color=color)

    df = speaker_df[speaker_df['# speakers'] == n_speakers]
    f.add_trace(Scatter(mode='lines', x=df['samples per speaker'], y=df['f1-score'],
                        name=n_speakers, legendgroup=n_speakers, marker=marker),
                row=1, col=1)

    df = threshold_df[threshold_df['# speakers'] == n_speakers]
    f.add_trace(Scatter(mode='markers+lines', opacity=0.9, x=df['threshold'], y=df['f1-score'],
                        name=n_speakers, legendgroup=n_speakers, marker=marker, showlegend=False),
                row=2, col=1)
1 Like