Show and Tell - Time History Plotting with Dynamic Plots and linked X axis Zooming

Starting with the finance tutorial https://dash-gallery.plotly.host/dash-stock-tickers-demo-app/ I was able to create a proof of concept that provided an example of

  • Dynamic Plots
  • Linked Zoom on the x axis.

I hope that this example can serve others if they are looking for a Linked Zoom example with dynamic plots. Unfortunately there is a bit of a bug when you zoom on the first graph - so it is best to avoid using the first plot for zooming.

The dynamic callbacks created quite a bit of complexity. I am looking forward when this can be done on the browser side and have been watching https://github.com/plotly/plotly.js/pull/3506 which should take care of it.

Next I would like to implement the ability to move and resize figures as mentioned in Figure Resizing and Moving Options, but I do not know where to start for the time being.

Anyhow, thanks for the great documentation, and I hope this example helps get someone started, albeit knowing that it is not perfect.

import dash
from dash.dependencies import Input, Output
import dash_html_components as html
import dash_core_components as dcc

import colorlover as cl
import datetime as dt

import pandas as pd
import numpy as np
import random
import requests

app = dash.Dash(
    external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
)
server = app.server

colorscale = cl.scales['9']['qual']['Paired']

######
# Generate some random time series data
######

word_site = "http://svnweb.freebsd.org/csrg/share/dict/words?view=co&content-type=text/plain"
response = requests.get(word_site)
WORDS = response.content.splitlines()
WORDS = [x.decode('utf-8') for x in WORDS]
random.shuffle(WORDS)

n=3600 # Points
N=100 # Channels

data = np.random.randn(n,N)

df = pd.DataFrame(np.random.randn(n, N), columns=WORDS[0:N])

app.layout = html.Div([
    html.Div(id='hidden-div', style={'display':'none'}),

html.Div([
        html.H2('Time History Plotter',
                style={'display': 'inline',
                       'float': 'left',
                       'font-size': '2.65em',
                       'margin-left': '7px',
                       'font-weight': 'bolder',
                       'font-family': 'Product Sans',
                       'color': "rgba(117, 117, 117, 0.95)",
                       'margin-top': '20px',
                       'margin-bottom': '0'
                       }),
        html.Img(src="https://s3-us-west-1.amazonaws.com/plotly-tutorials/logo/new-branding/dash-logo-by-plotly-stripe.png",
                style={
                    'height': '100px',
                    'float': 'right'
                },
        ),
    ]),
     dcc.Dropdown(
        id='Signals',
        options=[{'label': s, 'value': str(s)}
                 for s in df],
        value=[WORDS[0]],
        multi=True
    ),
        html.Div(id='container'),

html.Div(dcc.Graph(id='empty', figure={'data': []}), style={'display': 'none'}),
])

app.config['suppress_callback_exceptions']=True

# Dynamically generate all of the callbacks
for i in range(N):
    @app.callback(Output('graph_{}'.format(i), 'children'), [
        Input('Signals', 'value'),
        Input('igraph_{}'.format(i), 'relayoutData'),
        ])
    def graph_update(Sigs,relayoutData):
        if relayoutData is None:
            return 
        elif 'xaxis.range[0]' in relayoutData:
            XR0=relayoutData['xaxis.range[0]']
            XR1=relayoutData['xaxis.range[1]']
            return replot(Sigs,[XR0,XR1])
        elif 'xaxis.autorange' in relayoutData:
            return replot(Sigs,'autoX')
        else:
            return

def replot(Sigs,X):
    graphs = []
    for i, Sig in enumerate(Sigs):

        dff = df[Sig]
        time = pd.date_range('2016-07-01', periods=len(dff), freq='S')

        trace = [{
                    'x': time, 'y': dff,
                    'type': 'scatter', 'mode': 'lines',
                    'line': {'width': 1, 'color': colorscale[(i*2) % len(colorscale)]},
                    #'hoverinfo': 'none',
                    #'legendgroup': Sig,
                    'showlegend': True,
                    'name': '{}'.format(Sig)
                }]

        graphs.append(html.Div(
            children=[
            dcc.Graph(
                        id='igraph_{}'.format(i),
                        figure={
                            'data': trace,
                            'layout': {
                            'legend': {'x': 0},
                            'margin': {'l': 40, 'r': 20, 't': 20, 'b': 40},
                            'xaxis': {
                            'title': 'Time [s]',
                            'autorange': True if X == 'autoX' else False,
                            'range': False if X == 'autoX' else X
                            },
                            'yaxis': {
                            'title': 'Time [s]'
                            },
                        }
                    }, className="four columns",
                ), 
            html.Div(id="graph_{}".format(i)),
        ]))
    return html.Div(graphs)

# Requried to ADD plots
@app.callback(Output('container', 'children'), [Input('Signals', 'value')])
def display_graphs(Sigs):
    return replot(Sigs,'autoX')

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

I’ve updated the code in the original post to improve one of the odd issues that I was having. This example is working well now. However, if you are very quick with back to back zooms you can put the backend into what appears to be an infinite loop.

Does anyone know if Implementing matching axes #3506 works with dash? #3506 is in plotly.js 1.47.2, dash-core-components is currently on 1.47.0.

