Update form with intervals: react error

Hi, I’m writing an application in order to plot data according to selections made by the user in a simple form.

Here it is the application. I have simplified it to let you run if you would like to verify what I’m saying on your setup.

#!/usr/bin/env python3
'''plotly web interface'''
import uuid
import time
import os
import dash
import dash_core_components as dcc
import dash_html_components as html
from rq import Queue
from CouchbaseManager import CouchbaseManager
from worker import conn                 # worker.py handles the connection to Redis

# init objects
try:
    CMANAGER = CouchbaseManager()
except:
    print("Couchbase authentication error")

# configuring dash app
APP = dash.Dash('DBE view', static_folder='css')
static_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'css')

APP.layout = html.Div(children=[

    html.Link(href='/css/loader.css', rel='stylesheet'),

    html.H1(children='DBE view plot', style={"text-align": "center"}),

    # BATCH NUMBER AND TYPE OF DATA SELECTION
    html.Table([
        html.Tr([
            html.Td([
                html.H3(children='Select data to plot'),
                ], colSpan=4)
            ]),
        html.Tr([
            html.Td([
                html.Div(id='select-batch',
                         children='Batch code'),
                dcc.Dropdown(
                    id='batch-selection',
                    options=[
                        {'label': '10', 'value': 10},
                        {'label': '20', 'value': 20},
                        {'label': '30', 'value': 30}
                        ]
                )
                ], colSpan=1),
            html.Td([
                html.Div(id='select-type',
                         children='Data type (max 10)',
                         style={"color": "black"}),
                dcc.Dropdown(
                    id='data-plot',
                    options=[],
                    multi=True,
                )
                ], colSpan=3)
        ])
    ], style={"width": "100%"}),

    dcc.Interval(
        id='interval-delay',
        interval=60*60*1000, # in milliseconds
        n_intervals=0
    ),

    dcc.Interval(
        id='interval-delay-2',
        interval=60*60*1000, # in milliseconds
        n_intervals=0
    ),

    dcc.Interval(
        id='interval-delay-3',
        interval=60*60*1000, # in milliseconds
        n_intervals=0
    ),

    dcc.Interval(
        id='interval-delay-4',
        interval=60*60*1000, # in milliseconds
        n_intervals=0
    ),

    # invisible div to safely store the current job-id
    html.Div(id='job-id-min-time', style={'display': 'none'}),

    # invisible div to safely store the current job-id
    html.Div(id='job-id-max-time', style={'display': 'none'}),

    # invisible div to safely store the current job-id
    html.Div(id='job-id-data-plot', style={'display': 'none'}),

    # invisible div to safely store the current job-id
    html.Div(id='job-id-plot', style={'display': 'none'}),

    # TIME SLOT SELECTION TABLE
    html.Table([
        html.Tr([
            html.Td([
                html.Div(id='select-time-range1',
                         children='From:'),
                dcc.Dropdown(
                    id='min-time',
                    options=[]
                )
                ], colSpan=2),
            html.Td([
                html.Div(id='select-time-range2',
                         children='To:'),
                dcc.Dropdown(
                    id='max-time',
                    options=[]
                ),
                ], colSpan=2)
        ]),
        html.Tr([
            html.Td([
                html.Button('Draw plot', id='button')
                ], colSpan=4)
        ]),
    ], style={"width": "100%"}),

    dcc.Graph(
        id='plot-graph',
        figure={
            'data': [
                {'x': [], 'y': [], 'type': 'lines+markers', 'name': ''},
            ],
            'layout': {
                'title': 'Plot'
            }
        },
        config={
            'displayModeBar': False
        }
    ),

    html.A(
        'Download Plot',
        id='download-link',
        download="plot.pdf",
        href="/tmp/plot.pdf",
        target="_blank",
        style={"pointer-events": "none", "color": "grey"}
    )
])

################################
### --------  MIN TIME SELECTION
################################

@APP.callback(
    dash.dependencies.Output('job-id-min-time', 'children'),
    [dash.dependencies.Input('batch-selection', 'value')]
)
def get_time_range(batch):
    '''callback to get time ranges'''
    if isinstance(batch, type(None)):
        return None
    queue = Queue(connection=conn)
    job_id = str(uuid.uuid4())
    queue.enqueue_call(func=CMANAGER.get_time_range_values,
                       args=(batch,),
                       timeout='3m',
                       job_id=job_id)
    return job_id

