Hi everyone,
I’m working on a Dash app that visualizes Jira data fetched from a MongoDB database. The app allows users to filter data by date range, issue type, workgroup, and granularity, and then updates the URL based on the selected filters. However, I’m encountering a circular dependency issue that I haven’t been able to resolve despite multiple attempts.
The Problem
I keep getting the following error:
Error: Dependency Cycle Found: store-date-picker-range.data -> store-url-params.data -> url.search -> date-picker-range.start_date -> store-date-picker-range.data
I’ve tried different approaches to decouple the dependencies, such as using dcc.Store
to hold intermediate states and directly handling URL updates via dcc.Location
, but the circular dependency persists.
Current Implementation Overview
- Date Range Picker: Allows users to select a start and end date.
- Issue Type Dropdown: Lets users select multiple issue types (e.g., Feature, Bug).
- Workgroup Dropdown: Lets users filter by specific workgroups.
- Granularity Dropdowns: Control the granularity (daily, weekly, monthly) for the charts.
- URL Sync: Updates the URL parameters based on the selected filters and also reads from the URL to initialize the app.
Callbacks Setup
- Callback 1: Updates
store-date-picker-range
when the date picker changes. - Callback 2: Updates the URL (
url.search
) based on the current state of all filters (including data fromstore-date-picker-range
). - Callback 3: Initializes the app state by reading parameters from
url.search
. - Callback 4: Updates the charts based on the current state of filters (using data from
store-date-picker-range
).
What I’ve Tried
- Removing
dcc.Store
for URL Params: Attempted to remove intermediate stores and directly manage URL handling viadcc.Location
. - Separate Callbacks: Attempted to separate the concerns of reading from and writing to the URL.
Code Example
Here is a simplified version of the problematic part of my app:
import dash
from dash import dcc, html
from dash.dependencies import Input, Output, State
from urllib.parse import urlencode, parse_qs
app = dash.Dash(__name__)
app.layout = html.Div([
dcc.Location(id='url', refresh=False),
dcc.Store(id='store-date-picker-range'),
dcc.DatePickerRange(id='date-picker-range'),
dcc.Dropdown(id='issue-types', options=[...], multi=True),
dcc.Dropdown(id='workgroup-dropdown', options=[...], multi=True),
dcc.Dropdown(id='velocity-granularity', options=[...]),
dcc.Dropdown(id='burnup-granularity', options=[...]),
dcc.Dropdown(id='status-changes-granularity', options=[...]),
dcc.Graph(id='velocity-chart'),
dcc.Graph(id='burnup-chart'),
dcc.Graph(id='status-changes-line-chart'),
])
@app.callback(
Output('store-date-picker-range', 'data'),
[Input('date-picker-range', 'start_date'),
Input('date-picker-range', 'end_date')]
)
def update_date_store(start_date, end_date):
return {'start_date': start_date, 'end_date': end_date}
@app.callback(
Output('url', 'search'),
[Input('velocity-granularity', 'value'),
Input('burnup-granularity', 'value'),
Input('status-changes-granularity', 'value'),
Input('workgroup-dropdown', 'value'),
Input('store-date-picker-range', 'data'),
Input('issue-types', 'value')]
)
def update_url(velocity_granularity, burnup_granularity, status_changes_granularity, selected_workgroups, date_picker_range, issue_types):
params = {
'start_date': date_picker_range['start_date'],
'end_date': date_picker_range['end_date'],
'issue_types': issue_types,
'velocity_granularity': velocity_granularity,
'burnup_granularity': burnup_granularity,
'status_changes_granularity': status_changes_granularity,
'selected_workgroups': selected_workgroups
}
return '?' + urlencode(params, doseq=True)
@app.callback(
[Output('date-picker-range', 'start_date'), Output('date-picker-range', 'end_date'),
Output('issue-types', 'value'), Output('velocity-granularity', 'value'),
Output('burnup-granularity', 'value'), Output('status-changes-granularity', 'value'),
Output('workgroup-dropdown', 'value')],
[Input('url', 'search')]
)
def load_config_from_url(search):
if not search:
raise dash.exceptions.PreventUpdate
params = parse_qs(search.lstrip('?'))
return (
params.get('start_date', [None])[0],
params.get('end_date', [None])[0],
params.get('issue_types', [''])[0],
params.get('velocity_granularity', ['W'])[0],
params.get('burnup_granularity', ['D'])[0],
params.get('status_changes_granularity', ['D'])[0],
params.get('selected_workgroups', [[]])[0] or []
)
@app.callback(
[Output('velocity-chart', 'figure'), Output('burnup-chart', 'figure'),
Output('status-changes-line-chart', 'figure')],
[Input('store-date-picker-range', 'data'),
Input('issue-types', 'value'), Input('velocity-granularity', 'value'),
Input('burnup-granularity', 'value'), Input('status-changes-granularity', 'value'),
Input('workgroup-dropdown', 'value')]
)
def update_charts(date_picker_range, issue_types, velocity_granularity, burnup_granularity, status_changes_granularity, selected_workgroups):
# Logic to update charts based on filters
pass
if __name__ == '__main__':
app.run_server(debug=True)
What I Need Help With
- Breaking the Circular Dependency: How can I restructure my callbacks or state management to avoid the circular dependency issue?
- Best Practices: Are there best practices for handling URL synchronization with state in Dash to prevent such issues?
Any help or suggestions would be greatly appreciated!
Thank you in advance for your support!