dcc.Graph - figure incorrectly rendered when in inactive tab, ok if tab active

Hello,

I have an application which has multiple tabs. One tab contains a dcc.Graph object, whose figure is a go.Figure with Scattermapbox objects as data.

The figure is calculated upon a user date range entry above the tabs. This can happen when any tab is active. My issue is that when the map tab is not active, the map is not correctly rendered when opening the tab for the first time. It occupies only a small portion of the space (top left corner), and the rest is blank.

If the user recalculates the figure (i.e. selects a new date range) when the tab is active, or clicks the ‘reset view’ button in the graph, or resizes the browser window, the figure goes back to normal and occupies the full dedicated space as expected.

I have not yet found an explanation or a solution, except refreshing the figure at each tab change with a callback. However, this has the significant disadvantage that many calculations are rerun for nothing each time a tab is clicked (this takes several seconds).

I tried setting the various autosize and responsive properties but nothing seem to work.

Here is a screenshot of the figure with the problem (sorry for the link, image upload does not seem to go through): link

Here is the tab content code:

tab4_content = dmc.TabsPanel(
    dbc.CardBody(
        [
            dbc.Spinner(dcc.Graph(id="map-graph", responsive=True))
        ]
), value='map')

(I’m using dmc Tabs, but the problem is identical with dbc Tabs)

Any help appreciated!

Thanks.

Hello @dme3,

Welcome to the community!

My guess is that you are encountering this issue due to the size of the element when hidden.

You might be able to set the div which houses the element to 100% width and height and see if that helps it render properly.

Hello, thanks for your answer!

Unfortunately, I tried tweaking all style width, height parameters without any success. I tried at the level of the dcc.Graph component itself, or at the level of the CardBody parent, or even when nesting the Graph in a html.Div. Nothing works and when the map is updated when its tab is inactive I still get such a render:

link

Any time the window is resized, or the zoom in/out buttons or reset view buttons are used, the display goes back to normal:

link

I just can’t find a clean solution or a workaround that works well. I even tried to slightly change the Div width by one percent alternatively at each tab change:

@app.callback(
    Output('map-graph', 'style'),
    Input("tabs", "value"),
    State('map-graph', 'style')
    )
def refresh_map(at, style):

    new_width = '99%' if style['width'] == '100%' else '100%'

    return {'display': 'inline-block', 'width': new_width}

but it does not work… :confused:

I don’t know what I am doing wrong, or if this is a bug. Maybe there is a way to reset the Graph view from within a callback?

Can you post a minimum working example of how you are using it, so that we can replicate?

OK, see below an example of code that works for me to reproduce the issue.

Load the app. The map tab should be correct. Go to another tab, hit calculate, and go back to map. It should now be drawn incorrectly.

In the meantime, I’ve found the following old post, that seems to be about the same issue: Need to reset each Scattermapbox for it to show up properly

There does not seem to be a solution since.

import dash
import dash_bootstrap_components as dbc
import dash_mantine_components as dmc
from dash import Dash, dcc, html, dash_table, ctx
from dash.dependencies import Input, Output, State
from plotly import graph_objs as go
import plotly.express as px


# Plotly mapbox public token
mapbox_access_token = "put your own token here"

### App declaration
#
app = dash.Dash(external_stylesheets=[dbc.themes.SLATE, dbc.icons.FONT_AWESOME])
#
###

### Tabs
#
tab1_content = dmc.TabsPanel(
    html.P('0', id = 'tab1_text'),
    value = 'tab1',
)
tab2_content = dmc.TabsPanel(
    html.P('0', id = 'tab2_text'),
    value = 'tab2',
)
tab3_content = dmc.TabsPanel(
    dbc.CardBody(
        [
            html.P('0', id = 'tab3_text'),
            html.Div(            
            dbc.Spinner(dcc.Graph(id="map-graph", responsive=True, style={'display': 'inline-block', 'width': '100%'}))
            , id = 'map-div')

        ], id = 'map-card'
), value='tab3')

tabs = html.Div([dmc.TabsList(
            [
                dmc.Tab("Tab 1", value="tab1", style={"color": 'white'}),
                dmc.Tab("Tab 2", value="tab2", style={"color": 'white'}),
                dmc.Tab("Map", value="tab3", style={"color": 'white'}),
            ],
            id = 'tabslist'
)])
#
###

