Skip to content

Funnel view

Executive Summary

The goal of this project is to add a funnel visualization to the existing recruiting page to help users understand patient attrition through protocol criteria. This feature will complement the current table view by providing a clear visualization of how each criterion impacts the eligible patient pool. The implementation will maintain existing patterns and state management while adding the new visualization option.

Motivation and Goals

Key Objectives

  • Add funnel visualization to existing recruiting page
  • Maintain current protocol selection and criteria panel functionality
  • Provide clear visualization of patient attrition
  • Keep consistent UX with existing features

Benefits

  • The filter view clearly shows the impact of each criterion
  • Provides quick identification of restrictive criteria, and better visibility into how criteria affect patient eligibility

Non-Goals

  • Other changes related to v0.2:
  • Custom Criteria
  • Navigation redesign

Proposal

Core Features

  • Simple view toggle between:
  • Existing table view
  • New funnel visualization
  • Statistics summarizing the applied criteria
  • Total number of patients considered
  • Criteria applied (out of the total available criteria)
  • Sites selected (out of the total number of sites)
  • Patients Matching (total number and percentage of total patients)
  • Funnel visualization showing:
  • Total initial patient pool
  • Patient count after each criterion
  • Percentage remaining at each step
  • Number excluded by each criterion

Design and Implementation Details

Backend Implementation

The backend changes require adding a new endpoint to calculate funnel statistics for a given protocol. This will leverage existing database models but introduce new computation logic for tracking patient attrition through each criterion.

API Endpoint

  • GET /protocols/{protocol_id}/funnel
  • Uses existing database models
  • Iteratively calculates exclusions relative to remaining patient pool
  • Fetch initial patient pool for the given protocol
  • Fetch currently active criteria for the given protocol
  • Iterate through criteria and calculate exclusion statistics for each criterion applied
    # API Interface

    class CriteriaFunnelStats(BaseModel):

        criteria_id: int

        criteria_summary: str

        criteria_type: CriteriaType

        patients_excluded: int

        patients_remaining: int


    class ProtocolFunnelStats(BaseModel):

        total_patients: int

        final_matches: int

        criteria_stats: List[CriteriaFunnelStats]


    @router.get(
        "/protocols/{protocol_id}/funnel",
        response_model=ProtocolFunnelStats,
        response_model_exclude_none=True,
    )
    async def get_protocol_funnel(
        protocol_id: int,
        service: ProtocolsService = Depends(get_protocols_service),
    ) -> ProtocolFunnelStats:
        """Get funnel statistics for a protocol."""
        return service.get_protocol_funnel(protocol_id)
    # API Implementation

    def get_protocol_funnel(self, protocol_id: int) -> ProtocolFunnelStats:
        remaining_patient_ids = # Get initial patient pool for the protocol
        criteria_list = # Get list of active criteria for the protocol
        total_patients = len(remaining_patient_ids)
        criteria_stats = []

        for criterion in criteria_list:
            failing_patient_ids = # checks patients still in the remaining pool that fail this criterion

            # Remove failing patients from remaining pool
            remaining_patient_ids -= failing_patient_ids

            excluded_count = len(failing_patient_ids)
            remaining_count = len(remaining_patient_ids)
            percentage_remaining = (remaining_count / total_patients * 100) if total_patients > 0 else 0

            criteria_stats.append(CriteriaFunnelStats(
                criteria_id=criterion.id,
                criteria_summary=criterion.summary,
                criteria_type=criterion.type,
                patients_excluded=excluded_count,
                patients_remaining=remaining_count,
            ))

        return ProtocolFunnelStats(
            total_patients=total_patients,
            final_matches=len(remaining_patient_ids),
            criteria_stats=criteria_stats
        )

Frontend Implementation

The frontend changes introduce a new visualization option while maintaining the existing table view and criteria panel functionality. This implementation focuses on maintaining existing patterns while adding the new funnel view.

State Management

Option 1: Local State (Current Implementation)
- Keep existing protocol selection state
- Add simple view toggle state
- Use existing SWR pattern for fetching and caching funnel data
- Maintains current patterns
- Simpler implementation
Option 2: URL-based State
- Store protocol ID in URL
- Individual pages for table and funnel view
    - /recruiting/:protocolId/table
    - /recruiting/:protocolId/funnel
- Can bookmark/share more easily
- Would require more restructuring
Recommendation

I would recommend Option 1 for the following reasons:

  • Matches existing implementation
  • Simpler to implement
  • Can be easily refactored later if needed

Data Flow

  • Protocol selection drives both views
  • Criteria updates affect both visualizations

Use existing external state management pattern to fetch and cache data:

const useProtocolFunnel = (protocolId?: number) => {
  const { data: funnelData, mutate, ...rest } = useSWR<ProtocolFunnel>(
    protocolId ? `/protocols/${protocolId}/funnel` : null
  )

  return {
    funnelData,
    ...rest,
  }
}

And update funnel data when criteria is changed:

const {
    funnelData,
    isLoading: isLoadingFunnel,
    mutate: mutateFunnel,
    error: funnelError,
} = useProtocolFunnel(selectedProtocolId)

...

const handleCriterionToggle = () => {
  mutateCriteria()
  mutatePatients()
  mutateFunnel()
}
graph TD
    %% Components
    RP[RecruitingPage]

    subgraph "Shared Components"
        PS[Protocol Selection]
        CP[Criteria Panel]
    end

    subgraph "Views"
        TV[Table View]
        FV[Funnel View]
    end

    subgraph "Data Hooks"
        PH["useProtocols()"]
        CH["useProtocolCriteria()"]
        TH["useProtocolMatches()"]
        FH["useProtocolFunnel()"]
    end

    subgraph "API Endpoints"
        PE["/protocols"]
        CE["/protocols/{id}/criteria"]
        TE["/protocols/{id}/matches"]
        FE["/protocols/{id}/funnel"]
    end

    RP --> PS
    RP --> CP
    RP --> TV
    RP --> FV

    PS --> PH
    CP --> CH
    TV --> TH
    FV --> FH

    PH --> PE
    CH --> CE
    TH --> TE
    FH --> FE

    class RP,PS,CP,TV,FV component
    class PH,CH,TH,FH hook
    class PE,CE,TE,FE api