Add both primary and secondary axis for same line

I am working on a school project. I have created the following callback to update a graph. The graph simply plots 1) a line showing the values of the variable Wealth, 2) a horizontal line at the starting value of Wealth, 3) fills the area between the first and the second line. I would like to modify the code to add ticks on the vertical axis on the left, showing the difference between the value of Wealth and the starting its value. Therefore, I don’t want to add a 3rd line, I just want to add the ticks that essentially show the profit. I will show my attempt below this code snippet.

# Callbacks to update graphs based on the original code
@app.callback(
    Output('wealth-over-time', 'figure'),
    Input('wealth-time-range-slider', 'value'))
def update_wealth_over_time(selected_range):
    filtered_df = df[df['Day'].between(selected_range[0], selected_range[1])]
    # Create a new DataFrame for the fill area
    fill_area_df = pd.DataFrame({
        'Day': filtered_df['Day'],
        'Starting Wealth': starting_wealth,
        'Wealth ($)': filtered_df['Wealth ($)']
    })

    fig = go.Figure()
    
    # Add fill area between starting wealth and Wealth ($)
    fig.add_trace(go.Scatter(
        x=pd.concat([fill_area_df['Day'], fill_area_df['Day'][::-1]]),  # X-values concatenated with reverse for seamless fill
        y=pd.concat([fill_area_df['Starting Wealth'], fill_area_df['Wealth ($)'][::-1]]),  # Y-values for starting wealth and Wealth ($) with reverse for fill
        fill='toself',
        fillcolor='rgba(135, 206, 250, 0.5)',  # Light blue fill with transparency
        line=dict(color='rgba(255,255,255,0)'),  # Invisible line around the fill area
        showlegend=False,
        name='Fill Area',
    ))
    
    # Add Wealth ($) line on top of the fill area
    fig.add_trace(go.Scatter(
        x=filtered_df['Day'], 
        y=filtered_df['Wealth ($)'],
        mode='lines+markers',
        showlegend=False,
        name='Wealth ($)',
        line=dict(color='DeepSkyBlue'),
    ))

    # Add dashed horizontal line for starting wealth
    fig.add_shape(
        type='line',
        x0=filtered_df['Day'].min(),
        x1=filtered_df['Day'].max(),
        y0=starting_wealth,
        y1=starting_wealth,
        line=dict(color='Gray', dash='dash', width=2),
    )
    
    fig.update_layout(
        title={'text': "Wealth", 'font': {'color': 'black'}},
        plot_bgcolor='white',
        xaxis=dict(
            title='Day',
            color='black',
            showgrid=True,
            gridcolor='lightgrey',
            gridwidth=1,
            showline=True,
            linewidth=2,
            linecolor='black',
            mirror=True
        ),
        yaxis=dict(
            title='Wealth ($)',
            color='black',
            showgrid=True,
            gridcolor='lightgrey',
            gridwidth=1,
            showline=True,
            linewidth=2,
            linecolor='black',
            mirror=True
        ),
        xaxis_range=[filtered_df['Day'].min(), filtered_df['Day'].max()]
    )
    fig.add_shape(
        go.layout.Shape(
            type="line",
            x0=min(filtered_df['Day']),
            y0=starting_wealth,
            x1=max(filtered_df['Day']),
            y1=starting_wealth,
            line=dict(
                color="black",
                width=2,
                dash="dash",
            ),
        )
    )
    return fig

This generates the following image:

I have attempted to do this modification following the steps illustrated here. In particular, I made the following changes:

  1. from plotly.subplots import make_subplots

  2. Changed fig = go.Figure() to fig = make_subplots(specs=[[{"secondary_y": True}]])

  3. Added secondary_y=True as follows:

    # Add Wealth ($) line on top of the fill area
    fig.add_trace(go.Scatter(
        x=filtered_df['Day'], 
        y=filtered_df['Wealth ($)'],
        mode='lines+markers',
        showlegend=False,
        name='Wealth ($)',
        line=dict(color='DeepSkyBlue'),
    ), secondary_y=True,)

However, when I run the code, 1) the blue area is no longer aligned properly:

Moreover, 2) I do not know how to show the correct ticks (the difference of the ticks on the left and the starting value of Wealth).

I would really appreciate some help with this.

1 Like

Hi @nohedge.

First of all thank you for the detailed description of your issue. This makes helping you a lot easier!

I came up with a slightly different approach, maybe it helps:

from plotly.subplots import make_subplots
import plotly.graph_objects as go
import numpy as np
np.random.seed(42)

# create some data
starting_wealth = 14

x = np.arange(100)
y = np.random.randint(starting_wealth, 20, size=(100))
y_rel = y - starting_wealth
min_dif = y_rel.min()
max_dif = y_rel.max()

# base figure
fig = make_subplots(specs=[[{"secondary_y": True}]])

