Python Plotly scatter plot with hover labels built from an X and Y column of lists of 5 values to create rectangles

I am trying to create a Python Plotly go.scatter plot with multiple rectangles without a loop. A loop would be fine if my hover labels would plot correctly. I created X and Y columns using the dates and Y values that will trace these rectangles. I would like them to have a hover label with the custom data. I’ve been working on various ways to accomplish this but cant seem to figure it out.

This code below doesn’t work (the rectangles plot on top of each other):

df = pd.DataFrame(columns=['description','date_begin','date_end','y0','y1'])
df.loc[0] = ['red', '2022-09-24 08:22:31', '2022-09-24 20:40:00', 0, 1]
df.loc[1] = ['blue', '2022-10-09 02:54:56', '2022-10-10 19:09:40', 0, 1]
df.loc[2] = ['green', '2022-11-03 06:21:51', '2022-11-05 15:15:48', 0, 1]

df['date_begin'] =  pd.to_datetime(df['date_begin'])
df['date_end'] =  pd.to_datetime(df['date_end'])

df['x'] = df.apply(lambda x: [x['date_begin'], x['date_begin'], x['date_end'], x['date_end'], x['date_begin']], axis=1)
df['y'] = df.apply(lambda x: [x['y1'], x['y0'], x['y0'], x['y1'], x['y1']], axis=1)

fig = go.Figure()

fig.add_traces(go.Scatter(  x=df['x'], 
                            y=df['y'],
                            mode='lines+markers',
                            customdata=df[['description','date_begin','date_end','y0','y1']],
                            hovertemplate="<br>".join(["Description: %{customdata[0]}",
                                                       "Beginning date: %{customdata[1]}",
                                                       "Ending date: %{customdata[2]}",
                                                       '<extra></extra>']),
                            line=dict(color="black",width=1),
                            fillcolor="grey",
                            opacity=0.5,
                            fill="toself",
                )) 

fig.show()

When I just use one row, the rectangle plots as expected but the hover labels aren’t working:

df = pd.DataFrame(columns=['description','date_begin','date_end','y0','y1'])
df.loc[0] = ['red', '2022-09-24 08:22:31', '2022-09-24 20:40:00', 0, 1]
df.loc[1] = ['blue', '2022-10-09 02:54:56', '2022-10-10 19:09:40', 0, 1]
df.loc[2] = ['green', '2022-11-03 06:21:51', '2022-11-05 15:15:48', 0, 1]

df['date_begin'] =  pd.to_datetime(df['date_begin'])
df['date_end'] =  pd.to_datetime(df['date_end'])

df['x'] = df.apply(lambda x: [x['date_begin'], x['date_begin'], x['date_end'], x['date_end'], x['date_begin']], axis=1)
df['y'] = df.apply(lambda x: [x['y1'], x['y0'], x['y0'], x['y1'], x['y1']], axis=1)

fig = go.Figure()
fig.add_traces(go.Scatter(  x=df['x'].iloc[0], 
                            y=df['y'].iloc[0],
                            mode='lines+markers',
                            customdata=df[['description','date_begin','date_end','y0','y1']].iloc[[0]],
                            hovertemplate="<br>".join(["Description: %{customdata[0]}",
                                                    "Beginning date: %{customdata[1]}",
                                                    "Ending date: %{customdata[2]}",
                                                    '<extra></extra>']),
                            line=dict(color="black",width=1),
                            fillcolor="grey",
                            opacity=0.5,
                            fill="toself",
                )) 
fig.show()

If I create a loop, the rectangles plot correctly but the hover labels aren’t correct:

df = pd.DataFrame(columns=['description','date_begin','date_end','y0','y1'])
df.loc[0] = ['red', '2022-09-24 08:22:31', '2022-09-24 20:40:00', 0, 1]
df.loc[1] = ['blue', '2022-10-09 02:54:56', '2022-10-10 19:09:40', 0, 1]
df.loc[2] = ['green', '2022-11-03 06:21:51', '2022-11-05 15:15:48', 0, 1]

