I am trying to link a data table with multiple pages and a graph that displays all the pages of the data table. When I click on the data table, the relevant vertex in the graph is highlighted. When I hover over a vertex, the page_current value in the data table is updated and the active_cell is changed.
The issue I am having is when I update page_current and active_cell in a callback, active_cell updates, then page_current_updates, which clears the active cell. This results in the active_cell only highlighting if the vertex on the graph is in the same page as the data table. If it isn’t, the active_cell blinks and the page_current changes, with no cell highlighted. If you unhover then hover over the same point, the active cell will highlight.
I created a workaround by creating a chained callback (which is commented out below) named placeholder. With the chained callback, page_current is updated on the data table and the active cell is passed to the placeholder, which then updates the active cell in the data table. This works, but throws the errors below
Circular Dependencies
10:31:41 AM
Error: Dependency Cycle Found: data_table.page_current → placeholder.active_cell → data_table.active_cell → placeholder.active_cell
Is there a way to link a graph and data table with multiple pages without throwing errors by updating the page_current before the active_cell in a single call back or using a method like the chained callbacks I commented out?
from jupyter_dash import JupyterDash
from dash import Dash, dcc, html, Input, Output, no_update, dash_table, State, callback_context
from dash.exceptions import PreventUpdate
import plotly.express as px
import pandas as pd
import dash_bootstrap_components as dbc
import json
from datetime import datetime as dt, timedelta
import plotly.graph_objects as go
import os, sys
import datetime
import re
import time
import numpy as np
from collections import defaultdict
app = JupyterDash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
dict_list = [{'fid': 'a1', 'tail': 'test1', 'x': 1, 'y': 1}, {'fid': 'b2', 'tail': 'test2', 'x': 2, 'y': 2},
{'fid': 'c3', 'tail': 'test3', 'x': 3, 'y': 1}, {'fid': 'd4', 'tail': 'test1', 'x': 2, 'y': 2}, {'fid': 'e5', 'tail': 'test2', 'x': 4, 'y': 4},
{'fid': 'f6', 'tail': 'test3', 'x': 6, 'y': 4}, {'fid': 'g7', 'tail': 'test1', 'x': 3, 'y': 3}, {'fid': 'h8', 'tail': 'test2', 'x': 6, 'y': 6},
{'fid': 'i9', 'tail': 'test3', 'x':12, 'y': 10}]
df = pd.DataFrame(dict_list)
app.layout = html.Div(
dbc.Container([dbc.Row(
dash_table.DataTable(id='data_table',
columns=[{"name": i, "id": i} for i in ['fid', 'tail', 'x', 'y']],
page_current=0,
page_size=5,
page_action='custom',
filter_action='custom',
filter_query='',
sort_action='custom',
sort_mode='multi',
sort_by=[],
style_filter_conditional=[{
'if': {'column_id': 'date_processed'},
'pointer-events': 'None'}]),
style={'height': 750, 'overflowY': 'scroll'},
className='six columns'),
dbc.Row([dcc.Graph(id="graph",clear_on_unhover=True),
dcc.Tooltip(id="graph-tooltip",background_color="lightgrey",border_color="blue")]),
dbc.Row([dbc.Alert(id='tbl_out'),dcc.Store(id='memory-output')])
]))
operators = [['ge ', '>='],
['le ', '<='],
['lt ', '<'],
['gt ', '>'],
['ne ', '!='],
['eq ', '='],
['contains '],
['datestartswith ']]
def split_filter_part(filter_part):
for operator_type in operators:
for operator in operator_type:
if operator in filter_part:
name_part, value_part = filter_part.split(operator, 1)
name = name_part[name_part.find('{') + 1: name_part.rfind('}')]
value_part = value_part.strip()
v0 = value_part[0]
if (v0 == value_part[-1] and v0 in ("'", '"', '`')):
value = value_part[1: -1].replace('\\' + v0, v0)
else:
try:
value = float(value_part)
except ValueError:
value = value_part
# word operators need spaces after them in the filter string,
# but we don't want these later
return name, operator_type[0].strip(), value
return [None] * 3
@app.callback(
Output('data_table', "data"),
Output('graph', "figure"),
Output("graph-tooltip", "show"),
Output("graph-tooltip", "bbox"),
Output("graph-tooltip", "children"),
#Output("memory-output", "data"),
Output("data_table", "active_cell"),
Output("data_table", "selected_cells"),
Output('data_table', 'page_current'),
Output('tbl_out', 'children'),
Input('data_table', "page_current"),
Input('data_table', "sort_by"),
Input('data_table', "filter_query"),
Input("graph", "hoverData"),
Input("graph", "figure"),
Input('data_table', 'active_cell'),
State('data_table', 'data'),
State('data_table', "page_size"))
def update_table(page_current, sort_by, filter, hoverData, dict_fig, active_cell, data_table, page_size):
dff = get_table(df, page_size, page_current, sort_by, filter)
triggered_id = callback_context.triggered[0]['prop_id']
if 'data_table.active_cell' == triggered_id:
try:
active_cell, selected_cells, fig = update_graphs(active_cell, data_table, dict_fig)
return no_update, fig, False, no_update, no_update, active_cell, selected_cells, no_update, F"{callback_context.triggered}-----{callback_context.triggered_prop_ids}---{callback_context.triggered_id}"
except Exception as e:
return no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, str(e)
elif 'graph.hoverData' == triggered_id:
try:
#show, bbox, children, active_cell, selected_cells, geojson3, idx, error = update_tooltip_content(hoverData, flights_geojson, flight_data, dff, page_size)
try:
show, bbox, children, active_cell, selected_cells, idx, error = update_tooltip_content(hoverData, data_table, dff, page_size)
hover_rec = hoverData['points'][0]['customdata'][0]
except:
hover_rec = None
if hover_rec is not None:
fig = go.Figure(dict_fig)
fig.for_each_trace(lambda trace: trace.update(visible=False) if trace.name == "highlight" else())
page_current = int((idx)/page_size)
#
return dff.iloc[
page_current*page_size: (page_current + 1)*page_size
].to_dict('records'), fig, show, bbox, children,active_cell, selected_cells, page_current, page_current
else:
return no_update, no_update, False, bbox, no_update,no_update, no_update, no_update,no_update#F"{idx}:{active_cell}"
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
return no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update,F"{exc_type}, {fname}, {exc_tb.tb_lineno}"
else:
fig = px.line(dff.sort_values('x').reset_index(drop=True)[['tail', 'fid', 'x', 'y']], x="x", y="y",color="tail",hover_data=[dff.fid])
fig.update_traces(
hoverinfo="none",
hovertemplate=None
)
fig.add_scatter(y=[0], x=[0], mode="markers", visible = False, name = 'highlight', hoverinfo='skip', showlegend=False, marker=dict(size=12))
return dff.iloc[
page_current*page_size: (page_current + 1)*page_size
].to_dict('records'), fig, no_update, no_update, no_update,no_update, no_update, page_current, no_update
#@app.callback(
# Output("data_table", "active_cell"),
# Input('memory-output', "data"))
def update_highlight(active_cell):
return active_cell
def get_table(dff, page_size, page_current, sort_by, filter):
if filter is not None:
filtering_expressions = filter.split(' && ')
for filter_part in filtering_expressions:
col_name, operator, filter_value = split_filter_part(filter_part)
if operator in ('eq', 'ne', 'lt', 'le', 'gt', 'ge'):
# these operators match pandas series operator method names
dff = dff.loc[getattr(dff[col_name], operator)(filter_value)]
elif operator == 'contains':
dff = dff.loc[dff[col_name].str.contains(filter_value)]
elif operator == 'datestartswith':
# this is a simplification of the front-end filtering logic,
# only works with complete fields in standard format
dff = dff.loc[dff[col_name].str.startswith(filter_value)]
if len(sort_by):
dff = dff.sort_values(
[col['column_id'] for col in sort_by],
ascending=[
col['direction'] == 'asc'
for col in sort_by
],
inplace=False
)
return dff.reset_index(drop=True)
def find_xy(fid, json_dict):
for tail_obj in json_dict:
i = 0
for obj in tail_obj['customdata']:
if obj[0] == fid:
return tail_obj['x'][i], tail_obj['y'][i]
i +=1
return None, None
def find_table_idx(fid, flight_data):
i = 0
for row_obj in flight_data:
if fid == row_obj['fid']:
return i
i += 1
return None
def update_tooltip_content(hoverData, flight_data, dff, page_size):
try:
if hoverData is None:
return no_update, no_update, no_update, no_update, no_update, None, None
pt = hoverData["points"][0]
bbox = pt["bbox"]
fid = hoverData['points'][0]['customdata'][0]
try:
idx = dff.index[dff['fid'] == fid][0]
activecell, selectedcells = [{'column':0, 'row':idx-int(idx/page_size)*page_size, 'column_id': 'noupdate'}, [{'column':0, 'row':idx-int(idx/page_size)*page_size}]]
except:
idx = 0
activecell, selectedcells = [None, []]
children = [
html.P(f"Date: {pt['x']}, ASP: {pt['y']}, fid: {fid}")
]
return True, bbox, children, activecell, selectedcells, idx, F"{idx}:{fid}:{dff.index[dff['fid'] == fid][0]}-----{dff.to_json(orient='records')}"
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
return no_update, no_update, no_update, no_update, no_update, 0, F"{dff.to_json(orient='records')}----{idx}:{fid}----{exc_type}, {fname}, {exc_tb.tb_lineno}"
def update_graphs(active_cell, flight_data, dict_fig):
if active_cell:
fig = go.Figure(dict_fig)
fig.for_each_trace(
lambda trace: trace.update(visible=False) if trace.name == "highlight" else (),
)
if active_cell['column_id'] =='fid':
fid = flight_data[active_cell['row']]['fid']
x, y = find_xy(fid, dict_fig['data'])
if x is not None and y is not None:
fig.for_each_trace(
lambda trace: trace.update(visible=True, x=[x], y=[y]) if trace.name == "highlight" else (),
)
return active_cell, [active_cell], fig
elif active_cell['column_id'] =='noupdate':
return active_cell, [active_cell], no_update
else:
return None, [], fig
else:
return None, [], no_update
if __name__ == '__main__':
app.run_server(debug=True) # Running the application```