Source: js/seasonal-patterns-dashboard.js

/**
 * @module TemporalIntelligence/SeasonalAnalysis
 * @category Intelligence Analysis - Seasonal Parliamentary Patterns & Anomaly Detection
 * 
 * @description
 * **Swedish Parliamentary Seasonal Activity Analysis & Quarterly Pattern Intelligence Dashboard**
 * 
 * Advanced intelligence analysis platform implementing **23-year temporal pattern analysis**
 * (2002-2025) of Swedish parliamentary quarterly activity with sophisticated Z-score anomaly 
 * detection and seasonal pattern classification. Identifies systematic quarterly variations,
 * detects activity anomalies, and classifies seasonal patterns through multi-year aggregation
 * and cross-quarter comparative analysis using D3.js and Chart.js visualization.
 * 
 * ## Intelligence Methodology
 * 
 * This module implements **temporal pattern intelligence analysis**:
 * - **Historical Scope**: 23 years × 4 quarters = 92 quarterly data points
 * - **Analysis Approach**: Seasonal decomposition with Z-score anomaly detection
 * - **Granularity**: Quarterly activity aggregation across parliamentary entities
 * - **Anomaly Threshold**: Z-score ≥ 2.0 (p<0.05) for statistical significance
 * 
 * ## Seasonal Intelligence Framework
 * 
 * **Four-Dimensional Analysis Taxonomy**:
 * 
 * 1. **Quarterly Activity Patterns** (Seasonal Decomposition)
 *    - Q1 (January-March): Spring session, budget discussions beginning
 *    - Q2 (April-June): Legislative focus, committee work intensification
 *    - Q3 (July-September): Summer recess, reduced activity baseline
 *    - Q4 (October-December): Fall session, pre-election activity surge
 *    - Activity types: Ballots, documents, committee decisions, attendance
 * 
 * 2. **Z-Score Anomaly Detection** (Statistical Outlier Identification)
 *    - Baseline: Mean and standard deviation per quarter (23 years)
 *    - Z-score calculation: (Value - Mean) / StdDev
 *    - Anomaly threshold: |Z-score| ≥ 2.0 (95% confidence)
 *    - Classification: Normal, Elevated, Anomaly, Critical
 * 
 * 3. **Seasonal Pattern Classification** (Behavioral Categorization)
 *    - Normal: Activity within ±1 StdDev of quarterly average
 *    - Elevated: Activity 1-2 StdDev above/below average
 *    - Anomaly: Activity >2 StdDev from average (statistical outlier)
 *    - Critical: Extreme anomaly >3 StdDev (very rare events)
 * 
 * 4. **Cross-Year Quarter Comparison** (Temporal Consistency)
 *    - Year-over-year quarter consistency analysis
 *    - Trend identification within same quarter across years
 *    - Activity volatility assessment by quarter
 *    - Quarter-to-quarter transition patterns
 * 
 * ## Data Sources (CIA Platform)
 * 
 * **Primary Intelligence Feeds**:
 * - `view_riksdagen_seasonal_activity_patterns_sample.csv`
 *   * Fields: year, quarter, ballot_count, document_count, decision_count, attendance_rate, 
 *            avg_speech_length, committee_meetings, anomaly_score, anomaly_class
 *   * Scope: 23 years (2002-2025) × 4 quarters = 92 quarterly records
 *   * Use: Seasonal pattern baseline, anomaly detection, quarterly benchmarking
 *   * Coverage: Full spectrum of parliamentary activity metrics
 * 
 * ## OSINT Collection Strategy
 * 
 * **Temporal Pattern Intelligence**:
 * 1. **Parliamentary Activity Tracking**: Vote counts, document filings, committee meetings
 * 2. **Attendance Intelligence**: Member participation rates, session attendance patterns
 * 3. **Speech Analytics**: Contribution frequency, speech length, rhetoric intensity
 * 4. **Calendar Intelligence**: Recess periods, session schedules, emergency sessions
 * 5. **Budget Cycles**: Fiscal year boundaries and budget discussion phases
 * 6. **Electoral Calendars**: Election-adjacent activity surge patterns
 * 7. **Government Transitions**: Cabinet change and policy uncertainty impact on activity
 * 
 * ## Visualization Intelligence
 * 
 * **D3.js Quarterly Heat Map** (Primary):
 * - **23×4 Matrix Visualization**: Years (Y-axis) × Quarters (X-axis)
 *   * Each cell represents quarterly activity level
 *   * Color intensity: Activity magnitude (blue/low → red/high)
 *   * Color saturation: Anomaly magnitude (white/normal → black/critical)
 *   * Interactive: Tooltip reveals detailed metrics (count, Z-score, classification)
 *   * Scrollable: 23 years with year labels and Q1-Q4 grid
 *   * Sortable: By activity level, anomaly score, or time sequence
 * 
 * **Chart.js Seasonal Decomposition** (Pattern Analysis):
 * - **Box-and-Whisker Plot** by quarter across 23 years:
 *   * X-axis: 4 quarters (Q1, Q2, Q3, Q4)
 *   * Y-axis: Activity metric value
 *   * Box: Interquartile range (25th-75th percentile)
 *   * Line: Median (50th percentile)
 *   * Whiskers: 1.5×IQR range
 *   * Points: Outliers beyond whiskers (statistical anomalies)
 *   * Shows quarterly pattern consistency and outlier years
 * 
 * **Chart.js Anomaly Timeline** (Outlier Tracking):
 * - **Anomaly Score Time Series**: Z-scores for all quarters
 *   * Multi-line chart with threshold bands
 *   * Separate lines for different activity types
 *   * Horizontal reference lines at Z=0, Z=2, Z=-2, Z=3, Z=-3
 *   * Color-coded zones: Green (normal), Yellow (elevated), Red (anomaly)
 *   * Identifies anomalous quarters and their characteristics
 * 
 * **Chart.js Quarter Comparison** (Relative Strength):
 * - **Average Activity by Quarter**: Multi-year aggregated pattern
 *   * Bar chart showing Q1, Q2, Q3, Q4 average activities
 *   * Error bars showing ±1 StdDev confidence bands
 *   * Shows expected seasonal variation
 *   * Identifies which quarters are typically high/low activity
 * 
 * **Chart.js Activity Quartile Distribution** (Ranking):
 * - **Quartile Membership Heat Map**: Each quarter's historical ranking
 *   * Shows frequency of each quarter landing in quartiles
 *   * Q1 column: How often Q1s rank in top/bottom quartiles
 *   * Helps identify most/least reliable quarters
 * 
 * ## Intelligence Analysis Frameworks Applied
 * 
 * @intelligence
 * - **Temporal Decomposition**: Separating trend, seasonal, and anomaly components
 * - **Statistical Anomaly Detection**: Z-score methodology with confidence intervals
 * - **Seasonal Pattern Classification**: Normal/elevated/anomaly/critical taxonomy
 * - **Year-Over-Year Analysis**: Consistency assessment and trend identification
 * - **Outlier Investigation**: Identification of anomalous quarters and causes
 * 
 * @osint
 * - **Activity Intelligence**: Real-time parliamentary activity monitoring
 * - **Pattern Recognition**: Historical seasonal signatures and exception detection
 * - **Confidence Quantification**: Statistical bounds on anomaly identification
 * - **Root Cause Analysis**: Linking anomalies to external events (elections, crises)
 * 
 * @risk
 * - **Seasonal Activity Disruption**: Anomalous activity surge/decline risks
 * - **Summer Recess Risk**: Reduced oversight during Q3 periods
 * - **Pre-Election Volatility**: Q4 election-year activity unpredictability
 * - **Activity Concentration Risk**: Uneven quarterly distribution affecting oversight
 * 
 * ## GDPR Compliance
 * 
 * @gdpr Seasonal activity analysis uses only aggregate parliamentary data (Article 9(2)(e)):
 * - Aggregate activity counts (ballots, documents, decisions)
 * - No individual parliamentary member tracking
 * - No personal behavioral data or voting pattern surveillance
 * - No content analysis of speeches or documents
 * - Purely temporal pattern analysis of aggregate activity volumes
 * 
 * ## Security Architecture
 * 
 * @security D3.js SVG rendering with input sanitization on all text labels
 * @security Chart.js with XSS-safe tooltip content and axis labels
 * @security All CSV data validated with type checking and range enforcement
 * @security Z-score calculation algorithm transparent and reproducible
 * @security No authentication required; all data is public record
 * @risk Low - Temporal activity patterns are public aggregate data
 * 
 * ## Performance Characteristics
 * 
 * - **Data Volume**: 23 years × 4 quarters × 6-8 metrics = ~552-736 data points
 * - **Rendering**: D3.js heat map (23×4 = 92 cells) + Chart.js (5 visualizations)
 * - **Calculations**: Z-scores, percentiles, quartile calculations
 * - **Memory**: <1.5MB for complete seasonal analysis dataset
 * - **Cache Duration**: 24-hour expiry; weekly updates typical
 * 
 * ## Data Transformation Pipeline
 * 
 * **Load Strategy**:
 * 1. Attempt local cache load (`cia-data/seasonal/`)
 * 2. Parse CSV file into quarterly time-series structure
 * 3. Fallback to remote GitHub repository if local unavailable
 * 4. Calculate Z-scores for each metric by quarter
 * 5. Classify quarters: Normal/Elevated/Anomaly/Critical
 * 6. Compute quarterly aggregates (means, std dev) across 23 years
 * 7. Cache results with 24-hour expiry
 * 8. Render visualizations with aggregated/transformed data
 * 
 * **Statistical Processing**:
 * - **Per-Quarter Statistics**: Mean and StdDev for each Q1/Q2/Q3/Q4 across 23 years
 * - **Z-Score**: (Current_Value - Quarter_Mean) / Quarter_StdDev
 * - **Percentile**: Ranking within historical distribution for that quarter
 * - **Anomaly Class**: Normal (|Z|<1), Elevated (1≤|Z|<2), Anomaly (|Z|≥2)
 * - **Confidence**: 95% confidence interval on quarterly means
 * 
 * ## Anomaly Thresholds
 * 
 * **Classification Levels**:
 * - **Normal**: Z-score between -1.0 and +1.0 (68% of data expected)
 * - **Elevated**: Z-score between -2.0 and -1.0 OR +1.0 and +2.0 (27% of data)
 * - **Anomaly**: Z-score < -2.0 OR > +2.0 (5% of data, statistical outliers)
 * - **Critical**: Z-score < -3.0 OR > +3.0 (<1% of data, extreme events)
 * 
 * **Activity-Specific Thresholds**:
 * - **Ballots**: >500 votes per quarter = high activity, <200 = low activity
 * - **Documents**: >150 documents per quarter = high legislative activity
 * - **Decisions**: >80 committee decisions per quarter = normal workload
 * - **Attendance**: >85% average attendance = high participation, <75% = concerning
 * 
 * @author Hack23 AB - Temporal Intelligence Team
 * @license Apache-2.0
 * @version 1.0.0
 * @since 2024
 * 
 * @see {@link https://github.com/Hack23/cia|CIA Platform Data Source}
 * @see {@link https://data.riksdagen.se|Riksdag Open Data API}
 * @see {@link ./THREAT_MODEL.md|Threat Model Documentation}
 * @see {@link ./SECURITY_ARCHITECTURE.md|Security Architecture}
 */