df['date_begin'] =  pd.to_datetime(df['date_begin'])
df['date_end'] =  pd.to_datetime(df['date_end'])

df['x'] = df.apply(lambda x: [x['date_begin'], x['date_begin'], x['date_end'], x['date_end'], x['date_begin']], axis=1)
df['y'] = df.apply(lambda x: [x['y1'], x['y0'], x['y0'], x['y1'], x['y1']], axis=1)

fig = go.Figure()
for i in range(df.shape[0]):
    fig.add_traces(go.Scatter(  x=df['x'].iloc[i], 
                                y=df['y'].iloc[i],
                                mode='lines+markers',
                                customdata=df[['description','date_begin','date_end','y0','y1']].iloc[i],
                                hovertemplate="<br>".join(["Description: %{customdata[0]}",
                                                        "Beginning date: %{customdata[1]}",
                                                        "Ending date: %{customdata[2]}",
                                                        '<extra></extra>']),
                                line=dict(color="black",width=1),
                                fillcolor="grey",
                                opacity=0.5,
                                fill="toself",
                    )) 
fig.show()

p.s. This is my first post and I don’t know how to embed the python code in this post

Hi @genericds! Welcome to the forums. I fixed the formatting for you, I hope you don’t mind. :pray:

Thanks, @AIMPED! How do I format it? (for next time)

Have a look here:

1 Like

Hi @genericds !
Welcome on the forum!

I took a look at your issue, it is because of the data type you provide in the traces for x, y and customdata.

These 3 arguments accept:

Type: list, numpy array, or Pandas series of numbers, strings, or datetimes

See reference docs: x / y / customdata

In your first example
df['x'] and df['y'] are pandas series, but of lists :x:
df[['description','date_begin','date_end','y0','y1']] is a dataframe :x:

In the second example
df['x'].iloc[0] and df['y'].iloc[0] both are a single list :ok_hand:
df[['description','date_begin','date_end','y0','y1']].iloc[[0]] is actually a dataframe with one row :x:
NOTE:
I think you meant .iloc[0], like in the third example, which can work but not the way you want:
the value of ‘description’ will be associated to the 1st point, ‘date_begin’ to the 2nd point, ‘date_end’ to the 3rd…
Meaning each point will receive a scalar as customdata, so you cannot call customdata[0] / [1] / [2]
If you want several custom data on each point you have to provide a list for each point, thus a list of list.
I don’t like it much, but something like: [df[['description', 'date_begin', 'date_end']].iloc[0]] * 5 should do the job

The third example is similar to the second, just looping through df rows to have the three rectangles.


Moreover, if you use fig.add_traces() (plural), you should provide a list of traces.
Plotly seems ok with a single trace, but you should use fig.add_trace() (singular) in your examples.
Like:

#plural
fig.add_traces([
    go.Scatter(
        x=df['x'].iloc[i],
        y=df['y'].iloc[i]
    ) for i in range(df.shape[0])
])

or

#singular
for i in range(df.shape[0]):
    fig.add_trace([
        go.Scatter(
            x=df['x'].iloc[i],
            y=df['y'].iloc[i]
        ) 
    ])

Last thing:

You will probably need to use a loop, but for information:

  • you can use a trick like second example here, you can use a single scatter plot and insert some None in your data lists to split the line to separate the rectangles
  • Or maybe you can use something like px.line(df, x=df['x'], y=df['y'], color=df['description']) that will generate one trace by ‘description’ field

But whatever method you use you have to reshape your data to provide the fig what it needs


I tried to be as clear as possible, I hope that helps.
I’m going to try to build an example of what you need, I’ll be back! :sunglasses:

Here we go that should do the job :grin:

df = pd.DataFrame(columns=['description', 'date_begin', 'date_end', 'y0', 'y1'])
df.loc[0] = ['red', '2022-09-24 08:22:31', '2022-09-24 20:40:00', 0, 1]
df.loc[1] = ['blue', '2022-10-09 02:54:56', '2022-10-10 19:09:40', 0, 1]
df.loc[2] = ['green', '2022-11-03 06:21:51', '2022-11-05 15:15:48', 0, 1]

