Search code examples
pythonplotlyplotly-dash

Plotly: 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:

enter image description here

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:

enter image description here

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.


Solution

  • Two things:

    1. The problem of non-aligning fill and graph is because you gave the line its own axis (secondary_y = True). Without specifying, this gets assigned a range automatically. You can solve this by setting the range of both Y-axes manually, or by grabbing the auto-range of the fill and assigning it to the second Y-axis.

    2. The easiest solution to your original question is to add an empty trace with an y-axis which is defined as the primary axis range minus the starting value.

    '

    full_fig = fig.full_figure_for_development()
    
    org_yrange = full_fig.layout.yaxis.range
    sec_yrange = (0, org_yrange[1] - org_yrange[0])
    
    fig.add_trace(go.Scatter(x=[],y=[],yaxis='y2'))
    fig.update_layout(
        yaxis2=dict(
            range=sec_yrange,
            side='right',
            overlaying='y',
            ),
    )