pythonpython-3.xplotly-dashplotly

How do I make it so that the zoom on a plotly chart stays persistent even when new data loads?


I have a chart that looks like this-

import datetime

import dash
from dash import dcc, html
import plotly
from dash.dependencies import Input, Output
from pyorbital.orbital import Orbital
satellite = Orbital('TERRA')

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.layout = html.Div(
    html.Div([
        html.H4('TERRA Satellite Live Feed'),
        html.Div(id='live-update-text'),
        dcc.Graph(id='live-update-graph'),
        dcc.Interval(
            id='interval-component',
            interval=10*1000, # in milliseconds
            n_intervals=0
        )
    ])
)


@app.callback(Output('live-update-text', 'children'),
              Input('interval-component', 'n_intervals'))
def update_metrics(n):
    lon, lat, alt = satellite.get_lonlatalt(datetime.datetime.now())
    style = {'padding': '5px', 'fontSize': '16px'}
    return [
        html.Span('Longitude: {0:.2f}'.format(lon), style=style),
        html.Span('Latitude: {0:.2f}'.format(lat), style=style),
        html.Span('Altitude: {0:0.2f}'.format(alt), style=style)
    ]


# Multiple components can update everytime interval gets fired.
@app.callback(Output('live-update-graph', 'figure'),
              Input('interval-component', 'n_intervals'))
def update_graph_live(n):
    satellite = Orbital('TERRA')
    data = {
        'time': [],
        'Latitude': [],
        'Longitude': [],
        'Altitude': []
    }

    # Collect some data
    for i in range(180):
        time = datetime.datetime.now() - datetime.timedelta(seconds=i*20)
        lon, lat, alt = satellite.get_lonlatalt(
            time
        )
        data['Longitude'].append(lon)
        data['Latitude'].append(lat)
        data['Altitude'].append(alt)
        data['time'].append(time)

    # Create the graph with subplots
    fig = plotly.tools.make_subplots(rows=2, cols=1, vertical_spacing=0.2)
    fig['layout']['margin'] = {
        'l': 30, 'r': 10, 'b': 30, 't': 10
    }
    fig['layout']['legend'] = {'x': 0, 'y': 1, 'xanchor': 'left'}

    fig.append_trace({
        'x': data['time'],
        'y': data['Altitude'],
        'name': 'Altitude',
        'mode': 'lines+markers',
        'type': 'scatter'
    }, 1, 1)
    fig.append_trace({
        'x': data['Longitude'],
        'y': data['Latitude'],
        'text': data['time'],
        'name': 'Longitude vs Latitude',
        'mode': 'lines+markers',
        'type': 'scatter'
    }, 2, 1)

    return fig


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

Right now, if I zoom into the chart to look at a specific section of data and then the data refreshes, I lose my zoom position. I want to make the zoom position persistent even as new data loads. Perhaps settings the update as a background update is the answer here.


Solution

  • Seems like this should work. I added a dcc.Store to save the relevant zoom information for each subplot, and when reloading I read from the data of that dcc.Store to see if it has any historic zoom information to load from. I used this source as a guideline - https://community.plotly.com/t/how-to-save-current-zoom-and-position-after-filtering/5310.

    import datetime
    
    import dash
    from dash import dcc, html
    import plotly
    from dash.dependencies import Input, Output, State
    from pyorbital.orbital import Orbital
    satellite = Orbital('TERRA')
    
    external_stylesheets = [
        'https://codepen.io/chriddyp/pen/bWLwgP.css',
        {
            'href': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css',
            'rel': 'stylesheet',
            'integrity': 'sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO',
            'crossorigin': 'anonymous'
        }
    ]
    
    
    app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
    app.layout = html.Div(
        html.Div([
            html.H4('TERRA Satellite Live Feed'),
            html.Div(id='live-update-text'),
            dcc.Graph(id='live-update-graph'),
            dcc.Interval(
                id='interval-component',
                interval=10*1000, # in milliseconds
                n_intervals=0
            ),
            dcc.Store(id='zoom_info')
        ])
    )
    
    
    @app.callback(
        Output('zoom_info', 'data'),
        [Input('live-update-graph', 'relayoutData'),
         Input('zoom_info', 'data')]
    )
    def update_zoom_info(relayout_data, zoom_info):
        if zoom_info is None:
            return relayout_data
        else:
            zoom_info.update(relayout_data)
            return zoom_info
    
    
    @app.callback(Output('live-update-text', 'children'),
                  Input('interval-component', 'n_intervals'))
    def update_metrics(n):
        lon, lat, alt = satellite.get_lonlatalt(datetime.datetime.now())
        style = {'padding': '5px', 'fontSize': '16px'}
        return [
            html.Span('Longitude: {0:.2f}'.format(lon), style=style),
            html.Span('Latitude: {0:.2f}'.format(lat), style=style),
            html.Span('Altitude: {0:0.2f}'.format(alt), style=style)
        ]
    
    # Multiple components can update everytime interval gets fired.
    @app.callback(Output('live-update-graph', 'figure'),
                  Input('interval-component', 'n_intervals'),
                  State('zoom_info', 'data'))
    def update_graph_live(n, zoom_info):
        satellite = Orbital('TERRA')
        data = {
            'time': [],
            'Latitude': [],
            'Longitude': [],
            'Altitude': []
        }
    
        # Collect some data
        for i in range(180):
            time = datetime.datetime.now() - datetime.timedelta(seconds=i*20)
            lon, lat, alt = satellite.get_lonlatalt(
                time
            )
            data['Longitude'].append(lon)
            data['Latitude'].append(lat)
            data['Altitude'].append(alt)
            data['time'].append(time)
    
        # Create the graph with subplots
        fig = plotly.tools.make_subplots(rows=2, cols=1, vertical_spacing=0.2)
        fig['layout']['margin'] = {
            'l': 30, 'r': 10, 'b': 30, 't': 10
        }
        fig['layout']['legend'] = {'x': 0, 'y': 1, 'xanchor': 'left'}
    
        fig.append_trace({
            'x': data['time'],
            'y': data['Altitude'],
            'name': 'Altitude',
            'mode': 'lines+markers',
            'type': 'scatter'
        }, 1, 1)
        fig.append_trace({
            'x': data['Longitude'],
            'y': data['Latitude'],
            'text': data['time'],
            'name': 'Longitude vs Latitude',
            'mode': 'lines+markers',
            'type': 'scatter'
        }, 2, 1)
    
        if zoom_info:
            for axis_name in ['axis', 'axis2']:
                if f'x{axis_name}.range[0]' in zoom_info:
                    fig['layout'][f'x{axis_name}']['range'] = [
                        zoom_info[f'x{axis_name}.range[0]'],
                        zoom_info[f'x{axis_name}.range[1]']
                    ]
                if f'y{axis_name}.range[0]' in zoom_info:
                    fig['layout'][f'y{axis_name}']['range'] = [
                        zoom_info[f'y{axis_name}.range[0]'],
                        zoom_info[f'y{axis_name}.range[1]']
                    ]
        return fig
    
    
    if __name__ == '__main__':
        app.run_server(debug=True)