(function() {
  'use strict';

  // ============================================================================
  // Configuration
  // ============================================================================
  
  const CONFIG = {
    dataUrls: [
      'cia-data/seasonal/view_riksdagen_seasonal_activity_patterns_sample.csv', // Local first
      'https://raw.githubusercontent.com/Hack23/cia/master/service.data.impl/sample-data/view_riksdagen_seasonal_activity_patterns_sample.csv' // Remote fallback
    ],
    cacheKey: 'riksdag_seasonal_patterns',
    cacheDuration: 24 * 60 * 60 * 1000, // 24 hours in milliseconds
    zScoreThreshold: 2.0, // Anomaly threshold
    colors: {
      primary: '#00d9ff',
      secondary: '#ff006e',
      tertiary: '#ffbe0b',
      success: '#008838',
      warning: '#fbc02d',
      danger: '#d32f2f',
      info: '#117a8b',
      normal: '#388e3c',
      elevated: '#f57c00',
      reduced: '#1976d2',
      anomaly: '#d32f2f'
    },
    quarterColors: {
      Q1: '#1976d2', // Blue - Winter
      Q2: '#388e3c', // Green - Spring
      Q3: '#fbc02d', // Yellow - Summer Recess
      Q4: '#f57c00'  // Orange - Autumn
    }
  };

  // ============================================================================
  // Translations (14 Languages)
  // ============================================================================
  
  const TRANSLATIONS = {
    en: {
      title: 'Seasonal Activity Patterns (2002-2025)',
      subtitle: 'Quarterly Analysis with Z-Score Anomaly Detection',
      filters: {
        year: 'Year',
        quarter: 'Quarter',
        election: 'Election Status',
        classification: 'Activity Classification',
        allYears: 'All Years',
        allQuarters: 'All Quarters',
        allElections: 'All',
        electionYears: 'Election Years',
        nonElectionYears: 'Non-Election Years',
        allClassifications: 'All Classifications'
      },
      quarters: {
        Q1: 'Q1 - Winter Session',
        Q2: 'Q2 - Spring Session',
        Q3: 'Q3 - Summer Recess',
        Q4: 'Q4 - Autumn Session'
      },
      charts: {
        heatmap: {
          title: 'Quarterly Activity Heat Map (2002-2025)',
          description: 'Ballot volume by year and quarter with Z-score overlay'
        },
        zscore: {
          title: 'Z-Score Anomaly Detection',
          description: 'Statistical outliers (|Z| ≥ 2.0) flagged in red'
        },
        comparison: {
          title: 'Average Activity by Quarter (All Years)',
          description: 'Q1-Q4 baselines with standard deviation bands'
        },
        classification: {
          title: 'Seasonal Pattern Classification',
          description: 'Distribution of NORMAL, ELEVATED, REDUCED, ANOMALY patterns'
        },
        qoq: {
          title: 'Quarter-over-Quarter Changes',
          description: 'Sequential ballot changes (% and absolute)'
        }
      },
      classifications: {
        NORMAL_ACTIVITY: 'Normal Activity',
        ELEVATED_ACTIVITY: 'Elevated Activity',
        REDUCED_ACTIVITY: 'Reduced Activity',
        ANOMALY_DETECTED: 'Anomaly Detected',
        NORMAL_SEASONAL_PATTERN: 'Normal Seasonal Pattern',
        Q3_SUMMER_LULL: 'Q3 Summer Lull',
        Q4_ELEVATED_ACTIVITY: 'Q4 Elevated Activity',
        UNUSUALLY_HIGH_ACTIVITY: 'Unusually High Activity',
        UNUSUALLY_LOW_ACTIVITY: 'Unusually Low Activity'
      },
      tooltips: {
        ballots: 'Ballots',
        zScore: 'Z-Score',
        classification: 'Classification',
        anomaly: 'ANOMALY',
        na: 'N/A',
        quarter: 'Quarter',
        year: 'Year'
      },
      chartLabels: {
        ballotZScore: 'Ballot Z-Score',
        documentZScore: 'Document Z-Score',
        attendanceZScore: 'Attendance Z-Score',
        yearQuarter: 'Year-Quarter',
        zScore: 'Z-Score',
        quarter: 'Quarter',
        averageBallots: 'Average Ballots',
        year: 'Year',
        count: 'Count',
        changePercent: 'Change (%)',
        qoqChange: 'QoQ Change (%)',
        anomaly: 'ANOMALY'
      },
      loading: 'Loading data...',
      error: 'Error loading data. Please try again.',
      dataAttribution: 'Data by CIA Platform'
    },
    sv: {
      title: 'Säsongsmönster (2002-2025)',
      subtitle: 'Kvartalsanalys med Z-poäng anomalidetektering',
      filters: {
        year: 'År',
        quarter: 'Kvartal',
        election: 'Valstatus',
        classification: 'Aktivitetsklassificering',
        allYears: 'Alla år',
        allQuarters: 'Alla kvartal',
        allElections: 'Alla',
        electionYears: 'Valår',
        nonElectionYears: 'Icke-valår',
        allClassifications: 'Alla klassificeringar'
      },
      quarters: {
        Q1: 'Q1 - Vintersession',
        Q2: 'Q2 - Vårsession',
        Q3: 'Q3 - Sommaruppehåll',
        Q4: 'Q4 - Höstsession'
      },
      charts: {
        heatmap: {
          title: 'Kvartalsaktivitet värmekarta (2002-2025)',
          description: 'Omröstningsvolym per år och kvartal med Z-poäng'
        },
        zscore: {
          title: 'Z-poäng anomalidetektering',
          description: 'Statistiska avvikelser (|Z| ≥ 2.0) markerade i rött'
        },
        comparison: {
          title: 'Genomsnittlig aktivitet per kvartal (alla år)',
          description: 'Q1-Q4 baslinjer med standardavvikelseband'
        },
        classification: {
          title: 'Säsongsmönster klassificering',
          description: 'Fördelning av NORMAL, FÖRHÖJD, REDUCERAD, ANOMALI mönster'
        },
        qoq: {
          title: 'Kvartal-till-kvartal förändringar',
          description: 'Sekventiella omröstningsförändringar (% och absolut)'
        }
      },
      classifications: {
        NORMAL_ACTIVITY: 'Normal aktivitet',
        ELEVATED_ACTIVITY: 'Förhöjd aktivitet',
        REDUCED_ACTIVITY: 'Reducerad aktivitet',
        ANOMALY_DETECTED: 'Anomali upptäckt',
        NORMAL_SEASONAL_PATTERN: 'Normalt säsongsmönster',
        Q3_SUMMER_LULL: 'Q3 sommaruppehåll',
        Q4_ELEVATED_ACTIVITY: 'Q4 förhöjd aktivitet',
        UNUSUALLY_HIGH_ACTIVITY: 'Ovanligt hög aktivitet',
        UNUSUALLY_LOW_ACTIVITY: 'Ovanligt låg aktivitet'
      },
      tooltips: {
        ballots: 'Omröstningar',
        zScore: 'Z-poäng',
        classification: 'Klassificering',
        anomaly: 'ANOMALI',
        na: 'Saknas',
        quarter: 'Kvartal',
        year: 'År'
      },
      chartLabels: {
        ballotZScore: 'Omröstningar Z-poäng',
        documentZScore: 'Dokument Z-poäng',
        attendanceZScore: 'Närvaro Z-poäng',
        yearQuarter: 'År-Kvartal',
        zScore: 'Z-poäng',
        quarter: 'Kvartal',
        averageBallots: 'Genomsnittliga omröstningar',
        year: 'År',
        count: 'Antal',
        changePercent: 'Förändring (%)',
        qoqChange: 'KtK-förändring (%)',
        anomaly: 'ANOMALI'
      },
      loading: 'Laddar data...',
      error: 'Fel vid inläsning av data. Försök igen.',
      dataAttribution: 'Data från CIA-plattformen'
    },
    da: {
      title: 'Sæsonmønstre (2002-2025)',
      subtitle: 'Kvartalsanalyse med Z-score anomalidetektion',
      filters: {
        year: 'År',
        quarter: 'Kvartal',
        election: 'Valgstatus',
        classification: 'Aktivitetsklassificering',
        allYears: 'Alle år',
        allQuarters: 'Alle kvartaler',
        allElections: 'Alle',
        electionYears: 'Valgår',
        nonElectionYears: 'Ikke-valgår',
        allClassifications: 'Alle klassificeringer'
      },
      quarters: {
        Q1: 'K1 - Vintersession',
        Q2: 'K2 - Forårssession',
        Q3: 'K3 - Sommerpause',
        Q4: 'K4 - Efterårssession'
      },
      charts: {
        heatmap: {
          title: 'Kvartalsaktivitet varmekort (2002-2025)',
          description: 'Afstemningsvolumen efter år og kvartal med Z-score'
        },
        zscore: {
          title: 'Z-score anomalidetektion',
          description: 'Statistiske afvigelser (|Z| ≥ 2.0) markeret med rødt'
        },
        comparison: {
          title: 'Gennemsnitlig aktivitet efter kvartal (alle år)',
          description: 'K1-K4 basislinjer med standardafvikelsesbånd'
        },
        classification: {
          title: 'Sæsonmønster klassificering',
          description: 'Fordeling af NORMAL, FORHØJET, REDUCERET, ANOMALI mønstre'
        },
        qoq: {
          title: 'Kvartal-til-kvartal ændringer',
          description: 'Sekventielle afstemningsændringer (% og absolut)'
        }
      },
      classifications: {
        NORMAL_ACTIVITY: 'Normal aktivitet',
        ELEVATED_ACTIVITY: 'Forhøjet aktivitet',
        REDUCED_ACTIVITY: 'Reduceret aktivitet',
        ANOMALY_DETECTED: 'Anomali opdaget',
        NORMAL_SEASONAL_PATTERN: 'Normalt sæsonmønster',
        Q3_SUMMER_LULL: 'K3 sommerpause',
        Q4_ELEVATED_ACTIVITY: 'K4 forhøjet aktivitet',
        UNUSUALLY_HIGH_ACTIVITY: 'Usædvanligt høj aktivitet',
        UNUSUALLY_LOW_ACTIVITY: 'Usædvanligt lav aktivitet'
      },
      tooltips: {
        ballots: 'Afstemninger',
        zScore: 'Z-score',
        classification: 'Klassificering',
        anomaly: 'ANOMALI',
        na: 'Mangler',
        quarter: 'Kvartal',
        year: 'År'
      },
      chartLabels: {
        ballotZScore: 'Afstemninger Z-score',
        documentZScore: 'Dokument Z-score',
        attendanceZScore: 'Fremmøde Z-score',
        yearQuarter: 'År-Kvartal',
        zScore: 'Z-score',
        quarter: 'Kvartal',
        averageBallots: 'Gennemsnitlige afstemninger',
        year: 'År',
        count: 'Antal',
        changePercent: 'Ændring (%)',
        qoqChange: 'KtK-ændring (%)',
        anomaly: 'ANOMALI'
      },
      loading: 'Indlæser data...',
      error: 'Fejl ved indlæsning af data. Prøv igen.',
      dataAttribution: 'Data fra CIA-platformen'
    },
    // Additional languages with full translations
    no: { title: 'Sesongmønstre (2002-2025)', subtitle: 'Kvartalsanalyse med Z-score anomalideteksjon', filters: { year: 'År', quarter: 'Kvartal', election: 'Valgstatus', classification: 'Aktivitetsklassifisering', allYears: 'Alle år', allQuarters: 'Alle kvartaler', allElections: 'Alle', electionYears: 'Valgår', nonElectionYears: 'Ikke-valgår', allClassifications: 'Alle klassifiseringer' }, quarters: { Q1: 'K1 - Vintersesjon', Q2: 'K2 - Vårsesjon', Q3: 'K3 - Sommerferie', Q4: 'K4 - Høstsesjon' }, charts: { heatmap: { title: 'Kvartalsaktivitet varmekart (2002-2025)', description: 'Avstemningsvolum etter år og kvartal med Z-score' }, zscore: { title: 'Z-score anomalideteksjon', description: 'Statistiske avvik (|Z| ≥ 2.0) markert i rødt' }, comparison: { title: 'Gjennomsnittlig aktivitet etter kvartal (alle år)', description: 'K1-K4 basislinjer med standardavviksbånd' }, classification: { title: 'Sesongmønster klassifisering', description: 'Fordeling av NORMAL, FORHØYET, REDUSERT, ANOMALI mønstre' }, qoq: { title: 'Kvartal-til-kvartal endringer', description: 'Sekvensielle avstemningsendringer (% og absolutt)' } }, classifications: { NORMAL_ACTIVITY: 'Normal aktivitet', ELEVATED_ACTIVITY: 'Forhøyet aktivitet', REDUCED_ACTIVITY: 'Redusert aktivitet', ANOMALY_DETECTED: 'Anomali oppdaget', NORMAL_SEASONAL_PATTERN: 'Normalt sesongmønster', Q3_SUMMER_LULL: 'K3 sommerferie', Q4_ELEVATED_ACTIVITY: 'K4 forhøyet aktivitet', UNUSUALLY_HIGH_ACTIVITY: 'Uvanlig høy aktivitet', UNUSUALLY_LOW_ACTIVITY: 'Uvanlig lav aktivitet' }, loading: 'Laster data...', error: 'Feil ved lasting av data. Prøv igjen.', dataAttribution: 'Data fra CIA-plattformen' },
    fi: { title: 'Kausivaihtelut (2002-2025)', subtitle: 'Neljännesvuosi-analyysi Z-pisteiden poikkeamatunnistuksella', filters: { year: 'Vuosi', quarter: 'Kvartaali', election: 'Vaalitilanne', classification: 'Aktiviteettiluokitus', allYears: 'Kaikki vuodet', allQuarters: 'Kaikki kvartaalit', allElections: 'Kaikki', electionYears: 'Vaalivuodet', nonElectionYears: 'Ei-vaalivuodet', allClassifications: 'Kaikki luokitukset' }, quarters: { Q1: 'Q1 - Talviistunto', Q2: 'Q2 - Kevätistunto', Q3: 'Q3 - Kesätauko', Q4: 'Q4 - Syysistunto' }, charts: { heatmap: { title: 'Neljännesvuosi-aktiviteetti lämpökartta (2002-2025)', description: 'Äänestysvolyymi vuoden ja kvartaalin mukaan Z-pisteillä' }, zscore: { title: 'Z-piste poikkeamatunnistus', description: 'Tilastolliset poikkeamat (|Z| ≥ 2.0) merkitty punaisella' }, comparison: { title: 'Keskimääräinen aktiviteetti kvartaaleittain (kaikki vuodet)', description: 'Q1-Q4 perusviivat keskihajontakaistaleilla' }, classification: { title: 'Kausivaihtelujen luokittelu', description: 'NORMAALI, KOHONNUT, ALENTUNUT, POIKKEAMA -mallien jakauma' }, qoq: { title: 'Kvartaalista toiseen muutokset', description: 'Peräkkäiset äänestysmuutokset (% ja absoluuttinen)' } }, classifications: { NORMAL_ACTIVITY: 'Normaali aktiviteetti', ELEVATED_ACTIVITY: 'Kohonnut aktiviteetti', REDUCED_ACTIVITY: 'Alentunut aktiviteetti', ANOMALY_DETECTED: 'Poikkeama havaittu', NORMAL_SEASONAL_PATTERN: 'Normaali kausimalli', Q3_SUMMER_LULL: 'Q3 kesätauko', Q4_ELEVATED_ACTIVITY: 'Q4 kohonnut aktiviteetti', UNUSUALLY_HIGH_ACTIVITY: 'Epätavallisen korkea aktiviteetti', UNUSUALLY_LOW_ACTIVITY: 'Epätavallisen matala aktiviteetti' }, loading: 'Ladataan tietoja...', error: 'Virhe tietojen lataamisessa. Yritä uudelleen.', dataAttribution: 'Data CIA-alustalta' },
    de: { title: 'Saisonale Muster (2002-2025)', subtitle: 'Quartalsanalyse mit Z-Score-Anomalieerkennung', filters: { year: 'Jahr', quarter: 'Quartal', election: 'Wahlstatus', classification: 'Aktivitätsklassifizierung', allYears: 'Alle Jahre', allQuarters: 'Alle Quartale', allElections: 'Alle', electionYears: 'Wahljahre', nonElectionYears: 'Nicht-Wahljahre', allClassifications: 'Alle Klassifizierungen' }, quarters: { Q1: 'Q1 - Wintersitzung', Q2: 'Q2 - Frühjahrssitzung', Q3: 'Q3 - Sommerpause', Q4: 'Q4 - Herbstsitzung' }, charts: { heatmap: { title: 'Quartalsaktivität Heatmap (2002-2025)', description: 'Abstimmungsvolumen nach Jahr und Quartal mit Z-Score' }, zscore: { title: 'Z-Score-Anomalieerkennung', description: 'Statistische Ausreißer (|Z| ≥ 2.0) rot markiert' }, comparison: { title: 'Durchschnittliche Aktivität nach Quartal (alle Jahre)', description: 'Q1-Q4 Basislinien mit Standardabweichungsbändern' }, classification: { title: 'Saisonale Musterklassifizierung', description: 'Verteilung von NORMAL, ERHÖHT, REDUZIERT, ANOMALIE Mustern' }, qoq: { title: 'Quartal-zu-Quartal Änderungen', description: 'Aufeinanderfolgende Abstimmungsänderungen (% und absolut)' } }, classifications: { NORMAL_ACTIVITY: 'Normale Aktivität', ELEVATED_ACTIVITY: 'Erhöhte Aktivität', REDUCED_ACTIVITY: 'Reduzierte Aktivität', ANOMALY_DETECTED: 'Anomalie erkannt', NORMAL_SEASONAL_PATTERN: 'Normales saisonales Muster', Q3_SUMMER_LULL: 'Q3 Sommerpause', Q4_ELEVATED_ACTIVITY: 'Q4 erhöhte Aktivität', UNUSUALLY_HIGH_ACTIVITY: 'Ungewöhnlich hohe Aktivität', UNUSUALLY_LOW_ACTIVITY: 'Ungewöhnlich niedrige Aktivität' }, loading: 'Daten werden geladen...', error: 'Fehler beim Laden der Daten. Bitte versuchen Sie es erneut.', dataAttribution: 'Daten von CIA-Plattform' },
    fr: { title: 'Schémas saisonniers (2002-2025)', subtitle: 'Analyse trimestrielle avec détection d\'anomalies par score Z', filters: { year: 'Année', quarter: 'Trimestre', election: 'Statut électoral', classification: 'Classification d\'activité', allYears: 'Toutes les années', allQuarters: 'Tous les trimestres', allElections: 'Tous', electionYears: 'Années électorales', nonElectionYears: 'Années non-électorales', allClassifications: 'Toutes les classifications' }, quarters: { Q1: 'T1 - Session d\'hiver', Q2: 'T2 - Session de printemps', Q3: 'T3 - Pause estivale', Q4: 'T4 - Session d\'automne' }, charts: { heatmap: { title: 'Carte de chaleur d\'activité trimestrielle (2002-2025)', description: 'Volume de scrutins par année et trimestre avec score Z' }, zscore: { title: 'Détection d\'anomalies par score Z', description: 'Valeurs aberrantes statistiques (|Z| ≥ 2.0) marquées en rouge' }, comparison: { title: 'Activité moyenne par trimestre (toutes les années)', description: 'Lignes de base T1-T4 avec bandes d\'écart-type' }, classification: { title: 'Classification des schémas saisonniers', description: 'Distribution des schémas NORMAL, ÉLEVÉ, RÉDUIT, ANOMALIE' }, qoq: { title: 'Changements d\'un trimestre à l\'autre', description: 'Changements séquentiels de scrutins (% et absolu)' } }, classifications: { NORMAL_ACTIVITY: 'Activité normale', ELEVATED_ACTIVITY: 'Activité élevée', REDUCED_ACTIVITY: 'Activité réduite', ANOMALY_DETECTED: 'Anomalie détectée', NORMAL_SEASONAL_PATTERN: 'Schéma saisonnier normal', Q3_SUMMER_LULL: 'T3 pause estivale', Q4_ELEVATED_ACTIVITY: 'T4 activité élevée', UNUSUALLY_HIGH_ACTIVITY: 'Activité exceptionnellement élevée', UNUSUALLY_LOW_ACTIVITY: 'Activité exceptionnellement basse' }, loading: 'Chargement des données...', error: 'Erreur lors du chargement des données. Veuillez réessayer.', dataAttribution: 'Données de la plateforme CIA' },
    es: { title: 'Patrones estacionales (2002-2025)', subtitle: 'Análisis trimestral con detección de anomalías por puntuación Z', filters: { year: 'Año', quarter: 'Trimestre', election: 'Estado electoral', classification: 'Clasificación de actividad', allYears: 'Todos los años', allQuarters: 'Todos los trimestres', allElections: 'Todos', electionYears: 'Años electorales', nonElectionYears: 'Años no electorales', allClassifications: 'Todas las clasificaciones' }, quarters: { Q1: 'T1 - Sesión de invierno', Q2: 'T2 - Sesión de primavera', Q3: 'T3 - Receso de verano', Q4: 'T4 - Sesión de otoño' }, charts: { heatmap: { title: 'Mapa de calor de actividad trimestral (2002-2025)', description: 'Volumen de votaciones por año y trimestre con puntuación Z' }, zscore: { title: 'Detección de anomalías por puntuación Z', description: 'Valores atípicos estadísticos (|Z| ≥ 2.0) marcados en rojo' }, comparison: { title: 'Actividad promedio por trimestre (todos los años)', description: 'Líneas base T1-T4 con bandas de desviación estándar' }, classification: { title: 'Clasificación de patrones estacionales', description: 'Distribución de patrones NORMAL, ELEVADO, REDUCIDO, ANOMALÍA' }, qoq: { title: 'Cambios de trimestre a trimestre', description: 'Cambios secuenciales de votaciones (% y absoluto)' } }, classifications: { NORMAL_ACTIVITY: 'Actividad normal', ELEVATED_ACTIVITY: 'Actividad elevada', REDUCED_ACTIVITY: 'Actividad reducida', ANOMALY_DETECTED: 'Anomalía detectada', NORMAL_SEASONAL_PATTERN: 'Patrón estacional normal', Q3_SUMMER_LULL: 'T3 receso de verano', Q4_ELEVATED_ACTIVITY: 'T4 actividad elevada', UNUSUALLY_HIGH_ACTIVITY: 'Actividad inusualmente alta', UNUSUALLY_LOW_ACTIVITY: 'Actividad inusualmente baja' }, loading: 'Cargando datos...', error: 'Error al cargar los datos. Por favor, inténtelo de nuevo.', dataAttribution: 'Datos de la plataforma CIA' },
    nl: { title: 'Seizoenspatronen (2002-2025)', subtitle: 'Kwartaalanalyse met Z-score anomaliedetectie', filters: { year: 'Jaar', quarter: 'Kwartaal', election: 'Verkiezingsstatus', classification: 'Activiteitsclassificatie', allYears: 'Alle jaren', allQuarters: 'Alle kwartalen', allElections: 'Alle', electionYears: 'Verkiezingsjaren', nonElectionYears: 'Niet-verkiezingsjaren', allClassifications: 'Alle classificaties' }, quarters: { Q1: 'K1 - Wintersessie', Q2: 'K2 - Voorjaarssessie', Q3: 'K3 - Zomerpauze', Q4: 'K4 - Herfst sessie' }, charts: { heatmap: { title: 'Kwartaalactiviteit heatmap (2002-2025)', description: 'Stemvolume per jaar en kwartaal met Z-score' }, zscore: { title: 'Z-score anomaliedetectie', description: 'Statistische uitschieters (|Z| ≥ 2.0) gemarkeerd in rood' }, comparison: { title: 'Gemiddelde activiteit per kwartaal (alle jaren)', description: 'K1-K4 basislijnen met standaardafwijkingsbanden' }, classification: { title: 'Seizoenspatroon classificatie', description: 'Verdeling van NORMAAL, VERHOOGD, VERMINDERD, ANOMALIE patronen' }, qoq: { title: 'Kwartaal-op-kwartaal veranderingen', description: 'Opeenvolgende stemveranderingen (% en absoluut)' } }, classifications: { NORMAL_ACTIVITY: 'Normale activiteit', ELEVATED_ACTIVITY: 'Verhoogde activiteit', REDUCED_ACTIVITY: 'Verminderde activiteit', ANOMALY_DETECTED: 'Anomalie gedetecteerd', NORMAL_SEASONAL_PATTERN: 'Normaal seizoenspatroon', Q3_SUMMER_LULL: 'K3 zomerpauze', Q4_ELEVATED_ACTIVITY: 'K4 verhoogde activiteit', UNUSUALLY_HIGH_ACTIVITY: 'Ongewoon hoge activiteit', UNUSUALLY_LOW_ACTIVITY: 'Ongewoon lage activiteit' }, loading: 'Gegevens laden...', error: 'Fout bij het laden van gegevens. Probeer het opnieuw.', dataAttribution: 'Data van CIA-platform' },
    ar: { title: 'الأنماط الموسمية (2002-2025)', subtitle: 'تحليل ربع سنوي مع كشف الشذوذ بالنقاط Z', filters: { year: 'السنة', quarter: 'الربع', election: 'حالة الانتخابات', classification: 'تصنيف النشاط', allYears: 'كل السنوات', allQuarters: 'كل الأرباع', allElections: 'الكل', electionYears: 'سنوات الانتخابات', nonElectionYears: 'سنوات بدون انتخابات', allClassifications: 'كل التصنيفات' }, quarters: { Q1: 'الربع 1 - جلسة الشتاء', Q2: 'الربع 2 - جلسة الربيع', Q3: 'الربع 3 - عطلة الصيف', Q4: 'الربع 4 - جلسة الخريف' }, charts: { heatmap: { title: 'خريطة حرارية للنشاط الفصلي (2002-2025)', description: 'حجم التصويت حسب السنة والربع مع نقاط Z' }, zscore: { title: 'كشف الشذوذ بالنقاط Z', description: 'القيم الشاذة الإحصائية (|Z| ≥ 2.0) مميزة بالأحمر' }, comparison: { title: 'متوسط النشاط حسب الربع (كل السنوات)', description: 'خطوط أساسية للربع 1-4 مع نطاقات الانحراف المعياري' }, classification: { title: 'تصنيف الأنماط الموسمية', description: 'توزيع الأنماط العادية والمرتفعة والمنخفضة والشاذة' }, qoq: { title: 'التغيرات من ربع لآخر', description: 'التغيرات المتسلسلة في التصويت (% ومطلق)' } }, classifications: { NORMAL_ACTIVITY: 'نشاط عادي', ELEVATED_ACTIVITY: 'نشاط مرتفع', REDUCED_ACTIVITY: 'نشاط منخفض', ANOMALY_DETECTED: 'شذوذ مكتشف', NORMAL_SEASONAL_PATTERN: 'نمط موسمي عادي', Q3_SUMMER_LULL: 'الربع 3 عطلة صيفية', Q4_ELEVATED_ACTIVITY: 'الربع 4 نشاط مرتفع', UNUSUALLY_HIGH_ACTIVITY: 'نشاط مرتفع بشكل غير عادي', UNUSUALLY_LOW_ACTIVITY: 'نشاط منخفض بشكل غير عادي' }, loading: 'جاري تحميل البيانات...', error: 'خطأ في تحميل البيانات. يرجى المحاولة مرة أخرى.', dataAttribution: 'البيانات من منصة CIA' },
    he: { title: 'דפוסים עונתיים (2002-2025)', subtitle: 'ניתוח רבעוני עם זיהוי חריגות Z-Score', filters: { year: 'שנה', quarter: 'רבעון', election: 'סטטוס בחירות', classification: 'סיווג פעילות', allYears: 'כל השנים', allQuarters: 'כל הרבעונים', allElections: 'הכל', electionYears: 'שנות בחירות', nonElectionYears: 'שנים ללא בחירות', allClassifications: 'כל הסיווגים' }, quarters: { Q1: 'רבעון 1 - מושב חורף', Q2: 'רבעון 2 - מושב אביב', Q3: 'רבעון 3 - הפסקת קיץ', Q4: 'רבעון 4 - מושב סתיו' }, charts: { heatmap: { title: 'מפת חום של פעילות רבעונית (2002-2025)', description: 'נפח הצבעות לפי שנה ורבעון עם ציון Z' }, zscore: { title: 'זיהוי חריגות Z-Score', description: 'ערכים סטטיסטיים חריגים (|Z| ≥ 2.0) מסומנים באדום' }, comparison: { title: 'פעילות ממוצעת לפי רבעון (כל השנים)', description: 'קווי בסיס רבעון 1-4 עם רצועות סטיית תקן' }, classification: { title: 'סיווג דפוסים עונתיים', description: 'התפלגות דפוסים רגילים, מוגברים, מופחתים וחריגים' }, qoq: { title: 'שינויים מרבעון לרבעון', description: 'שינויים רציפים בהצבעה (% ומוחלט)' } }, classifications: { NORMAL_ACTIVITY: 'פעילות רגילה', ELEVATED_ACTIVITY: 'פעילות מוגברת', REDUCED_ACTIVITY: 'פעילות מופחתת', ANOMALY_DETECTED: 'חריגה זוהתה', NORMAL_SEASONAL_PATTERN: 'דפוס עונתי רגיל', Q3_SUMMER_LULL: 'רבעון 3 הפסקת קיץ', Q4_ELEVATED_ACTIVITY: 'רבעון 4 פעילות מוגברת', UNUSUALLY_HIGH_ACTIVITY: 'פעילות גבוהה במיוחד', UNUSUALLY_LOW_ACTIVITY: 'פעילות נמוכה במיוחד' }, loading: 'טוען נתונים...', error: 'שגיאה בטעינת נתונים. נסה שוב.', dataAttribution: 'נתונים מפלטפורמת CIA' },
    ja: { title: '季節パターン (2002-2025)', subtitle: 'Zスコア異常検出を伴う四半期分析', filters: { year: '年', quarter: '四半期', election: '選挙状況', classification: '活動分類', allYears: 'すべての年', allQuarters: 'すべての四半期', allElections: 'すべて', electionYears: '選挙年', nonElectionYears: '非選挙年', allClassifications: 'すべての分類' }, quarters: { Q1: 'Q1 - 冬季会期', Q2: 'Q2 - 春季会期', Q3: 'Q3 - 夏季休会', Q4: 'Q4 - 秋季会期' }, charts: { heatmap: { title: '四半期活動ヒートマップ (2002-2025)', description: '年と四半期別の投票量とZスコア' }, zscore: { title: 'Zスコア異常検出', description: '統計的外れ値 (|Z| ≥ 2.0) を赤でマーク' }, comparison: { title: '四半期別平均活動(全年)', description: 'Q1-Q4のベースラインと標準偏差バンド' }, classification: { title: '季節パターン分類', description: '正常、上昇、減少、異常パターンの分布' }, qoq: { title: '四半期間の変化', description: '連続的な投票変化(%と絶対値)' } }, classifications: { NORMAL_ACTIVITY: '通常の活動', ELEVATED_ACTIVITY: '活動上昇', REDUCED_ACTIVITY: '活動減少', ANOMALY_DETECTED: '異常検出', NORMAL_SEASONAL_PATTERN: '通常の季節パターン', Q3_SUMMER_LULL: 'Q3夏季休会', Q4_ELEVATED_ACTIVITY: 'Q4活動上昇', UNUSUALLY_HIGH_ACTIVITY: '異常に高い活動', UNUSUALLY_LOW_ACTIVITY: '異常に低い活動' }, loading: 'データ読み込み中...', error: 'データの読み込みエラー。もう一度お試しください。', dataAttribution: 'CIAプラットフォームのデータ' },
    ko: { title: '계절별 패턴 (2002-2025)', subtitle: 'Z점수 이상 탐지를 통한 분기별 분석', filters: { year: '년도', quarter: '분기', election: '선거 상태', classification: '활동 분류', allYears: '모든 연도', allQuarters: '모든 분기', allElections: '모두', electionYears: '선거 연도', nonElectionYears: '비선거 연도', allClassifications: '모든 분류' }, quarters: { Q1: '1분기 - 겨울 회기', Q2: '2분기 - 봄 회기', Q3: '3분기 - 여름 휴회', Q4: '4분기 - 가을 회기' }, charts: { heatmap: { title: '분기별 활동 히트맵 (2002-2025)', description: '연도 및 분기별 투표량과 Z점수' }, zscore: { title: 'Z점수 이상 탐지', description: '통계적 이상값 (|Z| ≥ 2.0)은 빨간색으로 표시' }, comparison: { title: '분기별 평균 활동 (모든 연도)', description: '1~4분기 기준선과 표준편차 밴드' }, classification: { title: '계절별 패턴 분류', description: '정상, 상승, 감소, 이상 패턴의 분포' }, qoq: { title: '분기별 변화', description: '순차적 투표 변화 (% 및 절대값)' } }, classifications: { NORMAL_ACTIVITY: '정상 활동', ELEVATED_ACTIVITY: '상승 활동', REDUCED_ACTIVITY: '감소 활동', ANOMALY_DETECTED: '이상 탐지', NORMAL_SEASONAL_PATTERN: '정상 계절 패턴', Q3_SUMMER_LULL: '3분기 여름 휴회', Q4_ELEVATED_ACTIVITY: '4분기 상승 활동', UNUSUALLY_HIGH_ACTIVITY: '비정상적으로 높은 활동', UNUSUALLY_LOW_ACTIVITY: '비정상적으로 낮은 활동' }, loading: '데이터 로딩 중...', error: '데이터 로딩 오류. 다시 시도해주세요.', dataAttribution: 'CIA 플랫폼의 데이터' },
    zh: { title: '季节性模式 (2002-2025)', subtitle: '带Z分数异常检测的季度分析', filters: { year: '年份', quarter: '季度', election: '选举状态', classification: '活动分类', allYears: '所有年份', allQuarters: '所有季度', allElections: '全部', electionYears: '选举年', nonElectionYears: '非选举年', allClassifications: '所有分类' }, quarters: { Q1: '第1季度 - 冬季会期', Q2: '第2季度 - 春季会期', Q3: '第3季度 - 夏季休会', Q4: '第4季度 - 秋季会期' }, charts: { heatmap: { title: '季度活动热图 (2002-2025)', description: '按年份和季度的投票量与Z分数' }, zscore: { title: 'Z分数异常检测', description: '统计异常值 (|Z| ≥ 2.0) 标记为红色' }, comparison: { title: '按季度的平均活动(所有年份)', description: '第1-4季度基线与标准差带' }, classification: { title: '季节性模式分类', description: '正常、升高、降低、异常模式的分布' }, qoq: { title: '季度环比变化', description: '连续投票变化(%和绝对值)' } }, classifications: { NORMAL_ACTIVITY: '正常活动', ELEVATED_ACTIVITY: '活动升高', REDUCED_ACTIVITY: '活动降低', ANOMALY_DETECTED: '检测到异常', NORMAL_SEASONAL_PATTERN: '正常季节性模式', Q3_SUMMER_LULL: '第3季度夏季休会', Q4_ELEVATED_ACTIVITY: '第4季度活动升高', UNUSUALLY_HIGH_ACTIVITY: '异常高的活动', UNUSUALLY_LOW_ACTIVITY: '异常低的活动' }, loading: '加载数据中...', error: '加载数据出错。请重试。', dataAttribution: '数据来自CIA平台' }
  };

  // ============================================================================
  // Data Manager
  // ============================================================================
  
  class SeasonalPatternsDataManager {
    constructor() {
      this.data = null;
      this.cachedData = null;
    }

    /**
     * Fetch data from CIA platform with 24-hour caching
     * Implements local-first loading: tries local file, then remote fallback
     */
    async fetchData() {
      try {
        // Check cache first
        const cached = this.getCachedData();
        if (cached) {
          console.log('Using cached seasonal patterns data');
          this.data = cached;
          return cached;
        }

        // Try each URL in sequence (local first, then remote)
        let lastError = null;
        for (let i = 0; i < CONFIG.dataUrls.length; i++) {
          const url = CONFIG.dataUrls[i];
          const isLocal = !url.startsWith('http');
          
          try {
            console.log(`Fetching seasonal patterns data from ${isLocal ? 'local' : 'remote'} source (${i + 1}/${CONFIG.dataUrls.length})...`);
            const response = await fetch(url);
            
            if (!response.ok) {
              throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }

            const csvText = await response.text();
            const parsedData = this.parseCSV(csvText);
            
            // Cache the data
            this.setCachedData(parsedData);
            this.data = parsedData;
            
            console.log(`✅ Loaded ${parsedData.length} seasonal activity records from ${isLocal ? 'local' : 'remote'} source`);
            return parsedData;
          } catch (error) {
            lastError = error;
            console.warn(`Failed to load from ${isLocal ? 'local' : 'remote'} source: ${error.message}`);
            // Continue to next URL
          }
        }
        
        // All URLs failed
        throw lastError || new Error('All data sources failed');
        
      } catch (error) {
        console.error('Error fetching seasonal patterns data:', error);
        // Try to use cached data even if expired
        const cached = localStorage.getItem(CONFIG.cacheKey);
        if (cached) {
          try {
            const parsed = JSON.parse(cached);
            if (parsed && typeof parsed === 'object' && 'data' in parsed) {
              console.log('Using expired cache as fallback');
              this.data = parsed.data;
              return parsed.data;
            } else {
              console.warn('Expired cache is in an unexpected format, ignoring.');
            }
          } catch (parseError) {
            console.warn('Failed to parse expired cache, ignoring.', parseError);
          }
        }
        throw error;
      }
    }

    /**
     * Parse CSV data using PapaParse (if available) or fallback parser
     */
    parseCSV(csvText) {
      if (typeof Papa !== 'undefined') {
        const parsed = Papa.parse(csvText, {
          header: true,
          dynamicTyping: true,
          skipEmptyLines: true
        });
        return parsed.data;
      } else {
        // Fallback CSV parser
        return this.parseCSVFallback(csvText);
      }
    }

    /**
     * Fallback CSV parser (if PapaParse is not available)
     * Uses d3.csvParse when available (handles RFC 4180 quoted fields),
     * otherwise falls back to a minimal parser.
     */
    parseCSVFallback(csvText) {
      // Prefer d3.csvParse if D3 is available (handles RFC 4180 properly)
      if (typeof d3 !== 'undefined' && typeof d3.csvParse === 'function') {
        try {
          const parsed = d3.csvParse(csvText, (d) => {
            const row = {};
            Object.keys(d).forEach((key) => {
              const value = d[key];
              // Only coerce if the value is purely numeric (no letters, no dashes except leading minus)
              // This prevents "2022-2026" from becoming 2022
              if (typeof value === 'string') {
                // Match pattern: optional minus, followed by digits, optional decimal point and more digits
                if (/^-?\d+(\.\d+)?$/.test(value.trim())) {
                  row[key] = parseFloat(value);
                } else {
                  row[key] = value;
                }
              } else {
                row[key] = value;
              }
            });
            return row;
          });
          return parsed;
        } catch (err) {
          console.warn('d3.csvParse failed, using basic parser:', err);
        }
      }

      // Minimal fallback parser (does not handle quoted fields with commas)
      console.warn('Using basic CSV parser - quoted fields with commas may not parse correctly');
      const lines = csvText.trim().split('\n');
      const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
      const data = [];

      for (let i = 1; i < lines.length; i++) {
        const values = lines[i].split(',').map(v => v.trim().replace(/^"|"$/g, ''));
        const row = {};
        headers.forEach((header, index) => {
          const value = values[index];
          // Only coerce if purely numeric
          if (/^-?\d+(\.\d+)?$/.test(value)) {
            row[header] = parseFloat(value);
          } else {
            row[header] = value;
          }
        });
        data.push(row);
      }

      return data;
    }

    /**
     * Get cached data from LocalStorage
     */
    getCachedData() {
      try {
        const cached = localStorage.getItem(CONFIG.cacheKey);
        if (!cached) return null;

        const parsed = JSON.parse(cached);
        const now = Date.now();
        
        if (now - parsed.timestamp > CONFIG.cacheDuration) {
          console.log('Cache expired');
          return null;
        }

        return parsed.data;
      } catch (error) {
        console.error('Error reading cache:', error);
        return null;
      }
    }

    /**
     * Save data to LocalStorage cache
     */
    setCachedData(data) {
      try {
        const cacheObject = {
          data: data,
          timestamp: Date.now()
        };
        localStorage.setItem(CONFIG.cacheKey, JSON.stringify(cacheObject));
        console.log('Data cached successfully');
      } catch (error) {
        console.error('Error caching data:', error);
      }
    }

    /**
     * Aggregate data by quarter (cross-year averages)
     */
    aggregateByQuarter() {
      if (!this.data) return null;

      const quarters = { Q1: [], Q2: [], Q3: [], Q4: [] };
      
      this.data.forEach(row => {
        const quarter = `Q${row.quarter}`;
        if (quarters[quarter]) {
          quarters[quarter].push(row);
        }
      });

      const aggregated = {};
      Object.keys(quarters).forEach(q => {
        const records = quarters[q];
        if (records.length === 0) return;

        const ballots = records.map(r => r.total_ballots || 0);
        const attendance = records.map(r => r.attendance_rate || 0);
        const docs = records.map(r => r.documents_produced || 0);

        aggregated[q] = {
          quarter: q,
          avgBallots: this.mean(ballots),
          stddevBallots: this.stddev(ballots),
          avgAttendance: this.mean(attendance),
          stddevAttendance: this.stddev(attendance),
          avgDocs: this.mean(docs),
          stddevDocs: this.stddev(docs),
          count: records.length
        };
      });

      return aggregated;
    }

    /**
     * Identify anomalies (|Z-score| >= threshold)
     */
    identifyAnomalies(threshold = CONFIG.zScoreThreshold) {
      if (!this.data) return [];

      const anomalies = this.data.filter(row => {
        const ballotZ = Math.abs(row.ballot_z_score || 0);
        const docZ = Math.abs(row.doc_z_score || 0);
        const attendanceZ = Math.abs(row.attendance_z_score || 0);
        
        return ballotZ >= threshold || docZ >= threshold || attendanceZ >= threshold;
      });

      // Sort by maximum Z-score (descending)
      anomalies.sort((a, b) => {
        const maxZa = Math.max(
          Math.abs(a.ballot_z_score || 0),
          Math.abs(a.doc_z_score || 0),
          Math.abs(a.attendance_z_score || 0)
        );
        const maxZb = Math.max(
          Math.abs(b.ballot_z_score || 0),
          Math.abs(b.doc_z_score || 0),
          Math.abs(b.attendance_z_score || 0)
        );
        return maxZb - maxZa;
      });

      return anomalies;
    }

    /**
     * Calculate mean of an array
     */
    mean(arr) {
      if (arr.length === 0) return 0;
      return arr.reduce((sum, val) => sum + val, 0) / arr.length;
    }

    /**
     * Calculate standard deviation of an array
     */
    stddev(arr) {
      if (arr.length === 0) return 0;
      const avg = this.mean(arr);
      const squareDiffs = arr.map(val => Math.pow(val - avg, 2));
      const avgSquareDiff = this.mean(squareDiffs);
      return Math.sqrt(avgSquareDiff);
    }

    /**
     * Filter data by criteria
     */
    filterData(filters) {
      if (!this.data) return [];

      return this.data.filter(row => {
        if (filters.year && filters.year !== 'all' && row.year !== parseInt(filters.year)) {
          return false;
        }
        if (filters.quarter && filters.quarter !== 'all' && row.quarter !== parseInt(filters.quarter)) {
          return false;
        }
        if (filters.election && filters.election !== 'all') {
          const isElection = row.is_election_year === 't' || row.is_election_year === true;
          if (filters.election === 'election' && !isElection) return false;
          if (filters.election === 'non-election' && isElection) return false;
        }
        if (filters.classification && filters.classification !== 'all') {
          if (row.base_activity_classification !== filters.classification &&
              row.seasonal_pattern_classification !== filters.classification) {
            return false;
          }
        }
        return true;
      });
    }
  }

  // ============================================================================
  // Chart Renderers
  // ============================================================================
  
  class SeasonalPatternsCharts {
    constructor(dataManager, language = 'en') {
      this.dataManager = dataManager;
      this.language = language;
      this.translations = TRANSLATIONS[language] || TRANSLATIONS.en;
      this.chartInstances = {};
    }

    /**
     * Destroy all chart instances
     */
    destroyCharts() {
      Object.keys(this.chartInstances).forEach(key => {
        if (this.chartInstances[key]) {
          this.chartInstances[key].destroy();
          delete this.chartInstances[key];
        }
      });
    }

    /**
     * Render all charts
     */
    async renderAll(filteredData = null) {
      const data = filteredData || this.dataManager.data;
      if (!data || data.length === 0) {
        console.warn('No data available for rendering');
        return;
      }

      this.destroyCharts();

      // Render each chart
      this.renderSeasonalHeatmap(data);
      this.renderZScoreTimeline(data);
      this.renderQuarterComparison(data);
      this.renderClassificationChart(data);
      this.renderQoQChangeChart(data);
    }

    /**
     * Render seasonal heat map using D3.js
     */
    renderSeasonalHeatmap(data) {
      const container = document.getElementById('seasonal-heatmap');
      if (!container || typeof d3 === 'undefined') {
        console.warn('D3.js not loaded or container not found');
        return;
      }

      // Clear container
      container.innerHTML = '';

      // Dimensions
      const margin = { top: 40, right: 100, bottom: 60, left: 60 };
      const width = Math.min(container.clientWidth, 1200) - margin.left - margin.right;
      const height = 600 - margin.top - margin.bottom;

      // Create SVG
      const svg = d3.select(container)
        .append('svg')
        .attr('width', width + margin.left + margin.right)
        .attr('height', height + margin.top + margin.bottom)
        .attr('role', 'img')
        .attr('aria-label', this.translations.charts.heatmap.title)
        .append('g')
        .attr('transform', `translate(${margin.left},${margin.top})`);

      // Get unique years and quarters
      const years = [...new Set(data.map(d => d.year))].sort();
      const quarters = [1, 2, 3, 4];

      // Scales
      const xScale = d3.scaleBand()
        .domain(quarters)
        .range([0, width])
        .padding(0.05);

      const yScale = d3.scaleBand()
        .domain(years)
        .range([0, height])
        .padding(0.05);

      // Color scale for ballots
      const maxBallots = d3.max(data, d => d.total_ballots || 0);
      const colorScale = d3.scaleSequential()
        .domain([0, maxBallots])
        .interpolator(d3.interpolateYlOrRd);

      // Create heat map cells
      svg.selectAll('.cell')
        .data(data)
        .enter()
        .append('rect')
        .attr('class', 'cell')
        .attr('x', d => xScale(d.quarter))
        .attr('y', d => yScale(d.year))
        .attr('width', xScale.bandwidth())
        .attr('height', yScale.bandwidth())
        .attr('fill', d => colorScale(d.total_ballots || 0))
        .attr('stroke', '#fff')
        .attr('stroke-width', 1)
        .attr('role', 'presentation')
        .on('mouseover', function(event, d) {
          // Tooltip
          d3.select(this).attr('stroke', '#000').attr('stroke-width', 2);
        })
        .on('mouseout', function() {
          d3.select(this).attr('stroke', '#fff').attr('stroke-width', 1);
        })
        .append('title')
        .text(d => {
          const t = this.translations;
          const classText = t.classifications[d.seasonal_pattern_classification] || t.classifications[d.base_activity_classification] || d.seasonal_pattern_classification || t.tooltips.na;
          return `${d.year} Q${d.quarter}\n${t.tooltips.ballots}: ${d.total_ballots}\n${t.tooltips.zScore}: ${(d.ballot_z_score || 0).toFixed(2)}\n${t.tooltips.classification}: ${classText}`;
        });

      // Add anomaly markers
      const anomalies = data.filter(d => Math.abs(d.ballot_z_score || 0) >= CONFIG.zScoreThreshold);
      svg.selectAll('.anomaly-marker')
        .data(anomalies)
        .enter()
        .append('circle')
        .attr('class', 'anomaly-marker')
        .attr('cx', d => xScale(d.quarter) + xScale.bandwidth() / 2)
        .attr('cy', d => yScale(d.year) + yScale.bandwidth() / 2)
        .attr('r', 8)
        .attr('fill', CONFIG.colors.danger)
        .attr('stroke', '#fff')
        .attr('stroke-width', 2)
        .attr('role', 'presentation')
        .append('title')
        .text(d => {
          const t = this.translations;
          return `${t.tooltips.anomaly}: ${d.year} Q${d.quarter}\n${t.tooltips.zScore}: ${(d.ballot_z_score || 0).toFixed(2)}`;
        });

      // Add axes
      const xAxis = d3.axisBottom(xScale)
        .tickFormat(q => `Q${q}`);
      
      const yAxis = d3.axisLeft(yScale);

      svg.append('g')
        .attr('transform', `translate(0,${height})`)
        .call(xAxis)
        .attr('class', 'axis');

      svg.append('g')
        .call(yAxis)
        .attr('class', 'axis');

      // Add axis labels
      const quarterLabel = this.translations.filters?.quarter || 'Quarter';
      const yearLabel = this.translations.filters?.year || 'Year';
      
      svg.append('text')
        .attr('x', width / 2)
        .attr('y', height + 40)
        .attr('text-anchor', 'middle')
        .text(quarterLabel)
        .style('font-size', '14px')
        .style('font-weight', '500');

      svg.append('text')
        .attr('transform', 'rotate(-90)')
        .attr('x', -height / 2)
        .attr('y', -40)
        .attr('text-anchor', 'middle')
        .text(yearLabel)
        .style('font-size', '14px')
        .style('font-weight', '500');

      // Add legend
      const legendHeight = 10;
      const legend = svg.append('g')
        .attr('transform', `translate(${width + 20}, 0)`);

      const legendScale = d3.scaleLinear()
        .domain([0, maxBallots])
        .range([0, legendHeight * 20]);

      const legendAxis = d3.axisRight(legendScale)
        .ticks(5);

      // Legend gradient
      const defs = svg.append('defs');
      const gradient = defs.append('linearGradient')
        .attr('id', 'legend-gradient')
        .attr('x1', '0%')
        .attr('y1', '100%')
        .attr('x2', '0%')
        .attr('y2', '0%');

      gradient.selectAll('stop')
        .data(d3.range(0, 1.1, 0.1))
        .enter()
        .append('stop')
        .attr('offset', d => `${d * 100}%`)
        .attr('stop-color', d => colorScale(d * maxBallots));

      legend.append('rect')
        .attr('width', legendHeight)
        .attr('height', legendHeight * 20)
        .style('fill', 'url(#legend-gradient)');

      legend.append('g')
        .attr('transform', `translate(${legendHeight}, 0)`)
        .call(legendAxis);

      legend.append('text')
        .attr('x', 0)
        .attr('y', -10)
        .text(this.translations.tooltips.ballots)
        .style('font-size', '12px')
        .style('font-weight', '500');
    }

    /**
     * Render Z-score timeline using Chart.js
     */
    renderZScoreTimeline(data) {
      const canvas = document.getElementById('zscore-timeline-chart');
      if (!canvas || typeof Chart === 'undefined') {
        console.warn('Chart.js not loaded or canvas not found');
        return;
      }

      const ctx = canvas.getContext('2d');
      const t = this.translations.chartLabels;

      // Sort data by year and quarter
      const sortedData = [...data].sort((a, b) => {
        if (a.year !== b.year) return a.year - b.year;
        return a.quarter - b.quarter;
      });

      const labels = sortedData.map(d => `${d.year}-Q${d.quarter}`);
      const ballotZScores = sortedData.map(d => d.ballot_z_score || 0);
      const docZScores = sortedData.map(d => d.doc_z_score || 0);
      const attendanceZScores = sortedData.map(d => d.attendance_z_score || 0);

      this.chartInstances.zscore = new Chart(ctx, {
        type: 'line',
        data: {
          labels: labels,
          datasets: [
            {
              label: t.ballotZScore,
              data: ballotZScores,
              borderColor: CONFIG.colors.primary,
              backgroundColor: CONFIG.colors.primary + '40',
              borderWidth: 2,
              pointRadius: 3,
              pointHoverRadius: 5,
              tension: 0.1
            },
            {
              label: t.documentZScore,
              data: docZScores,
              borderColor: CONFIG.colors.secondary,
              backgroundColor: CONFIG.colors.secondary + '40',
              borderWidth: 2,
              pointRadius: 3,
              pointHoverRadius: 5,
              tension: 0.1
            },
            {
              label: t.attendanceZScore,
              data: attendanceZScores,
              borderColor: CONFIG.colors.tertiary,
              backgroundColor: CONFIG.colors.tertiary + '40',
              borderWidth: 2,
              pointRadius: 3,
              pointHoverRadius: 5,
              tension: 0.1
            }
          ]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: {
            title: {
              display: false
            },
            legend: {
              position: 'top',
              labels: {
                boxWidth: 12,
                padding: 15
              }
            },
            tooltip: {
              callbacks: {
                label: function(context) {
                  let label = context.dataset.label || '';
                  if (label) {
                    label += ': ';
                  }
                  label += context.parsed.y.toFixed(2);
                  const absZ = Math.abs(context.parsed.y);
                  if (absZ >= CONFIG.zScoreThreshold) {
                    label += ' 🔴 ' + t.anomaly;
                  }
                  return label;
                }
              }
            }
          },
          scales: {
            x: {
              display: true,
              title: {
                display: true,
                text: t.yearQuarter
              },
              ticks: {
                maxRotation: 90,
                minRotation: 45,
                autoSkip: true,
                maxTicksLimit: 20
              }
            },
            y: {
              display: true,
              title: {
                display: true,
                text: t.zScore
              },
              min: -4,
              max: 4
            }
          }
        }
      });
    }

    /**
     * Render quarter comparison chart using Chart.js
     */
    renderQuarterComparison(data) {
      const canvas = document.getElementById('quarter-comparison-chart');
      if (!canvas || typeof Chart === 'undefined') {
        console.warn('Chart.js not loaded or canvas not found');
        return;
      }

      const ctx = canvas.getContext('2d');
      
      // Aggregate the provided filtered data
      const aggregated = this.aggregateDataByQuarter(data || this.dataManager.data);

      if (!aggregated) {
        console.warn('No aggregated data available');
        return;
      }

      const labels = ['Q1', 'Q2', 'Q3', 'Q4'];
      const avgBallots = labels.map(q => aggregated[q]?.avgBallots || 0);
      const stddevBallots = labels.map(q => aggregated[q]?.stddevBallots || 0);

      this.chartInstances.comparison = new Chart(ctx, {
        type: 'bar',
        data: {
          labels: labels.map(q => this.translations.quarters[q] || q),
          datasets: [
            {
              label: this.translations.charts?.comparison?.title || 'Average Ballots',
              data: avgBallots,
              backgroundColor: labels.map(q => CONFIG.quarterColors[q]),
              borderColor: labels.map(q => CONFIG.quarterColors[q]),
              borderWidth: 2
            }
          ]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: {
            title: {
              display: false
            },
            legend: {
              display: false
            },
            tooltip: {
              callbacks: {
                label: function(context) {
                  const avg = context.parsed.y;
                  const stddev = stddevBallots[context.dataIndex];
                  return [
                    `Average: ${avg.toFixed(1)} ballots`,
                    `Std Dev: ±${stddev.toFixed(1)}`
                  ];
                }
              }
            }
          },
          scales: {
            x: {
              display: true,
              title: {
                display: true,
                text: this.translations.chartLabels.quarter
              }
            },
            y: {
              display: true,
              title: {
                display: true,
                text: this.translations.chartLabels.averageBallots
              },
              beginAtZero: true
            }
          }
        }
      });
    }

    /**
     * Aggregate data by quarter (for filtered datasets)
     */
    aggregateDataByQuarter(data) {
      if (!data) return null;

      const quarters = { Q1: [], Q2: [], Q3: [], Q4: [] };
      
      data.forEach(row => {
        const quarter = `Q${row.quarter}`;
        if (quarters[quarter]) {
          quarters[quarter].push(row);
        }
      });

      const aggregated = {};
      Object.keys(quarters).forEach(q => {
        const records = quarters[q];
        if (records.length === 0) return;

        const ballots = records.map(r => r.total_ballots || 0);
        const attendance = records.map(r => r.attendance_rate || 0);
        const docs = records.map(r => r.documents_produced || 0);

        aggregated[q] = {
          quarter: q,
          avgBallots: this.mean(ballots),
          stddevBallots: this.stddev(ballots),
          avgAttendance: this.mean(attendance),
          stddevAttendance: this.stddev(attendance),
          avgDocs: this.mean(docs),
          stddevDocs: this.stddev(docs),
          count: records.length
        };
      });

      return aggregated;
    }

    /**
     * Calculate mean of an array
     */
    mean(arr) {
      if (!arr || arr.length === 0) return 0;
      return arr.reduce((sum, val) => sum + val, 0) / arr.length;
    }

    /**
     * Calculate standard deviation of an array
     */
    stddev(arr) {
      if (!arr || arr.length === 0) return 0;
      const avg = this.mean(arr);
      const squareDiffs = arr.map(val => Math.pow(val - avg, 2));
      return Math.sqrt(this.mean(squareDiffs));
    }

    /**
     * Render classification distribution chart using Chart.js
     */
    renderClassificationChart(data) {
      const canvas = document.getElementById('classification-chart');
      if (!canvas || typeof Chart === 'undefined') {
        console.warn('Chart.js not loaded or canvas not found');
        return;
      }

      const ctx = canvas.getContext('2d');

      // Count classifications by year
      const years = [...new Set(data.map(d => d.year))].sort();
      const classifications = {};

      data.forEach(row => {
        const classification = row.seasonal_pattern_classification || 'UNKNOWN';
        if (!classifications[classification]) {
          classifications[classification] = {};
        }
        if (!classifications[classification][row.year]) {
          classifications[classification][row.year] = 0;
        }
        classifications[classification][row.year]++;
      });

      const datasets = Object.keys(classifications).map(classification => {
        const counts = years.map(year => classifications[classification][year] || 0);
        let color;
        
        if (classification.includes('NORMAL')) {
          color = CONFIG.colors.normal;
        } else if (classification.includes('ELEVATED') || classification.includes('HIGH')) {
          color = CONFIG.colors.elevated;
        } else if (classification.includes('REDUCED') || classification.includes('LOW')) {
          color = CONFIG.colors.reduced;
        } else if (classification.includes('ANOMALY')) {
          color = CONFIG.colors.anomaly;
        } else {
          color = CONFIG.colors.info;
        }

        return {
          label: this.translations.classifications[classification] || classification,
          data: counts,
          backgroundColor: color,
          borderColor: color,
          borderWidth: 1
        };
      });

      this.chartInstances.classification = new Chart(ctx, {
        type: 'bar',
        data: {
          labels: years,
          datasets: datasets
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: {
            title: {
              display: false
            },
            legend: {
              position: 'top'
            },
            tooltip: {
              mode: 'index',
              intersect: false
            }
          },
          scales: {
            x: {
              stacked: true,
              display: true,
              title: {
                display: true,
                text: this.translations.chartLabels.year
              }
            },
            y: {
              stacked: true,
              display: true,
              title: {
                display: true,
                text: this.translations.chartLabels.count
              },
              beginAtZero: true
            }
          }
        }
      });
    }

    /**
     * Render QoQ change waterfall chart using Chart.js
     */
    renderQoQChangeChart(data) {
      const canvas = document.getElementById('qoq-change-chart');
      if (!canvas || typeof Chart === 'undefined') {
        console.warn('Chart.js not loaded or canvas not found');
        return;
      }

      const ctx = canvas.getContext('2d');

      // Sort data and filter those with QoQ change data
      const sortedData = [...data]
        .filter(d => d.qoq_ballot_change_pct !== null && d.qoq_ballot_change_pct !== undefined)
        .sort((a, b) => {
          if (a.year !== b.year) return a.year - b.year;
          return a.quarter - b.quarter;
        });

      const labels = sortedData.map(d => `${d.year}-Q${d.quarter}`);
      const changes = sortedData.map(d => d.qoq_ballot_change_pct || 0);
      
      // Color by positive/negative
      const colors = changes.map(change => {
        if (change > 0) return CONFIG.colors.success;
        if (change < 0) return CONFIG.colors.danger;
        return CONFIG.colors.info;
      });

      this.chartInstances.qoq = new Chart(ctx, {
        type: 'bar',
        data: {
          labels: labels,
          datasets: [
            {
              label: this.translations.chartLabels.qoqChange,
              data: changes,
              backgroundColor: colors,
              borderColor: colors,
              borderWidth: 1
            }
          ]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: {
            title: {
              display: false
            },
            legend: {
              display: false
            },
            tooltip: {
              callbacks: {
                label: function(context) {
                  return `Change: ${context.parsed.y.toFixed(2)}%`;
                }
              }
            }
          },
          scales: {
            x: {
              display: true,
              title: {
                display: true,
                text: this.translations.chartLabels.yearQuarter
              },
              ticks: {
                maxRotation: 90,
                minRotation: 45,
                autoSkip: true,
                maxTicksLimit: 20
              }
            },
            y: {
              display: true,
              title: {
                display: true,
                text: this.translations.chartLabels.changePercent
              }
            }
          }
        }
      });
    }
  }

  // ============================================================================
  // Dashboard Controller
  // ============================================================================
  
  class SeasonalPatternsDashboard {
    constructor() {
      this.dataManager = new SeasonalPatternsDataManager();
      this.chartRenderer = null;
      this.currentLanguage = this.detectLanguage();
      // Initialize translations with fallback to English
      this.translations = TRANSLATIONS[this.currentLanguage] || TRANSLATIONS.en;
      // Ensure tooltips exist (fallback to English if missing)
      if (!this.translations.tooltips) {
        this.translations.tooltips = TRANSLATIONS.en.tooltips;
      }
      // Ensure chartLabels exist (fallback to English if missing)
      if (!this.translations.chartLabels) {
        this.translations.chartLabels = TRANSLATIONS.en.chartLabels;
      }
      this.currentFilters = {
        year: 'all',
        quarter: 'all',
        election: 'all',
        classification: 'all'
      };
    }

    /**
     * Detect current language from URL
     */
    detectLanguage() {
      const path = window.location.pathname;
      const match = path.match(/index_([a-z]{2})\.html/);
      if (match) {
        return match[1];
      }
      return 'en';
    }

    /**
     * Initialize dashboard
     */
    async initialize() {
      try {
        // Show loading state
        this.showLoading();

        // Fetch data
        await this.dataManager.fetchData();

        // Initialize chart renderer
        this.chartRenderer = new SeasonalPatternsCharts(this.dataManager, this.currentLanguage);

        // Setup filters
        this.setupFilters();

        // Render charts
        await this.chartRenderer.renderAll();

        // Hide loading state
        this.hideLoading();

        console.log('Seasonal Patterns Dashboard initialized successfully');
      } catch (error) {
        console.error('Error initializing dashboard:', error);
        this.showError();
      }
    }

    /**
     * Setup filter controls
     */
    setupFilters() {
      const yearFilter = document.getElementById('year-filter');
      const quarterFilter = document.getElementById('quarter-filter');
      const electionFilter = document.getElementById('election-filter');
      const classificationFilter = document.getElementById('classification-filter');

      if (yearFilter) {
        yearFilter.addEventListener('change', (e) => {
          this.currentFilters.year = e.target.value;
          this.applyFilters();
        });

        // Populate year options
        const years = [...new Set(this.dataManager.data.map(d => d.year))].sort((a, b) => b - a);
        years.forEach(year => {
          const option = document.createElement('option');
          option.value = year;
          option.textContent = year;
          yearFilter.appendChild(option);
        });
      }

      if (quarterFilter) {
        quarterFilter.addEventListener('change', (e) => {
          this.currentFilters.quarter = e.target.value;
          this.applyFilters();
        });
      }

      if (electionFilter) {
        electionFilter.addEventListener('change', (e) => {
          this.currentFilters.election = e.target.value;
          this.applyFilters();
        });
      }

      if (classificationFilter) {
        classificationFilter.addEventListener('change', (e) => {
          this.currentFilters.classification = e.target.value;
          this.applyFilters();
        });

        // Populate classification options from both seasonal and base activity classifications
        const classificationSet = new Set();
        this.dataManager.data.forEach(d => {
          if (d.seasonal_pattern_classification) {
            classificationSet.add(d.seasonal_pattern_classification);
          }
          if (d.base_activity_classification) {
            classificationSet.add(d.base_activity_classification);
          }
        });

        const classifications = [...classificationSet].sort();
        classifications.forEach(classification => {
          const option = document.createElement('option');
          option.value = classification;
          const translatedLabel = this.translations.classifications?.[classification]
            || TRANSLATIONS.en?.classifications?.[classification]
            || classification;
          option.textContent = translatedLabel;
          classificationFilter.appendChild(option);
        });
      }
    }

    /**
     * Apply filters and re-render charts
     */
    async applyFilters() {
      const filteredData = this.dataManager.filterData(this.currentFilters);
      await this.chartRenderer.renderAll(filteredData);
    }

    /**
     * Show loading state
     */
    showLoading() {
      const container = document.getElementById('seasonal-patterns-dashboard');
      if (container) {
        container.classList.add('loading');
        container.setAttribute('aria-busy', 'true');
        // Add screen reader announcement
        const loadingMsg = document.createElement('div');
        loadingMsg.setAttribute('role', 'status');
        loadingMsg.setAttribute('aria-live', 'polite');
        loadingMsg.className = 'sr-only';
        loadingMsg.textContent = this.translations.loading || 'Loading data...';
        container.prepend(loadingMsg);
      }
    }

    /**
     * Hide loading state
     */
    hideLoading() {
      const container = document.getElementById('seasonal-patterns-dashboard');
      if (container) {
        container.classList.remove('loading');
        container.removeAttribute('aria-busy');
        // Remove screen reader loading message
        const loadingMsg = container.querySelector('[role="status"]');
        if (loadingMsg) {
          loadingMsg.remove();
        }
      }
    }

    /**
     * Show error message
     */
    showError() {
      const container = document.getElementById('seasonal-patterns-dashboard');
      if (container) {
        // Clear existing content
        while (container.firstChild) {
          container.removeChild(container.firstChild);
        }

        // Create error elements using DOM methods
        const errorWrapper = document.createElement('div');
        errorWrapper.className = 'error-message';
        errorWrapper.setAttribute('role', 'alert');

        const errorText = document.createElement('p');
        const message = this.translations.error || TRANSLATIONS.en.error;
        errorText.textContent = `⚠️ ${message}`;

        errorWrapper.appendChild(errorText);
        container.appendChild(errorWrapper);
      }
    }
  }

  // ============================================================================
  // Initialize on DOM ready
  // ============================================================================
  
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initDashboard);
  } else {
    initDashboard();
  }

  function initDashboard() {
    const dashboardContainer = document.getElementById('seasonal-patterns-dashboard');
    if (dashboardContainer) {
      const dashboard = new SeasonalPatternsDashboard();
      dashboard.initialize();
    }
  }

})();