@APP.callback(
    dash.dependencies.Output('min-time', 'options'),
    [dash.dependencies.Input('interval-delay', 'n_intervals')],
    [dash.dependencies.State('job-id-min-time', 'children')]
)
def update_min_time(_, job_id):
    '''update min-time when worker job ends'''
    queue = Queue(connection=conn)
    job = queue.fetch_job(job_id)
    if job is not None:
        # job exists - try to get result
        result = job.result
        if result is None:
            # results aren't ready, pause then return empty results
            # You will need to fine tune this interval depending on
            # your environment
            time.sleep(3)
            return ''
        return result
    else:
        # no job exists with this id
        return ''

@APP.callback(
    dash.dependencies.Output('interval-delay', 'interval'),
    [dash.dependencies.Input('job-id-min-time', 'children'),
     dash.dependencies.Input('interval-delay', 'n_intervals')])
def stop_or_start_min_time_update(job_id, _):
    ''' this callback orders the table to be regularly refreshed if
    the user is waiting for results, or to be static (refreshed once
    per hour) if they are not.'''
    queue = Queue(connection=conn)
    job = queue.fetch_job(job_id)
    if job is not None:
        # the job exists - try to get results
        result = job.result
        if result is None:
            # a job is in progress but we're waiting for results
            # therefore regular refreshing is required.  You will
            # need to fine tune this interval depending on your
            # environment.
            return 1000
        # the results are ready, therefore stop regular refreshing
        return 60*60*1000
    # the job does not exist, therefore stop regular refreshing
    return 60*60*1000

################################
### --------  MAX TIME SELECTION
################################

@APP.callback(
    dash.dependencies.Output('job-id-max-time', 'children'),
    [dash.dependencies.Input('min-time', 'value')],
    [dash.dependencies.State('batch-selection', 'value')]
)
def get_time_range_max(minimum, batch):
    '''callback to get time ranges (max)'''
    if ((isinstance(batch, type(None))) or (isinstance(minimum, type(None)))):
        return None
    queue = Queue(connection=conn)
    job_id = str(uuid.uuid4())
    queue.enqueue_call(func=CMANAGER.get_time_range_max_value,
                       args=(batch, minimum),
                       timeout='3m',
                       job_id=job_id)
    return job_id

@APP.callback(
    dash.dependencies.Output('max-time', 'options'),
    [dash.dependencies.Input('interval-delay-2', 'n_intervals')],
    [dash.dependencies.State('job-id-max-time', 'children')]
)
def update_max_time(_, job_id):
    '''update min-time when worker job ends'''
    queue = Queue(connection=conn)
    job = queue.fetch_job(job_id)
    if job is not None:
        # job exists - try to get result
        result = job.result
        if result is None:
            # results aren't ready, pause then return empty results
            # You will need to fine tune this interval depending on
            # your environment
            time.sleep(3)
            return ''
        return result
    else:
        # no job exists with this id
        return ''

@APP.callback(
    dash.dependencies.Output('interval-delay-2', 'interval'),
    [dash.dependencies.Input('job-id-max-time', 'children'),
     dash.dependencies.Input('interval-delay-2', 'n_intervals')])
def stop_or_start_max_time_update(job_id, _):
    ''' this callback orders the table to be regularly refreshed if
    the user is waiting for results, or to be static (refreshed once
    per hour) if they are not.'''
    queue = Queue(connection=conn)
    job = queue.fetch_job(job_id)
    if job is not None:
        # the job exists - try to get results
        result = job.result
        if result is None:
            # a job is in progress but we're waiting for results
            # therefore regular refreshing is required.  You will
            # need to fine tune this interval depending on your
            # environment.
            return 1000
        # the results are ready, therefore stop regular refreshing
        return 60*60*1000
    # the job does not exist, therefore stop regular refreshing
    return 60*60*1000

#################################
### --------  DATA TYPE SELECTION
#################################