header = dbc.CardBody(
    [
        html.P("Header", style={"color": 'white'}),
        dbc.Row([
        dbc.Col(dbc.Button('Calculate', id='calculate-button'), width="auto", align="center"),
    ], justify="between"),
    ],
)

#### Helper functions
#
def generate_map_fig():

    filtered_fig = go.Figure(data=[
        go.Scattermapbox(
            lat=[48],
            lon=[4],
            mode="markers",
            hoverinfo="lat+lon+text",
        )])

    filtered_fig.update_layout(
        hovermode='closest',
        showlegend=False,
        mapbox=dict(
            accesstoken=mapbox_access_token,
            bearing=0,
            center=go.layout.mapbox.Center(
                lat=48,
                lon=4
            ),
            pitch=0,
            zoom=12,
            style='streets'
        ),
        margin={"r":10,"t":10,"l":10,"b":10}
    )

    return filtered_fig
#
####

#### Callbacks
#
@app.callback(
    [Output('tab1_text', 'children'),
    Output('tab2_text', 'children'),
    Output('tab3_text', 'children'),
    Output('map-graph', 'figure')],
    [Input('calculate-button', 'n_clicks')],
    State('tab1_text', 'children')
    )
def update_output(clicks, value):

    result = 42 + int(value)
    new_fig = generate_map_fig()
    return result, result, result, new_fig
#
####

### Layout
#
app.layout = html.Div([
    header,
    dmc.Divider(variant="solid"),
    dmc.Tabs([
        tabs,
        tab1_content,
        tab2_content,
        tab3_content,
    ], value='tab1',id='tabs', persistence=True, variant='pills', color='blue'),
])
#
###

if __name__ == '__main__':
    app.run_server(debug=True)

Hello @dme3,

It seems that this is specifically related to mapbox and how it operates.

In the picture, we can see the graph is reading as taking up the entire space.

To update the map, we’d have to use a clientside callback because of how the javascript renders the mapbox.

Give this a try:

import dash
import dash_bootstrap_components as dbc
import dash_mantine_components as dmc
from dash import Dash, dcc, html, dash_table, ctx
from dash.dependencies import Input, Output, State
from plotly import graph_objs as go
import plotly.express as px


# Plotly mapbox public token
mapbox_access_token = "key"

### App declaration
#
app = dash.Dash(external_stylesheets=[dbc.themes.SLATE, dbc.icons.FONT_AWESOME],
                external_scripts=["https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"])
#
###

### Tabs
#
tab1_content = dmc.TabsPanel(
    html.P('0', id = 'tab1_text'),
    value = 'tab1',
)
tab2_content = dmc.TabsPanel(
    html.P('0', id = 'tab2_text'),
    value = 'tab2',
)
tab3_content = dmc.TabsPanel(
    dbc.CardBody(
        [
            html.P('0', id = 'tab3_text'),
            html.Div(
            dbc.Spinner(dcc.Graph(id="map-graph", responsive=True, style={'display': 'inline-block', 'width': '100%'}))
            , id = 'map-div')

        ], id = 'map-card'
), value='tab3', id='map-panel')

tabs = html.Div([dmc.TabsList(
            [
                dmc.Tab("Tab 1", value="tab1", style={"color": 'white'}),
                dmc.Tab("Tab 2", value="tab2", style={"color": 'white'}),
                dmc.Tab("Map", id='map-tab', value="tab3", style={"color": 'white'}),
            ],
            id = 'tabslist'
)])
#
###

header = dbc.CardBody(
    [
        html.P("Header", style={"color": 'white'}),
        dbc.Row([
        dbc.Col(dbc.Button('Calculate', id='calculate-button'), width="auto", align="center"),
    ], justify="between"),
    ],
)

#### Helper functions
#
def generate_map_fig():

    filtered_fig = go.Figure(data=[
        go.Scattermapbox(
            lat=[48],
            lon=[4],
            mode="markers",
            hoverinfo="lat+lon+text",
        )])

    filtered_fig.update_layout(
        hovermode='closest',
        showlegend=False,
        mapbox=dict(
            accesstoken=mapbox_access_token,
            bearing=0,
            center=go.layout.mapbox.Center(
                lat=48,
                lon=4
            ),
            pitch=0,
            zoom=12,
            style='streets'
        ),
        margin={"r":10,"t":10,"l":10,"b":10}
    )

    #filtered_fig = px.scatter_geo()

    return filtered_fig
#
####

