Mobile Layout Architecture
page.tsx (Main Component)
├── useIsMobile() hook
│
├── [if mobile]
│ └── MobileLayout
│ ├── Metric Selector Header
│ │ ├── Previous Button (←)
│ │ ├── Current Metric Label
│ │ └── Next Button (→)
│ │
│ └── Cards List (scrollable)
│ ├── Card (Highway 1, County 1)
│ │ ├── Title: Highway + County
│ │ ├── Map Button
│ │ └── MiniSegmentChart (current metric)
│ │
│ ├── Card (Highway 1, County 2)
│ ├── Card (Highway 2, County 1)
│ └── ... (more cards)
│
├── [if charts active]
│ └── ChartsView (full screen overlay)
│ ├── Header
│ │ ├── Back Button
│ │ ├── Title: "PMIS Heat Maps"
│ │ └── Close All Button
│ │
│ ├── Score Gauges (horizontal scroll)
│ │ ├── Gauge 1
│ │ ├── Gauge 2
│ │ └── ...
│ │
│ └── Charts List (vertical scroll)
│ ├── HeatMapModal 1
│ │ ├── Highway Segment Chart
│ │ └── Close Button
│ │
│ ├── HeatMapModal 2
│ └── ...
│
└── MapExplorerModal (when map button clicked)
┌─────────────────────────────────────────────────────────┐
│ page.tsx │
│ ┌────────────────────────────────────────────────┐ │
│ │ 1. Load CSV data (hw_cnty_avg.csv) │ │
│ │ 2. Process features → processedData[] │ │
│ │ 3. Load GeoJSON → availableHighways Set │ │
│ │ 4. Build segment map → segmentDataByHighway │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────┐ │
│ │ TableModalPMIS Component │ │
│ │ (Processes data and calls callbacks) │ │
│ │ │ │
│ │ • onDataProcessed(processedData) │ │
│ │ • onSegmentDataReady(segmentMap) │ │
│ │ • onAvailableHighwaysReady(highways) │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Mobile State Updated │ │
│ │ │ │
│ │ processedData: [{highway, county, scores}] │ │
│ │ segmentDataByHighwayCounty: Map<key, data> │ │
│ │ availableHighways: Set<highway> │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Pass data to MobileLayout as props │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────┐
│ User Opens │
│ Mobile │
└──────┬──────┘
│
▼
┌─────────────────────┐
│ Card List Shows │
│ (Default: Cond.) │
└──────┬──────────────┘
│
├─ Swipe Left ────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Next Metric │ │ Prev Metric │
│ (Distress) │ │ (Cost) │
└──────┬───────┘ └──────┬───────┘
│ │
└──────────┬───────────┘
│
┌──────────┴──────────┐
│ │
Tap Arrow Tap Arrow
Buttons Buttons
┌──────────────┐
│ User Taps │
│ Metric Card │
└──────┬───────┘
│
▼
┌──────────────────────┐
│ handleChartClick() │
│ • Create chart data │
│ • Add to state │
└──────┬───────────────┘
│
▼
┌──────────────────────┐
│ activeHeatMapData │
│ length > 0 │
└──────┬───────────────┘
│
▼
┌──────────────────────┐
│ ChartsView Shows │
│ (Full Screen) │
└──────┬───────────────┘
│
├─── Back Button ────┐
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ Hide Charts │ │ Return to │
│ View │ │ Card List │
└─────────────┘ └──────────────┘
┌──────────────┐
│ User Taps │
│ Map Button │
└──────┬───────┘
│
▼
┌───────────────────────┐
│ handleMobileMapClick()│
│ • Get highway │
│ • Get county │
│ • Get viewType │
└──────┬────────────────┘
│
▼
┌───────────────────────┐
│ showMapModal() │
│ • Set mapModalInfo │
│ • Set mapModalOpen │
└──────┬────────────────┘
│
▼
┌───────────────────────┐
│ MapExplorerModal │
│ (Full Screen) │
└──────┬────────────────┘
│
▼
┌───────────────────────┐
│ User Closes Modal │
│ Returns to Cards │
└───────────────────────┘
// Current metric index (0-4)
const [ currentMetricIndex , setCurrentMetricIndex ] = useState ( 0 )
const METRICS = [
{ field : "TX_CONDITION_SCORE" , label : "Condition Score" } , // 0
{ field : "TX_DISTRESS_SCORE" , label : "Distress Score" } , // 1
{ field : "TX_RIDE_SCORE" , label : "Ride Score" } , // 2
{ field : "TX_AADT_CURRENT" , label : "AADT" } , // 3
{ field : "TX_MAINTENANCE_COST_AMT" , label : "Maintenance Cost" } // 4
]
// Swipe detection
onTouchStart → Store start X position
onTouchMove → Store end X position
onTouchEnd → Calculate distance
├─ If distance > 50 px left → Next metric ( index + 1 ) % 5
└─ If distance > 50 px right → Prev metric ( index - 1 + 5 ) % 5
State 1: Card List (Default)
┌─────────────────────────┐
│ ← Condition Score → │ ← Header
│ Swipe to change (1/5) │
├─────────────────────────┤
│ │
│ ┌───────────────────┐ │
│ │ IH 35 • Travis │ │
│ │ [Map Button] │ │
│ │ ┌───────────────┐ │ │
│ │ │ Chart Preview │ │ │ ← Card 1
│ │ └───────────────┘ │ │
│ └───────────────────┘ │
│ │
│ ┌───────────────────┐ │
│ │ US 290 • Travis │ │
│ │ [Map Button] │ │
│ │ ┌───────────────┐ │ │ ← Card 2
│ │ │ Chart Preview │ │ │
│ │ └───────────────┘ │ │
│ └───────────────────┘ │
│ ... │
└─────────────────────────┘
┌─────────────────────────┐
│ ← PMIS Heat Maps [✕] │ ← Header
├─────────────────────────┤
│ [Gauge1][Gauge2][...] │ ← Score Gauges
├─────────────────────────┤
│ │
│ ┌─────────────────────┐ │
│ │ IH 35 • Travis [✕]│ │
│ │ ┌─────────────────┐ │ │
│ │ │ Highway Chart │ │ │ ← Chart 1
│ │ │ │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ US 290 • Travis [✕]│ │
│ │ ┌─────────────────┐ │ │
│ │ │ Highway Chart │ │ │ ← Chart 2
│ │ │ │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────┘ │
│ ... │
└─────────────────────────┘
State 3: Map Modal (Overlay)
┌─────────────────────────┐
│ PMIS Explorer - IH 35 │
│ [✕] │
├─────────────────────────┤
│ │
│ [Map View] │
│ │
│ Interactive Map │
│ with Highway │
│ Highlighted │
│ │
│ [Year Selector: 2022] │
│ │
└─────────────────────────┘
Mobile: < 768px → MobileLayout
Tablet: 768-1024px → Desktop Layout (grid)
Desktop: > 1024px → Desktop Layout (grid)
Performance Optimizations
React.memo on Card and TableRow components
useMemo for expensive calculations:
Processed data
Segment data mapping
Filtered and sorted data
useCallback for event handlers:
Map clicks
Chart clicks
Remove operations
Lazy loading with dynamic imports:
MobileLayout
HeatMapModal
Score gauges
Chart components
Deferred rendering in MiniSegmentChart to prevent blocking