@APP.callback(
    dash.dependencies.Output('job-id-data-plot', 'children'),
    [dash.dependencies.Input('batch-selection', 'value')]
)
def select_data_default_callback(batch):
    '''select values to be plotted'''
    if isinstance(batch, type(None)):
        return None
    queue = Queue(connection=conn)
    job_id = str(uuid.uuid4())
    queue.enqueue_call(func=CMANAGER.get_data_plot_selection,
                       args=(batch,),
                       timeout='3m',
                       job_id=job_id)
    return job_id
    # [_, default] = CMANAGER.get_data_plot_selection(batch)
    # return default

@APP.callback(
    dash.dependencies.Output('data-plot', 'options'),
    [dash.dependencies.Input('interval-delay-3', 'n_intervals')],
    [dash.dependencies.State('job-id-data-plot', 'children')]
)
def update_data_plot_options(_, job_id):
    '''update min-time when worker job ends'''
    queue = Queue(connection=conn)
    job = queue.fetch_job(job_id)
    if job is not None:
        # job exists - try to get result
        result = job.result
        if result is None:
            # results aren't ready, pause then return empty results
            # You will need to fine tune this interval depending on
            # your environment
            time.sleep(3)
            return ''
        return result[0]
    else:
        # no job exists with this id
        return ''

@APP.callback(
    dash.dependencies.Output('interval-delay-3', 'interval'),
    [dash.dependencies.Input('job-id-data-plot', 'children'),
     dash.dependencies.Input('interval-delay-3', 'n_intervals')])
def stop_or_start_data_plot_update(job_id, _):
    ''' this callback orders the table to be regularly refreshed if
    the user is waiting for results, or to be static (refreshed once
    per hour) if they are not.'''
    queue = Queue(connection=conn)
    job = queue.fetch_job(job_id)
    if job is not None:
        # the job exists - try to get results
        result = job.result
        if result is None:
            # a job is in progress but we're waiting for results
            # therefore regular refreshing is required.  You will
            # need to fine tune this interval depending on your
            # environment.
            return 1000
        # the results are ready, therefore stop regular refreshing
        return 60*60*1000
    # the job does not exist, therefore stop regular refreshing
    return 60*60*1000

@APP.callback(
    dash.dependencies.Output('select-type', 'children'),
    [dash.dependencies.Input('data-plot', 'value')]
)
def data_plot_value_limit(data):
    '''setting limit to plot lines'''
    default_string = "Data type (max 10)"
    if isinstance(data, type(None)):
        return default_string
    if len(data) > 10:
        return "Data type LIMIT EXCEEDED ({0} max 10)".format(len(data))
    return default_string

@APP.callback(
    dash.dependencies.Output('select-type', 'style'),
    [dash.dependencies.Input('data-plot', 'value')]
)
def data_plot_value_limit_style(data):
    '''setting limit to plot lines'''
    default_style = {"color": "black"}
    if isinstance(data, type(None)):
        return default_style
    if len(data) > 10:
        return {"color": "red"}
    return default_style

#################################
### --------  PLOT DISLPAY SECT.
#################################

@APP.callback(
    dash.dependencies.Output('job-id-plot', 'children'),
    [dash.dependencies.Input('button', 'n_clicks')],
    [dash.dependencies.State('min-time', 'value'),
     dash.dependencies.State('max-time', 'value'),
     dash.dependencies.State('batch-selection', 'value'),
     dash.dependencies.State('data-plot', 'value')]
)
def get_new_data_for_plot(_, time_min, time_max, batch, data):
    '''update plot'''
    if (isinstance(time_min, type(None)) or
            isinstance(time_max, type(None)) or
            isinstance(batch, type(None)) or
            isinstance(data, type(None))):
        return None
    if len(data) > 10:
        return None
    datal = data
    if not isinstance(datal, type([])):
        datal = [data]
    queue = Queue(connection=conn)
    job_id = str(uuid.uuid4())
    queue.enqueue_call(func=CMANAGER.get_new_plot_data,
                       args=(time_min, time_max, batch, datal),
                       timeout='3m',
                       job_id=job_id)
    return job_id
    #create_pdf_plot(plotdata, batch, time_min, time_max, data)