#### Callbacks
#
@app.callback(
    [Output('tab1_text', 'children'),
    Output('tab2_text', 'children'),
    Output('tab3_text', 'children')],
    [Input('calculate-button', 'n_clicks')],
    State('tab1_text', 'children')
    )
def update_output(clicks, value):

    result = 42 + int(value)
    return result, result, result

@app.callback(
    Output('map-graph', 'figure'),
    Input('calc-chart','n_clicks')
)
def calcChart(n):
    return generate_map_fig()

app.clientside_callback(
    """
        function (i) {
            setTimeout(function() {
            var oldVal;
            $("#tabs-tab-tab3").on('click',
                function () {
                    if (oldVal != $('#tab3_text').text()) {
                        $('#calc-chart').click()
                        oldVal = $('#tab3_text').text()
                    }
                }
            )
            $('#calculate-button').on('click',
                function () {
                    $('#calc-chart').click()
                    if ($('#map-div').is(':visible')) {
                        setTimeout(function () {
                            oldVal = $('#tab3_text').text()
                        }, 300)
                    }
                }
            )
            }, 300)
            return window.dash_clientside.no_update
        }
    """,
    Output('map-div','id'),
    Input('map-div','id')
)

#
####

### Layout
#
app.layout = html.Div([
    html.Button(id='calc-chart', style={'display':'none'}),
    header,
    dmc.Divider(variant="solid"),
    dmc.Tabs([
        tabs,
        tab1_content,
        tab2_content,
        tab3_content,
    ], value='tab1',id='tabs', persistence=True, variant='pills', color='blue'),
])
#
###

if __name__ == '__main__':
    app.run_server(debug=True)
1 Like

@jinnyzor going to jump into this one, margin=dict(t=0, b=0, l=0, r=0) try that, also try settng go.Layout() here is my example:

layout = go.Layout(
            height=700,
            autosize=True,
            mapbox_accesstoken=token,
            mapbox_style="mapbox://styles/cryptopotluck/clbd5fc7d001414kcj49x1yof",
            mapbox_center={"lat": 27.952323793542178, "lon": -97.10239291191102},
            paper_bgcolor='rgba(0,0,0,0)',
            plot_bgcolor='rgba(0,0,0,0)',
            margin=dict(t=0, b=0, l=0, r=0),

            font=dict(
                family="Courier New, monospace",
                size=18,
                color="#7f7f7f"
            ),
            mapbox = dict(
                center=dict(
                    lat=27.952323793542178,
                    lon=-97.10239291191102
                ),
                pitch=0,
                zoom=3,
            )
        )

    fig = go.Figure(data= , layout=layout)

Just gotta add data

Thanks for your reply and proposal!

My actual application is more complex than the example I provided of course, so I had to adapt your code. I have to say that I am not familiar either client side callbacks nor js, so this was not easy. But I found that by simplifying it to the minimum, it can solve the issue. This is what I used:

@app.callback(
    Output('map-graph', 'figure'),
    [Input('calc-chart','n_clicks'),
    Input('date-range-picker', 'value'),
    Input('radio-map-setup', 'value')]
)
def calcChart(n, date_range, user_selection):
    return generate_map_fig(user_selection, date_range)[0]


app.clientside_callback(
    """
        function (i) {
            var oldVal;
            $("#tabs-tab-map").on('click',
                function () {
                    $('#calc-chart').click()
                }
            )
            return window.dash_clientside.no_update
        }
    """,
    Output('map-div','id'),
    Input('map-div','id')
)

If I understand correctly, the client side callback makes a virtual click on a hidden button when the map tab is selected, which in turn refreshes the figure…? With the additional inputs I added, the figure is also correctly updated when other settings are changed. I had to remove some of the subtleties of your code (e.g. Timeouts), but apparently it works. I’ll keep testing it!

Thanks again!

1 Like

Hello,

I had a try, but unfortunately setting the layout ahead or setting the margins to 0 do not resolve the issue. However @ jinnyzor proposal above seem to do the trick.

Hello @dme3,

Glad it works.

Yes, it recalculated based upon it being either the active tab recalculates or the first time the tab is activated afterwards.

It won’t fire again if it is not the initial time after hitting calculate.

As far as clientside and Js. Not a way around it as the elements needed to trigger this action do not have ids registered with the dash app.

Just to follow up as I used to have this issue with scattermapbox and I must admit I cannot remember how I solved it because it is not happening anymore.
However, if you Ctrl-scroll or other zoom change, window change etc, the figure should be displayed normally without re-computing.