# starting_wealth line
fig.add_trace(
    go.Scatter(
        x=np.arange(100),
        y=np.ones(shape=100) * starting_wealth,
        name='starting wealth',
        line={'color': 'rgba(0, 0, 0, 1.0)', 'dash': 'dash'}
    )
)

    
# wealth line, filled to starting_wealth
fig.add_trace(
        go.Scatter(
        x=x,
        y=y,
        fill='tonexty',
        fillcolor='rgba(135, 206, 250, 0.5)',  
        showlegend=False,
        name='Fill Area',
        mode='lines+markers',
        line={'color': 'rgba(135, 206, 250, 1.0)'},
    )
)

# relative values
fig.add_trace(
    go.Scatter(
        x=x,
        y=y_rel,
        showlegend=False,
        mode='lines+markers',
        line={'color': 'rgba(0, 0, 0, 0.0)'},
        # ^^ set color to invisble. visible=True is necessary for the scondary axis
        visible=True,
        hoverinfo='skip'
    ), 
    secondary_y=True
)

# set ticks for first y-axis
fig.update_yaxes(
    dict(
        tickmode = 'linear',
        tick0 = 0.0,
        dtick = 1.0
    ),
    selector=0
)

# set ticks for second y-axis
fig.update_yaxes(
    dict(
        tickmode = 'array',
        tickvals = np.arange(min_dif, max_dif+1)
    ),
    selector=1
)

Thank you very much. The code worked as expected. There is only one minor issue left if you could help me. I updated the call back following your steps:

@app.callback(
    Output('wealth-over-time', 'figure'),
    Input('wealth-time-range-slider', 'value'))
def update_wealth_over_time(selected_range):
    filtered_df = df[df['Day'].between(selected_range[0], selected_range[1])]
    # Create a new DataFrame for the fill area
    fill_area_df = pd.DataFrame({
        'Day': filtered_df['Day'],
        'Starting Wealth': starting_wealth,
        'Wealth ($)': filtered_df['Wealth ($)']
    })

    fig = make_subplots(specs=[[{"secondary_y": True}]])
    
    fig.add_trace(
    go.Scatter(
        x=filtered_df['Day'],
        y=np.ones(shape=filtered_df.shape[0]) * starting_wealth,
        name='starting wealth',
        showlegend=False,
        line={'color': 'rgba(0, 0, 0, 1.0)', 'dash': 'dash'}
    ))

    fig.add_trace(
        go.Scatter(
        x=filtered_df['Day'],
        y=filtered_df['Wealth ($)'],
        fill='tonexty',
        fillcolor='rgba(135, 206, 250, 0.5)',  
        showlegend=False,
        name='Fill Area',
        mode='lines+markers',
        line={'color': 'rgba(135, 206, 250, 1.0)'},
    ))

    fig.add_trace(
    go.Scatter(
        x=filtered_df['Day'],
        y=filtered_df['Cumul P&L ($)'],
        showlegend=False,
        mode='lines+markers',
        line={'color': 'rgba(0, 0, 0, 0.0)'},
        # ^^ set color to invisble. visible=True is necessary for the scondary axis
        visible=True,
        hoverinfo='skip'
    ), 
    secondary_y=True
)
    
    fig.update_layout(
        margin=dict(r=0),
        title={'text': "Wealth", 'font': {'color': 'black'}},
        plot_bgcolor='white',
        xaxis=dict(
            title='Day',
            color='black',
            showgrid=True,
            gridcolor='lightgrey',
            gridwidth=1,
            showline=True,
            linewidth=2,
            linecolor='black',
            mirror=True
        ),
        xaxis_range=[filtered_df['Day'].min(), filtered_df['Day'].max()]
    )

    fig.update_yaxes(
        dict(
            title='Wealth ($)',
            color='black',
            showgrid=True,
            gridcolor='lightgrey',
            gridwidth=1,
            showline=True,
            linewidth=2,
            linecolor='black',
            tickmode = 'linear',
            tick0 = 0.0,
            dtick = 1.0
        ),
        selector=0
    )

    fig.update_yaxes(
        dict(
            title='Cumul P&L ($)',
            color='black',
            showline=True,
            linewidth=2,
            linecolor='black',
            tickmode = 'linear',
            tick0 = 0.0,
            dtick = 1.0
        ),
        selector=1
    )

    return fig

Now however I obtain an odd behavior. When I focus on a specific part of the plot by dragging the slider that I have created, I notice that the dashed horizontal line starts to have some black markers, which are not initially visible. What is causing this behavior?

I have tried to remove the markets all together by setting mode='lines' instead of mode='lines+markers' but this is having no effect. If needed I could upload the entire code with the data. Thank you again for your wonderful answer.

That’s strange. How do you create the slider?

Not sure where these markers come from. Try changing to this