Hovertemplate with customdata or hover_data of variable shape

I’m trying to format hover text and include additional data with customdata or hover_data.
The issue is I do not know in advance the shape of the additional data.

Actually, I found 2 ways of doing this:
1) Plotly Express

fig = px.line( 
        ef,
        x=ef["Risk"],
        y=ef["Return"],
        hover_data=ef.columns[3:],  # DataFrame with assets weights
        title=f"Efficient Frontier",
    )

This approach seems to be the most straightforward. The additional data inherits labels form the original DataFrame (not only values).
However I can’t edit hovertemplate here. And I didn’t find a way to control the hover_data floats format.

  1. go.Figure
    weights_array = np.stack([ef[n] for n in ef.columns[3:]], axis=-1)
    fig = go.Figure(
        data=go.Scatter(
            x=ef["Risk"],
            y=ef["Return"],
            customdata=weights_array,
            hovertemplate=('<b>Risk: %{x:.2f}% <br>Return: %{y:.2}%</b><BR><BR>weights:<BR>' +
                           '%{customdata}%' +
                           '<extra></extra>'),
            mode="lines",
            name=f"Efficient Frontier",
        )
    )

It seems to be more flexible approach as hovertemplate can be managed here. However I still miss required features:

  • no way to get data labels
  • %{customdata:.2f}% does not work … no way to set float formats again

Do I miss something? Is there any other way to handle additional data with variable shape? Is there a way to set format for floats for the whole Hovertemplate?

@Chilango74
To get help you should give a more precise information on your data:

  • first, a minimal data frame with (synthetic) data, similar to your actual data;
  • what do you mean by unknown shape of additional data? the number of columns is unknown or the number of elements in each additional column or both?

If the number of additional columns is unknown a priori, meaning that you intend to run the same code with different dataframes, then with Python it seems to be impossible to define the hovertemplate string, such that to display formated customdata.
If we have the following dataframe, then, theoretically, the hovertemplate should be defined as follows:

import plotly.graph_objects as go
import pandas as pd
import numpy as np
n=20
rsk=  np.sort(4+2*np.random.rand(n))
w= np.random.rand(2*n)
df= pd.DataFrame({"Risk": rsk, "Return":np.log(rsk), "col2": w[:n], "col3": w[n:]})
customdata = np.stack([df[n] for n in df.columns[2:]], axis=-1)
############################
addstring = "<b>Risk: %{x:.2f}% <br>Return: %{y:.2}%</b><BR>"
hstring=""
for k in range(2, len(df.columns)):
     hstring = hstring  + f "%{customdata[{k-2}]:.2f}%<br>"    
hovertemplate = addstring + "<br>weights:<br>" + hstring+"<extra></extra>"

But if you run this code you’ll get errors, because the string, hovertemplate, contains { }, by default, and when using python f-strings, you also must insert {k-2} to get customdata[0], and customdata[1], and these two kinds of { } cannot be interpreted correctly. There is a conflict between plotly.js requirements for the string hovertemplate, and Python interpretation of f-strings.
Since the Julia language uses another way to format the equivalent of python f-strings, in this language I was able to get the right displaying of hoverformat.

using PlotlyJS, DataFrames
n=20
rsk=  sort(4 .+2*rand(n))
w= rand(2*n)
df= DataFrame(Risk= rsk, Return=log.(rsk), col2= w[1:n], col3= w[n+1:2*n])

#############################################
addstring = "<b>Risk: %{x:.2f}% <br>Return: %{y:.2}%</b><BR>"
hstring=""
for k in 3:length(names(df))
    hstring = hstring*"%{customdata[$(k-3)]:.2f}%<br>"
end    
hovertemplate =addstring*"<br>weights:<br>"*hstring*"<extra></extra>"
#############################################

pl=Plot(scatter(
            x=df[!, :Risk],
            y=df[!, :Return],
            customdata=Matrix(df[!, 3:end])', ###Important!!!! python  customdata must be transposed  in Julia                      
            hovertemplate=hovertemplate,
            mode="lines",
            name="Efficient Frontier"
        )
    )

Let us printing the string hovertemplate:

print(hovertemplate)
<b>Risk: %{x:.2f}% <br>Return: %{y:.2}%</b><BR><br>weights:<br>%{customdata[0]:.2f}%<br>%{customdata[1]:.2f}%<br><extra></extra>

3 Likes

@empet Thank you very much for explanation.
The additional data in customdata has both variable dimensions. The problem is obviously with columns.

The applications shows the Efficient Frontier for investment portfolios. X-axis is the risk (standard deviation) and Y-axis is the rate of return. However, portfolio can have any number of stocks inside. And the user should be able to get the information about weighs (not only risk and return).

I also tried to iterate over customdada:

hovertemplate=('<b>Risk: %{x:.2f}% <br>Return: %{y:.2}%</b><BR><BR>weights:<BR>'+'<BR>'.join(
                 ['%{customdata[{i}]}, ' for i in range(3, shape)]
             )

But didn’t succeed. Glad to know that it works in Julia. Hopefully in further versions of plotly hovertemplate will have some options for bulk formatting.

Actually, I found a way to get full portfolio information by printing the data below the Graph with clickData callback.

The working application is at okama.io. And the code at GitHub.

1 Like

Hi, I don’t speak English, but I will try.
I faced the same problem, and tried the same, iterar over the customdata . After that I decided put the html tags with the data:

customdata=[['<BR><b>Preço: </b> R$ '+str(df_sol.loc[i, 'preco']),
             '<BR><b>Segmento:</b> '+str(df_sol.loc[i, 'segmento'])]
              for i in df_sol.index],

hovertemplate = '<b>%{label} </b> <br>'+
                '<b>Investido:</b> R$ %{value:.2f}<br>%{customdata}'

And it work well to me:
Screenshot_20230527-175531~3

I addressed the issue doing this:
Dataframe

columns=list(df.columns)
template=[]
for i in df.index:
    dados_sim=[]
    index = f'<BR><b>Index: </b> {i}'
    dados_sim.append(index)
    for j in columns:
        column_name = j
        if column_name != 'Sharpe Ratio':
            linha = f'<BR><b>{j}: </b> {round(df.loc[i, j]*100,2)}%'
        else:
            linha = f'<BR><b>{j}: </b> {round(df.loc[i, j],2)}'
        dados_sim.append(linha)
    dados_sim = ','.join(dados_sim) + f'<extra></extra>'
    template.append(dados_sim)
Graph
columns_shares = list(df.columns[3:])
fig = go.Figure()
fig.add_trace(go.Scatter(customdata=template, x=df['Volatility'], 
                         y=df['Returns'], 
                      #- Add color scale for sharpe ratio   
                      marker=dict(color=(df['Sharpe Ratio']), 
                                  showscale=True, 
                                  size=5,
                                  line=dict(width=1),
                                  colorscale="RdBu",
                                  colorbar=dict(title="Sharpe<br>Ratio")
                                 ), 
                      mode='markers',
                       hovertemplate= template
                        ))

fig.update_layout(template='plotly_white',
                  xaxis=dict(title='Annualised Risk (Volatility)'),
                  yaxis=dict(title='Annualised Return'),
                  title='Sample of Random Portfolios',
                  coloraxis_colorbar=dict(title="Sharpe Ratio"))

Preformatted text

`