Here is a gif of it in action

1 Like

As it turns out, my first post is an excellent example of of what not to do - it results in a callback nightmare. And that got me thinking - I don’t need to give the user the ability to make endless plots. Providing them with enough plots does the job just fine and hiding the initial plots gives the illusion that they are being dynamically created.

Here is a much more stable solution:

import dash
from dash.dependencies import Input, Output
import dash_html_components as html
import dash_core_components as dcc

import colorlover as cl
import datetime as dt

import pandas as pd
import numpy as np


app = dash.Dash(
    external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
)
server = app.server

colorscale = cl.scales['9']['qual']['Paired']

######
# Generate some random time series data
######


n=3600 # Points
N=100 # Channels

WORDS=['Signal_{}'.format(i) for i in range(N)]


data = np.random.randn(n,N)

df = pd.DataFrame(np.random.randn(n, N), columns=WORDS[0:N])

# Make 10 placement graphs for use later

graph_div = []
for i in range(10):
    graph_div.append(html.Div(
                    children=[
                    dcc.Graph(id='igraph_{}'.format(i), figure={'data': []},style={'display': 'none'},className="four columns"),
                    html.Div(id="graph_{}".format(i))]))

# Basic layout

app.layout = html.Div([
    html.Div(id='hidden-div', style={'display':'none'}),

html.Div([
        html.H2('Time History Plotter',
                style={'display': 'inline',
                       'float': 'left',
                       'font-size': '2.65em',
                       'margin-left': '7px',
                       'font-weight': 'bolder',
                       'font-family': 'Product Sans',
                       'color': "rgba(117, 117, 117, 0.95)",
                       'margin-top': '20px',
                       'margin-bottom': '0'
                       }),
        html.Img(src="https://s3-us-west-1.amazonaws.com/plotly-tutorials/logo/new-branding/dash-logo-by-plotly-stripe.png",
                style={
                    'height': '100px',
                    'float': 'right'
                },
        ),
    ]),
     dcc.Dropdown(
        id='Signals',
        options=[{'label': s, 'value': str(s)}
                 for s in df],
        value=[],
        multi=True
    ),
        html.Div(id='container'),
        # Add those 10 graphs in
        html.Div(graph_div),
])


app.config['suppress_callback_exceptions']=True

# 10 call backs
@app.callback(Output('container', 'children'), [
    Input('Signals', 'value'),
    Input('igraph_0', 'relayoutData'),
    Input('igraph_1', 'relayoutData'),
    Input('igraph_2', 'relayoutData'),
    Input('igraph_3', 'relayoutData'),
    Input('igraph_4', 'relayoutData'),
    Input('igraph_5', 'relayoutData'),
    Input('igraph_6', 'relayoutData'),
    Input('igraph_7', 'relayoutData'),
    Input('igraph_8', 'relayoutData'),
    Input('igraph_9', 'relayoutData'),
    ])
def graph_update(Sigs,RD0,RD1,RD2,RD3,RD4,RD5,RD6,RD7,RD8,RD9):
    relayoutData=RD0
    # Do any of the RD0 have xaxis.range?
    for RD in [RD0,RD1,RD2,RD3,RD4,RD5,RD6,RD7,RD8,RD9]:
        if RD is not None and 'xaxis.range[0]' in RD:
            relayoutData=RD
            break

    if relayoutData is None:
        return replot(Sigs,'autoX')
        #return
    elif 'xaxis.range[0]' in relayoutData:
        XR0=relayoutData['xaxis.range[0]']
        XR1=relayoutData['xaxis.range[1]']
        return replot(Sigs,[XR0,XR1])
    elif 'xaxis.autorange' in relayoutData:
        return replot(Sigs,'autoX')
    elif Sigs:
        return replot(Sigs,'autoX')
    else:
        return

def replot(Sigs,X):
    graphs = []
    for i, Sig in enumerate(Sigs):

        dff = df[Sig]
        time = pd.date_range('2016-07-01', periods=len(dff), freq='S')

        trace = [{
                    'x': time, 'y': dff,
                    'type': 'scatter', 'mode': 'lines',
                    'line': {'width': 1, 'color': colorscale[(i*2) % len(colorscale)]},
                    #'hoverinfo': 'none',
                    #'legendgroup': Sig,
                    'showlegend': True,
                    'name': '{}'.format(Sig)
                }]

        graphs.append(html.Div(
            children=[
            dcc.Graph(
                        id='igraph_{}'.format(i),
                        figure={
                            'data': trace,
                            'layout': {
                            'legend': {'x': 0},
                            'margin': {'l': 40, 'r': 20, 't': 20, 'b': 40},
                            'xaxis': {
                            'title': 'Time [s]',
                            'autorange': True if X == 'autoX' else False,
                            'range': False if X == 'autoX' else X
                            },
                            'yaxis': {
                            'title': 'Time [s]'
                            },
                        }
                    }, className="four columns",
                ), 
            html.Div(id="graph_{}".format(i)),
        ]))
    return html.Div(graphs)


if __name__ == '__main__':
    app.run_server(debug=True, host='0.0.0.0')
2 Likes