# @APP.callback(
#     dash.dependencies.Output('plot-graph', 'figure'),
#     [dash.dependencies.Input('interval-delay-4', 'n_intervals')],
#     [dash.dependencies.State('job-id-plot', 'children')]
# )
# def update_plot_figure(_, job_id):
#     '''update min-time when worker job ends'''
#     queue = Queue(connection=conn)
#     job = queue.fetch_job(job_id)
#     if job is not None:
#         # job exists - try to get result
#         result = job.result
#         if result is None:
#             # results aren't ready, pause then return empty results
#             # You will need to fine tune this interval depending on
#             # your environment
#             time.sleep(3)
#             return ''
#         return result
#     else:
#         # no job exists with this id
#         return ''

@APP.callback(
    dash.dependencies.Output('interval-delay-4', 'interval'),
    [dash.dependencies.Input('job-id-plot', 'children'),
     dash.dependencies.Input('interval-delay-4', 'n_intervals')])
def stop_or_start_plot_update(job_id, _):
    ''' this callback orders the table to be regularly refreshed if
    the user is waiting for results, or to be static (refreshed once
    per hour) if they are not.'''
    queue = Queue(connection=conn)
    job = queue.fetch_job(job_id)
    if job is not None:
        # the job exists - try to get results
        result = job.result
        if result is None:
            # a job is in progress but we're waiting for results
            # therefore regular refreshing is required.  You will
            # need to fine tune this interval depending on your
            # environment.
            return 1000
        # the results are ready, therefore stop regular refreshing
        return 60*60*1000
    # the job does not exist, therefore stop regular refreshing
    return 60*60*1000

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

I’ve written it starting from wiley’s boilerplate found on this forum (https://github.com/WileyIntelligentSolutions/wiley-boilerplate-dash-app) since I have to manage long callbacks.

The weird thing that happens is that if i comment out the “update_plot_figure” callback the app intervals work fine (except obviously that I cannot see the plot), while if I uncomment it all the intervals stop running.

I’m not able to figure out what I’m doing wrong in that callback, could you help me?

If you want to run it, here you are the worker.py (wiley’s):

import os

import redis
from rq import Worker, Queue, Connection

listen = ['high', 'default', 'low']

redis_url = os.getenv('REDISTOGO_URL', 'redis://0.0.0.0:6379')

conn = redis.from_url(redis_url)

if __name__ == '__main__':
    with Connection(conn):
        worker = Worker(map(Queue, listen))
        worker.work()

and CouchbaseManager.py:

import time
import plotly.graph_objs as go

class CouchbaseManager:
    '''Couchbase communication object'''

    @classmethod
    def get_data_plot_selection(cls, _):
        '''get data to plot selected by the user'''
        print("getting plot data selection")

        time.sleep(5)

        dd_options = ["one", "two", "three"]

        return [dd_options, "one"]

    @classmethod
    def get_time_range_values(cls, batch):
        '''get valid time ranges for user input'''
        print("getting time range values (MIN)")

        time.sleep(5)

        records_time = ["21.04.18 10:54:04", "21.04.18 10:54:14", "21.04.18 10:54:24"]

        return records_time

    @classmethod
    def get_time_range_max_value(cls, batch, minimum):
        '''get valid time ranges (max) for user input'''
        print("getting time range values (MAX)")

        time.sleep(5)

        records_time = ["21.04.18 10:54:34", "21.04.18 10:54:44", "21.04.18 10:54:54"]

        return records_time

    @classmethod
    def get_new_plot_data(cls, time_min, time_max, _, data_selectors, config):
        '''getting data to draw the plot'''
        print("getting new plot data: ")

        time.sleep(10)

        for selector in data_selectors:
            print(selector)
        data = go.Data()
        for selector in data_selectors:

            values = [1, 2, 3]

            time_refs = ["21.04.18 10:54:34", "21.04.18 10:54:44", "21.04.18 10:54:54"]

            data.append(go.Scatter(
                x=time_refs,
                y=values,
                mode="markers+lines",
                text=values,
                name=selector
            ))
        layout = go.Layout(title='Plot')
        fig = go.Figure(data=data, layout=layout)
        return fig

Thank you in advance for your time

I add that when the function is uncommented I get the following error in the Java console:

I’m not familiar with react, can someone point me in the right direction?

Thanks in advance

Working on the last javascript error, I’ve found this post on this forum:
Javascript error (fantasy-land/map) undefined

The solution of the mystery for me was the one of @galacticketchup: returning a wrong figure value to plots causes this behaviour.

Thank you guys!