# Runbook: Adding a New Dashboard This runbook describes how to add a new data dashboard to the portfolio application. ## Prerequisites - [ ] Data sources identified and accessible - [ ] Database schema designed - [ ] Basic Dash/Plotly familiarity ## Directory Structure Create the following structure: ### Application Code (`portfolio_app/`) ``` portfolio_app/ ├── pages/ │ └── {dashboard_name}/ │ ├── dashboard.py # Main layout with tabs │ ├── methodology.py # Data sources and methods page │ ├── tabs/ │ │ ├── __init__.py │ │ ├── overview.py # Overview tab layout │ │ └── ... # Additional tab layouts │ └── callbacks/ │ ├── __init__.py │ └── ... # Callback modules ├── {dashboard_name}/ # Data logic (outside pages/) │ ├── __init__.py │ ├── parsers/ # API/CSV extraction │ │ └── __init__.py │ ├── loaders/ # Database operations │ │ └── __init__.py │ ├── schemas/ # Pydantic models │ │ └── __init__.py │ └── models/ # SQLAlchemy ORM (schema: raw_{dashboard_name}) │ └── __init__.py └── figures/ └── {dashboard_name}/ # Figure factories for this dashboard ├── __init__.py └── ... # Chart modules ``` ### dbt Models (`dbt/models/`) ``` dbt/models/ ├── staging/ │ └── {dashboard_name}/ # Staging models │ ├── _sources.yml # Source definitions (schema: raw_{dashboard_name}) │ ├── _staging.yml # Model tests/docs │ └── stg_*.sql # Staging models ├── intermediate/ │ └── {dashboard_name}/ # Intermediate models │ ├── _intermediate.yml │ └── int_*.sql └── marts/ └── {dashboard_name}/ # Mart tables ├── _marts.yml └── mart_*.sql ``` ### Documentation (`notebooks/`) ``` notebooks/ └── {dashboard_name}/ # Domain subdirectories ├── overview/ ├── ... ``` ## Step-by-Step Checklist ### 1. Data Layer - [ ] Create Pydantic schemas in `{dashboard_name}/schemas/` - [ ] Create SQLAlchemy models in `{dashboard_name}/models/` - [ ] Create parsers in `{dashboard_name}/parsers/` - [ ] Create loaders in `{dashboard_name}/loaders/` - [ ] Add database migrations if needed ### 2. Database Schema - [ ] Define schema constant in models (e.g., `RAW_FOOTBALL_SCHEMA = "raw_football"`) - [ ] Add `__table_args__ = {"schema": RAW_FOOTBALL_SCHEMA}` to all models - [ ] Update `scripts/db/init_schema.py` to create the new schema ### 3. dbt Models Create dbt models in `dbt/models/`: - [ ] `staging/{dashboard_name}/_sources.yml` - Source definitions pointing to `raw_{dashboard_name}` schema - [ ] `staging/{dashboard_name}/stg_{source}__{entity}.sql` - Raw data cleaning - [ ] `intermediate/{dashboard_name}/int_{domain}__{transform}.sql` - Business logic - [ ] `marts/{dashboard_name}/mart_{domain}.sql` - Final analytical tables Update `dbt/dbt_project.yml` with new subdirectory config: ```yaml models: portfolio: staging: {dashboard_name}: +materialized: view +schema: staging ``` Follow naming conventions: - Staging: `stg_{source}__{entity}` - Intermediate: `int_{domain}__{transform}` - Marts: `mart_{domain}` ### 4. Visualization Layer - [ ] Create figure factories in `figures/{dashboard_name}/` - [ ] Create `figures/{dashboard_name}/__init__.py` with exports - [ ] Follow the factory pattern: `create_{chart_type}_figure(data, **kwargs)` Import pattern: ```python from portfolio_app.figures.{dashboard_name} import create_choropleth_figure ``` ### 4. Dashboard Pages #### Main Dashboard (`pages/{dashboard_name}/dashboard.py`) ```python import dash from dash import html, dcc import dash_mantine_components as dmc dash.register_page( __name__, path="/{dashboard_name}", title="{Dashboard Title}", description="{Description}" ) def layout(): return dmc.Container([ # Header dmc.Title("{Dashboard Title}", order=1), # Tabs dmc.Tabs([ dmc.TabsList([ dmc.TabsTab("Overview", value="overview"), # Add more tabs ]), dmc.TabsPanel(overview_tab(), value="overview"), # Add more panels ], value="overview"), ]) ``` #### Tab Layouts (`pages/{dashboard_name}/tabs/`) - [ ] Create one file per tab - [ ] Export layout function from each #### Callbacks (`pages/{dashboard_name}/callbacks/`) - [ ] Create callback modules for interactivity - [ ] Import and register in dashboard.py ### 5. Navigation Add to sidebar in `components/sidebar.py`: ```python dmc.NavLink( label="{Dashboard Name}", href="/{dashboard_name}", icon=DashIconify(icon="..."), ) ``` ### 6. Documentation - [ ] Create methodology page (`pages/{dashboard_name}/methodology.py`) - [ ] Document data sources - [ ] Document transformation logic - [ ] Add notebooks to `notebooks/{dashboard_name}/` if needed ### 7. Testing - [ ] Add unit tests for parsers - [ ] Add unit tests for loaders - [ ] Add integration tests for callbacks - [ ] Run `make test` ### 8. Final Verification - [ ] All pages render without errors - [ ] All callbacks respond correctly - [ ] Data loads successfully - [ ] dbt models run cleanly (`make dbt-run`) - [ ] Linting passes (`make lint`) - [ ] Tests pass (`make test`) ## Example: Toronto Dashboard Reference implementation: `portfolio_app/pages/toronto/` Key files: - `dashboard.py` - Main layout with 5 tabs - `tabs/overview.py` - Livability scores, scatter plots - `callbacks/map_callbacks.py` - Choropleth interactions - `toronto/models/dimensions.py` - Dimension tables - `toronto/models/facts.py` - Fact tables ## Common Patterns ### Figure Factories ```python # figures/choropleth.py def create_choropleth_figure( gdf: gpd.GeoDataFrame, value_column: str, title: str, **kwargs ) -> go.Figure: ... ``` ### Callbacks ```python # callbacks/map_callbacks.py @callback( Output("neighbourhood-details", "children"), Input("choropleth-map", "clickData"), ) def update_details(click_data): ... ``` ### Data Loading ```python # {dashboard_name}/loaders/load.py def load_data(session: Session) -> None: # Parse from source records = parse_source_data() # Validate with Pydantic validated = [Schema(**r) for r in records] # Load to database for record in validated: session.add(Model(**record.model_dump())) session.commit() ```