df['date_begin'] = pd.to_datetime(df['date_begin'])
df['date_end'] = pd.to_datetime(df['date_end'])

data_list = [
    {
        'x': [df['date_begin'][row], df['date_begin'][row], df['date_end'][row], df['date_end'][row],
              df['date_begin'][row]],
        'y': [df['y1'][row], df['y0'][row], df['y0'][row], df['y1'][row], df['y1'][row]],
        'customdata': [[df['description'][row], df['date_begin'][row], df['date_end'][row]]] * 5,
    } for row in range(len(df))
]

fig = go.Figure()

fig.add_traces(
    [
        go.Scatter(
            x=data['x'],
            y=data['y'],
            mode='lines+markers',
            customdata=data['customdata'],
            hovertemplate=(
                'Description: %{customdata[0]}<br>'
                'Beginning date: %{customdata[1]}<br>'
                'Ending date: %{customdata[2]}<br>'
                '<extra></extra>'
            ),
            line=dict(color="black", width=1),
            fillcolor="grey",
            opacity=0.5,
            fill="toself",
        ) for data in data_list
    ]
)

fig.show()

1 Like

@Skiks, you are awesome! Thank you so much for the clear and concise explanation. This works exactly as I had hoped.

I like the way you structure your loops. I will be adopting this format from now on.

Glad to help! :tada:
It is not always easy to transform the data from raw data to the structured data the figures need.

That’s list comprehension, I love this feature of python! You can also use similar structure for dict

>>> {f'Key{i}': i for i in range(3)}
{'Key0': 0, 'Key1': 1, 'Key2': 2}

:sparkles: BONUS :sparkles:
I took the no trace loop challenge ! :muscle:

  • Using the little trick of adding None:
data = {
    'x': [],
    'y': [],
    'customdata': [],
}

for row in range(len(df)):
    data['x'] += [df['date_begin'][row], df['date_begin'][row], df['date_end'][row], df['date_end'][row],
                       df['date_begin'][row], None]
    data['y'] += [df['y1'][row], df['y0'][row], df['y0'][row], df['y1'][row], df['y1'][row], None]
    data['customdata'] += [[df['description'][row], df['date_begin'][row], df['date_end'][row]]] * 5 + [None]

fig = go.Figure()
fig.add_scatter(
        x=data['x'],
        y=data['y'],
        mode='lines+markers',
        customdata=data['customdata'],
        hovertemplate=(
            'Description: %{customdata[0]}<br>'
            'Beginning date: %{customdata[1]}<br>'
            'Ending date: %{customdata[2]}<br>'
            '<extra></extra>'
        ),
        line=dict(color="black", width=1),
        fillcolor="grey",
        opacity=0.5,
        fill="toself",
    )
  • Using px.line() :
data = {
    'x': [],
    'y': [],
    'Description': [],
    'Beginning date': [],
    'Ending date': [],
}

for row in range(len(df)):
    data['x'] += [df['date_begin'][row], df['date_begin'][row], df['date_end'][row], df['date_end'][row],
                       df['date_begin'][row]]
    data['y'] += [df['y1'][row], df['y0'][row], df['y0'][row], df['y1'][row], df['y1'][row]]
    data['Description'] += [df['description'][row]] * 5
    data['Beginning date'] += [df['date_begin'][row]] * 5
    data['Ending date'] += [df['date_end'][row]] * 5

fig = px.line(
    data,
    x="x",
    y="y",
    color='Description',
    hover_data={
        'x': False,
        'y': False,
        'Description': True,
        'Beginning date': True,
        'Ending date': True,
    }
)
1 Like

Very nice! It’s always good to see how these things work. I use a lot of this day to day and will be very helpful for future projects. @Skiks, thank you so much for all of your time!