Announcing Dash Bio 1.0.0 🎉 : a one-stop-shop for bioinformatics and drug development visualizations.

```Graph.extend``` data property not working when used with patten matching callback

So, I’m dynamically adding dcc.graphs to a div at the click of a button using pattern matching callbacks.


so when I click the button highlighted in red, the entire blue portion gets added below it.

I am also using another callback like this

@app.callback(Output({'type':'graph-1','index':MATCH}, 'extendData'),
               Input({'type':'interval','index':MATCH}, 'n_intervals'))
def update_graph(graph):
    /*some logic*/
    return data    /*[ideally should be on len 1 as it gets updated every 1 second]*/

What this callback does is update the graph every 1 second with real-time values when the sync toggle is turned on.
This works fine and does the job that I want it to do. However, when I add/delete another blue plot to the main div component, the update_graph function gets messed up and gives this kind of output. (Based on correct values it should have been a straight line…similar to 0-8 on x axis).

PS. I can add/delete as many graphs as I want before starting the sync toggle on any graph. But, if I do it during/after the sync toggle then I get this kind of a result.

Also adding a few callbacks for more clarification

def new_time_sig_plot(idx):
  new_div = html.Div(id={'type':'t-plot','index':idx}, children=[
    html.Div(id={'type': 'time-sig-options','index':idx}, className='filters', children=[
      daq.ToggleSwitch(id={'type':'sync-toggle','index':idx}, label='SYNC', value=False, size=40, color='#2D6BCF'), 
      dcc.Dropdown( placeholder='group', id={'type': 'group-filter','index':idx}, className='input-attrs', options=group_options, value=None),
      dcc.Dropdown( placeholder='variable', id={'type': 'variable-filter','index':idx}, className='input-attrs', options=variable_options, value=None),
      dcc.Dropdown( placeholder='node', id={'type': 'node-filter','index': idx}, className='input-attrs', 
                  options=[dict(label=option,value=option) for option in nodes_options], value=nodes_options[-1]),
      dcc.Dropdown( placeholder='component', id={'type': 'component-filter','index':idx}, className='input-attrs', options=component_options, value=None),
      html.Button('⚙ Filter', id={'type': 'apply-time-filters', 'index':idx},className="filter-btn", n_clicks=0),
      html.Button('🗑️', id={'type': 'del-t-plt', 'index':idx},className='del'),
    ]),
    dcc.Dropdown(placeholder='Choose time signal',id={'type': 't-signal-selector', 'index':idx},options=all_time_signals,multi=True,value=[]),
    dcc.Graph(id={'type':'graph-1','index':idx}, figure=dict(data=[],layout=time_sig_layout), animate=True),
    dcc.Interval(id={'type':'interval','index':idx},interval=1*1000)
  ])
  return new_div

def new_node_sig_div(idx):
  #pretty similar to the above function but it doesn't use the update_graph callback so I'm skipping it
  return new_div

@app.callback(
  [Output('time-sig-div', 'children'), Output('node-sig-div', 'children')],
  [Input('new-t-plt', 'n_clicks'), Input({'type': 'del-t-plt','index': ALL}, 'n_clicks'),
   Input('new-n-plt', 'n_clicks'), Input({'type': 'del-n-plt','index': ALL}, 'n_clicks')],
  [State('time-sig-div', 'children'), State('node-sig-div', 'children')],
  prevent_initial_call = False)
def edit_time_div(add_time, del_time, add_node, del_node, time_div, node_div):
  global group_options; global variable_options; global component_options; global all_time_signals; global all_node_signals;
  triggered = [t["prop_id"] for t in dash.callback_context.triggered][0]

  add_time = add_time or 0; add_node = add_node or 0

  if triggered == 'new-t-plt.n_clicks' or triggered == '.':
    if len(time_div) == 5: return dash.no_update
    time_div.append(new_time_sig_plot(add_time))

  if triggered == 'new-n-plt.n_clicks' or triggered == '.':
    if len(node_div) == 5: return dash.no_update
    node_div.append(new_node_sig_div(add_node))
  
  if triggered[-20:-17] == 'del':
    ID = json.loads(triggered[0:-9])
    del_idx = ID["index"]

    if ID["type"] == 'del-t-plt':
      for div_idx, div in enumerate(time_div):
        if del_idx == div["props"]["id"]["index"]:
          del time_div[div_idx]
          break
    else:
      for div_idx, div in enumerate(node_div):
        if del_idx == div["props"]["id"]["index"]:
          del node_div[div_idx]
          break
  
  # print(time_div[0])
  return time_div, node_div

Hi @supratik,

Could you share the callback triggered by “Add Time Plot” (that creates the new dcc.Graph and dcc.Interval components)?

hello jlfs, thanks for the response.

I’ve added the edit_time_div callback that you asked for in the original post.

Thanks! It makes sense that it is like this…

I suspect that the unusual behavior has to do with the updates in the dcc.Interval component. Every time you add/remove a graph, you are rewriting the entire div, including the specific dcc.Interval for existing graphs, then your “problematic” callback gets triggered with the original values (n_intervalsand so on) in the moment the callback updating the div is done.

If that is the case, I would suggest you to have a global dcc.Interval that is never rewritten. Then you just need to change the update_graph callback to use Output({'type':'graph-1','index': ALL}, 'extendData') and update all graphs in the same component. This is much better in terms of performance as well, as this callback won’t be triggered more often than once in a second for all components.

2 Likes

Hi, I tried to implement your suggestions but pretty much got the same results.
If I have my sync toggle ON and then try to add a new plot, I get the error saying gd.data must be a valid array.

I can also turn off the sync button and then add a new plot, but then my graph gets messed up like this.


Here is my update graph callback

@app.callback(Output({'type':'graph-1','index':ALL}, 'extendData'),
               Input('interval', 'n_intervals'),
              [State({'type':'graph-1','index':ALL}, 'figure'),
               State({'type': 't-signal-selector', 'index': ALL}, 'value'),
               State('hdf-filename', 'value'),
               State('time-sig-div', 'children')], prevent_initial_call=True)
def update_graph(interval, graphs, input_state, f5_file, time_div):
  output = []
  if len(graphs) == 0:
    return dash.no_update
  
  output = [0] * len(graphs)
  for idx, graph in enumerate(graphs):
    if not graph["data"] or len(input_state[idx])==0:
      print("early")
      output[idx] = {'x':[0],'y':[0]} 
      continue
        

    with h5py.File(f5_file, 'r', libver='latest', swmr=True) as f5:
      extended_time = []
      extended_data = []
###############################################################
      for l_idx, s_idx in enumerate(input_state[idx]):
        signal_dset = f5[signals.t[s_idx].table]
        time_dset = f5[signals.t[s_idx].timeline]
        
        h5_idx = len(time_dset)  #last index of table
        graph_idx = len(graph['data'][l_idx]['y'])

        t = time_dset[graph_idx:h5_idx]
        new_data = signal_dset[signals.t[s_idx].coord][graph_idx:h5_idx]

        extended_time.append(t)
        extended_data.append(new_data)
#####################################################################
        # print(extended_data)
      output[idx] = (dict(x=extended_time, y=extended_data))
    print()
  print(output)
  return [output[i] for i in range(len(graphs))]

You can skip the portion between the ###### lines; That is just the creation of the extended traces.

Here is also the structure of my webpage so that you can understand my return statement in a better way.

Could you share the output of the last print(output), just before the return statement? I don’t think extendData is formatted as it should, that’s why you are getting this error.

Second, are you sure that the new points are chronologically ordered? Sometimes the weird lines you’re seen is because of unordered time-series…

I’v actually checked regarding the chronology of the dataset and so far it seems fine.

Everything before the red horizontal line works fine as there is only one graph.
Then I add another plot and it stops updating, and the length of the ‘x’ and ‘y’ keeps increasing which should ideally be 1 as they get updated.
The thing that you see in yellow is just a placeholder as I cannot send an empty dictionary as an output.

1 Like

Could you try replacing your placeholder for:

{"x": [[0]], "y": [[0]]}

I am quite sure that updateData expects a list of lists for each coordinate, where the outer list has the same length as the number of traces being updated. In your case you have just one trace per plot, but you might still need it… Note that this is the same format as your first element, where you have a list with one array.

The accumulation is expected, as graph_idx does not get updated if the callback fails.