/**
* @module CoalitionIntelligence/StatusAnalysis
* @category Political Analysis - Coalition Dynamics & Party Behavior
*
* @description
* **Coalition Status Intelligence & Party Dynamics Analyzer**
*
* Real-time intelligence module for monitoring Swedish coalition formations,
* party membership dynamics, leadership roles, and political alignment patterns.
* Provides strategic assessment of the Tidö Agreement coalition (October 2022-)
* and comprehensive party-level metrics across all 8 Riksdag parties.
*
* ## Intelligence Focus
*
* **Strategic Coalition Monitoring**:
* - **Tidö Agreement**: M-SD-KD-L four-party coalition (2022-present)
* - **Government Formation**: Minority coalition with external support
* - **Stability Metrics**: Member cohesion, voting discipline, policy alignment
* - **Alternative Scenarios**: Potential realignment patterns
*
* ## Data Sources & Coverage
*
* **CIA Platform Exports** (4 primary datasets):
* 1. `view_riksdagen_party_summary_sample.csv` - Party statistics
* - Current member counts, committee assignments, leadership positions
* 2. `view_riksdagen_party_role_member_sample.csv` - Leadership roles
* - Party leaders, parliamentary group leaders, committee chairs
* 3. `view_riksdagen_politician_sample.csv` - MP profiles
* - Individual assignments, experience, voting records
* 4. `view_riksdagen_politician_experience_summary_sample.csv` - Experience data
* - Parliamentary tenure, committee experience, leadership history
*
* **Update Frequency**:
* - Freshness Threshold: 7 days (weekly update cycle)
* - Cache Strategy: localStorage with staleness detection
* - Retry Logic: 3 attempts with 2-second backoff
*
* ## Party Intelligence Profiles
*
* **8 Swedish Political Parties**:
*
* | Party | Code | Color | Ideology | Coalition Status |
* |-------|------|----------|------------------|------------------|
* | S | S | #E8112d | Social Democrat | Opposition |
* | M | M | #52BDEC | Conservative | Government |
* | SD | SD | #DDDD00 | Right Populist | External Support |
* | C | C | #009933 | Centre-Right | Opposition |
* | V | V | #DA291C | Left Socialist | Opposition |
* | KD | KD | #000077 | Christian Dem | Government |
* | L | L | #006AB3 | Liberal | Government |
* | MP | MP | #83CF39 | Green | Opposition |
*
* ## Coalition Analysis Metrics
*
* **Key Performance Indicators**:
* - **Membership Strength**: Active MPs per party (175 left + 174 right bloc)
* - **Committee Influence**: Committee chair distribution analysis
* - **Leadership Stability**: Party leader tenure tracking
* - **Policy Cohesion**: Voting discipline within coalition
* - **External Support**: SD cooperation patterns with government
*
* ## Intelligence Methodologies
*
* **Analytical Techniques**:
* - **Coalition Mapping**: Network analysis of party relationships
* - **Power Balance**: Seat distribution and influence metrics
* - **Stability Assessment**: Historical coalition patterns (1971-2024)
* - **Scenario Planning**: Alternative government formations
* - **Predictive Modeling**: Coalition longevity forecasts
*
* ## OSINT Data Pipeline
*
* **Multi-Source Strategy**:
* ```
* Primary: cia-data/party/view_riksdagen_party_summary_sample.csv (local)
* Fallback: GitHub Raw API (CIA repository master branch)
* Cache: localStorage with 7-day TTL
* Validation: Schema checks, data completeness, freshness validation
* ```
*
* **Data Quality Assurance**:
* - CSV format validation (UTF-8, comma-delimited)
* - Required fields: party_id, party_short_name, member_count, role
* - Range validation: member_count > 0, valid party codes
* - Referential integrity: Cross-dataset party ID consistency
*
* ## Visualization & Reporting
*
* **Dashboard Components**:
* 1. Coalition Status Card - Current government composition
* 2. Party Strength Chart - Member counts and distribution
* 3. Leadership Roster - Key political figures
* 4. Committee Power Map - Committee chair distribution
* 5. Historical Trends - Coalition stability over time
*
* ## Multi-Language Support
*
* **14 Languages**:
* - Western: EN, SV, DA, NO, FI, DE, FR, ES, NL
* - Middle Eastern: AR, HE (RTL support)
* - East Asian: JA, KO, ZH
*
* **Localized Content**:
* - Party names (official vs. colloquial)
* - Coalition terminology
* - Political system concepts
*
* ## GDPR Compliance
*
* @gdpr Public political data only (Article 9(2)(e) - manifestly public)
* All data sourced from official Riksdag records. No private information processed.
* Party membership lists are public record per Swedish transparency laws.
*
* ## Security Considerations
*
* @security Low risk - Public data only, read-only operations
* @risk Party affiliation data could be used for targeted political campaigns
*
* **Mitigation**:
* - Data already public in Riksdag database
* - No aggregation beyond official statistics
* - XSS protection via CSP headers
*
* ## Performance Optimization
*
* **Caching Strategy**:
* - localStorage: 7-day cache reduces GitHub API calls
* - Staleness detection: Auto-refresh on threshold breach
* - Parallel loading: 4 CSV files fetched concurrently
* - Error resilience: Graceful degradation with cached data
*
* @intelligence Coalition dynamics analysis, party behavior monitoring
* @osint Multi-source party data with fallback redundancy
* @risk Political affiliation exposure, coalition strategy visibility
*
* @author Hack23 AB - Coalition Intelligence Unit
* @license Apache-2.0
* @version 1.0.0
* @since 2024
*
* @see {@link https://github.com/Hack23/cia|CIA Platform Data Pipeline}
* @see {@link party-dashboard.js|Party Performance Analytics}
* @see {@link https://www.riksdagen.se|Official Riksdag Source}
*/
(function() {
'use strict';
// Configuration
const CONFIG = {
githubRawBase: 'https://raw.githubusercontent.com/Hack23/cia/master/service.data.impl/sample-data',
dataSources: {
partySummary: 'view_riksdagen_party_summary_sample.csv',
partyRoles: 'view_riksdagen_party_role_member_sample.csv',
politicianData: 'view_riksdagen_politician_sample.csv',
experienceData: 'view_riksdagen_politician_experience_summary_sample.csv'
},
freshnessThreshold: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
cachePrefix: 'coalition_data_',
retryDelay: 2000,
maxRetries: 3
};
// Party metadata with official names and colors
const PARTY_INFO = {
'S': { name: 'Social Democrats', nameShort: 'S', color: '#E8112d', fullName: 'Socialdemokraterna' },
'M': { name: 'Moderates', nameShort: 'M', color: '#52BDEC', fullName: 'Moderaterna' },
'SD': { name: 'Sweden Democrats', nameShort: 'SD', color: '#DDDD00', fullName: 'Sverigedemokraterna' },
'C': { name: 'Centre Party', nameShort: 'C', color: '#009933', fullName: 'Centerpartiet' },
'V': { name: 'Left Party', nameShort: 'V', color: '#DA291C', fullName: 'Vänsterpartiet' },
'KD': { name: 'Christian Democrats', nameShort: 'KD', color: '#000077', fullName: 'Kristdemokraterna' },
'L': { name: 'Liberals', nameShort: 'L', color: '#006AB3', fullName: 'Liberalerna' },
'MP': { name: 'Green Party', nameShort: 'MP', color: '#83CF39', fullName: 'Miljöpartiet' }
};
// Multi-language translations
const TRANSLATIONS = {
en: {
coalitionTitle: 'Current Coalition: Tidö Agreement',
coalitionStatus: 'Formation: October 2022 | Status: Active',
parliamentSeats: 'Parliament seats',
governmentMembers: 'Government members',
partyAssignments: 'Party assignments',
leader: 'Leader',
groupLeader: 'Group Leader',
yearsInPolitics: 'Years in politics',
totalDocuments: 'Documents authored',
activityLevel: 'Activity level',
specialization: 'Focus area',
partyFocused: 'Party-focused',
committeeFocused: 'Committee-focused',
loadingMessage: 'Loading coalition data...',
errorMessage: 'Unable to load coalition data',
dataAttribution: 'Data from CIA Platform',
lastUpdated: 'Last Updated'
},
sv: {
coalitionTitle: 'Nuvarande koalition: Tidöavtalet',
coalitionStatus: 'Bildande: oktober 2022 | Status: Aktiv',
parliamentSeats: 'Riksdagsmandat',
governmentMembers: 'Regeringsmedlemmar',
partyAssignments: 'Partiuppdrag',
leader: 'Partiledare',
groupLeader: 'Gruppledare',
yearsInPolitics: 'År i politiken',
totalDocuments: 'Dokument författade',
activityLevel: 'Aktivitetsnivå',
specialization: 'Fokusområde',
partyFocused: 'Partifokuserad',
committeeFocused: 'Utskottsfokuserad',
loadingMessage: 'Laddar koalitionsdata...',
errorMessage: 'Kunde inte ladda koalitionsdata',
dataAttribution: 'Data från CIA-plattformen',
lastUpdated: 'Senast uppdaterad'
},
da: {
coalitionTitle: 'Nuværende koalition: Tidö-aftalen',
coalitionStatus: 'Dannelse: oktober 2022 | Status: Aktiv',
parliamentSeats: 'Rigsdagsmandater',
governmentMembers: 'Regeringsmedlemmer',
partyAssignments: 'Partiopgaver',
leader: 'Leder',
groupLeader: 'Gruppeleder',
yearsInPolitics: 'År i politik',
totalDocuments: 'Dokumenter forfattet',
activityLevel: 'Aktivitetsniveau',
specialization: 'Fokusområde',
partyFocused: 'Partifokuseret',
committeeFocused: 'Udvalgsfokuseret',
loadingMessage: 'Indlæser koalitionsdata...',
errorMessage: 'Kunne ikke indlæse koalitionsdata',
dataAttribution: 'Data fra CIA-platformen',
lastUpdated: 'Senest opdateret'
},
no: {
coalitionTitle: 'Nåværende koalisjon: Tidö-avtalen',
coalitionStatus: 'Dannelse: oktober 2022 | Status: Aktiv',
parliamentSeats: 'Riksdagsmandater',
governmentMembers: 'Regjeringsmedlemmer',
partyAssignments: 'Partioppgaver',
leader: 'Leder',
groupLeader: 'Gruppeleder',
yearsInPolitics: 'År i politikken',
totalDocuments: 'Dokumenter forfattet',
activityLevel: 'Aktivitetsnivå',
specialization: 'Fokusområde',
partyFocused: 'Partifokusert',
committeeFocused: 'Komitéfokusert',
loadingMessage: 'Laster koalisjonsdata...',
errorMessage: 'Kunne ikke laste koalisjonsdata',
dataAttribution: 'Data fra CIA-plattformen',
lastUpdated: 'Sist oppdatert'
},
de: {
coalitionTitle: 'Aktuelle Koalition: Tidö-Vereinbarung',
coalitionStatus: 'Bildung: Oktober 2022 | Status: Aktiv',
parliamentSeats: 'Sitze im schwedischen Reichstag',
governmentMembers: 'Regierungsmitglieder',
partyAssignments: 'Parteiaufgaben',
leader: 'Vorsitzender',
groupLeader: 'Fraktionsvorsitzender',
yearsInPolitics: 'Jahre in der Politik',
totalDocuments: 'Verfasste Dokumente',
activityLevel: 'Aktivitätsniveau',
specialization: 'Schwerpunktbereich',
partyFocused: 'Parteifokussiert',
committeeFocused: 'Ausschussfokussiert',
loadingMessage: 'Koalitionsdaten werden geladen...',
errorMessage: 'Koalitionsdaten konnten nicht geladen werden',
dataAttribution: 'Daten von der CIA-Plattform',
lastUpdated: 'Zuletzt aktualisiert'
},
fr: {
coalitionTitle: 'Coalition actuelle : Accord de Tidö',
coalitionStatus: 'Formation : octobre 2022 | Statut : Actif',
parliamentSeats: 'Sièges au Riksdag suédois',
governmentMembers: 'Membres du gouvernement',
partyAssignments: 'Affectations de parti',
leader: 'Chef',
groupLeader: 'Chef de groupe',
yearsInPolitics: 'Années en politique',
totalDocuments: 'Documents rédigés',
activityLevel: 'Niveau d\'activité',
specialization: 'Domaine d\'expertise',
partyFocused: 'Axé parti',
committeeFocused: 'Axé comité',
loadingMessage: 'Chargement des données de coalition...',
errorMessage: 'Impossible de charger les données de coalition',
dataAttribution: 'Données de la plateforme CIA',
lastUpdated: 'Dernière mise à jour'
},
es: {
coalitionTitle: 'Coalición actual: Acuerdo de Tidö',
coalitionStatus: 'Formación: octubre 2022 | Estado: Activo',
parliamentSeats: 'Escaños del Riksdag sueco',
governmentMembers: 'Miembros del gobierno',
partyAssignments: 'Asignaciones de partido',
leader: 'Líder',
groupLeader: 'Líder del grupo',
yearsInPolitics: 'Años en política',
totalDocuments: 'Documentos escritos',
activityLevel: 'Nivel de actividad',
specialization: 'Área de enfoque',
partyFocused: 'Enfocado en partido',
committeeFocused: 'Enfocado en comité',
loadingMessage: 'Cargando datos de coalición...',
errorMessage: 'No se pudieron cargar los datos de coalición',
dataAttribution: 'Datos de la plataforma CIA',
lastUpdated: 'Última actualización'
},
fi: {
coalitionTitle: 'Nykyinen koalitio: Tidö-sopimus',
coalitionStatus: 'Muodostus: lokakuu 2022 | Tila: Aktiivinen',
parliamentSeats: 'Riksdagin paikat',
governmentMembers: 'Hallituksen jäseniä',
partyAssignments: 'Puoluetehtävät',
leader: 'Johtaja',
groupLeader: 'Ryhmänjohtaja',
yearsInPolitics: 'Vuotta politiikassa',
totalDocuments: 'Kirjoitettuja asiakirjoja',
activityLevel: 'Aktiivisuustaso',
specialization: 'Painopistealue',
partyFocused: 'Puoluepainotteinen',
committeeFocused: 'Valiokuntapainotteinen',
loadingMessage: 'Ladataan koalitiotietoja...',
errorMessage: 'Koalitiotietoja ei voitu ladata',
dataAttribution: 'Tiedot CIA-alustalta',
lastUpdated: 'Viimeksi päivitetty'
},
nl: {
coalitionTitle: 'Huidige coalitie: Tidö-akkoord',
coalitionStatus: 'Vorming: oktober 2022 | Status: Actief',
parliamentSeats: 'Zetels in het Zweedse Rijksdag',
governmentMembers: 'Regeringsleden',
partyAssignments: 'Partijfuncties',
leader: 'Leider',
groupLeader: 'Fractievoorzitter',
yearsInPolitics: 'Jaren in de politiek',
totalDocuments: 'Geschreven documenten',
activityLevel: 'Activiteitsniveau',
specialization: 'Focusgebied',
partyFocused: 'Partijgericht',
committeeFocused: 'Commissiegericht',
loadingMessage: 'Coalitiegegevens laden...',
errorMessage: 'Kan coalitiegegevens niet laden',
dataAttribution: 'Gegevens van het CIA-platform',
lastUpdated: 'Laatst bijgewerkt'
},
ar: {
coalitionTitle: 'الائتلاف الحالي: اتفاقية تيدو',
coalitionStatus: 'التشكيل: أكتوبر 2022 | الحالة: نشط',
parliamentSeats: 'مقاعد البرلمان',
governmentMembers: 'أعضاء الحكومة',
partyAssignments: 'مهام الحزب',
leader: 'القائد',
groupLeader: 'قائد المجموعة',
yearsInPolitics: 'سنوات في السياسة',
totalDocuments: 'الوثائق المكتوبة',
activityLevel: 'مستوى النشاط',
specialization: 'مجال التركيز',
partyFocused: 'التركيز على الحزب',
committeeFocused: 'التركيز على اللجنة',
loadingMessage: 'جاري تحميل بيانات الائتلاف...',
errorMessage: 'تعذر تحميل بيانات الائتلاف',
dataAttribution: 'البيانات من منصة CIA',
lastUpdated: 'آخر تحديث'
},
he: {
coalitionTitle: 'קואליציה נוכחית: הסכם טידו',
coalitionStatus: 'הקמה: אוקטובר 2022 | סטטוס: פעיל',
parliamentSeats: 'מושבי פרלמנט',
governmentMembers: 'חברי ממשלה',
partyAssignments: 'משימות מפלגה',
leader: 'מנהיג',
groupLeader: 'מנהיג הקבוצה',
yearsInPolitics: 'שנים בפוליטיקה',
totalDocuments: 'מסמכים שנכתבו',
activityLevel: 'רמת פעילות',
specialization: 'תחום התמחות',
partyFocused: 'ממוקד מפלגה',
committeeFocused: 'ממוקד וועדה',
loadingMessage: 'טוען נתוני קואליציה...',
errorMessage: 'לא ניתן לטעון נתוני קואליציה',
dataAttribution: 'נתונים מפלטפורמת CIA',
lastUpdated: 'עודכן לאחרונה'
},
ja: {
coalitionTitle: '現在の連立:ティドー協定',
coalitionStatus: '形成:2022年10月 | ステータス:アクティブ',
parliamentSeats: '国会議席',
governmentMembers: '政府メンバー',
partyAssignments: '党の任務',
leader: 'リーダー',
groupLeader: 'グループリーダー',
yearsInPolitics: '政治活動年数',
totalDocuments: '作成文書',
activityLevel: '活動レベル',
specialization: '専門分野',
partyFocused: '政党重視',
committeeFocused: '委員会重視',
loadingMessage: '連立データを読み込んでいます...',
errorMessage: '連立データを読み込めませんでした',
dataAttribution: 'CIAプラットフォームのデータ',
lastUpdated: '最終更新'
},
ko: {
coalitionTitle: '현재 연립: 티도 협정',
coalitionStatus: '구성: 2022년 10월 | 상태: 활성',
parliamentSeats: '의회 의석',
governmentMembers: '정부 구성원',
partyAssignments: '당 임무',
leader: '리더',
groupLeader: '그룹 리더',
yearsInPolitics: '정치 경력',
totalDocuments: '작성 문서',
activityLevel: '활동 수준',
specialization: '전문 분야',
partyFocused: '정당 중심',
committeeFocused: '위원회 중심',
loadingMessage: '연립 데이터 로드 중...',
errorMessage: '연립 데이터를 로드할 수 없습니다',
dataAttribution: 'CIA 플랫폼의 데이터',
lastUpdated: '마지막 업데이트'
},
zh: {
coalitionTitle: '当前联盟:蒂德协议',
coalitionStatus: '成立:2022年10月 | 状态:活跃',
parliamentSeats: '议会席位',
governmentMembers: '政府成员',
partyAssignments: '党派任务',
leader: '领导',
groupLeader: '团队领导',
yearsInPolitics: '从政年数',
totalDocuments: '撰写文件',
activityLevel: '活动水平',
specialization: '专注领域',
partyFocused: '政党导向',
committeeFocused: '委员会导向',
loadingMessage: '正在加载联盟数据...',
errorMessage: '无法加载联盟数据',
dataAttribution: '来自CIA平台的数据',
lastUpdated: '最后更新'
}
};
// Detect current language from HTML lang attribute
function getCurrentLanguage() {
const htmlLang = document.documentElement.lang || 'en';
return htmlLang.substring(0, 2); // Get first 2 chars (en, sv, etc.)
}
// Get translations for current language with fallback to English
function getTranslations() {
const lang = getCurrentLanguage();
return TRANSLATIONS[lang] || TRANSLATIONS.en;
}
/**
* Parse CSV string into array of objects
* @param {string} csvText - CSV content
* @returns {Array<Object>} Parsed data
*/
function parseCSV(csvText) {
const lines = csvText.trim().split('\n');
if (lines.length < 2) return [];
const headers = lines[0].split(',').map(h => h.trim());
const data = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',');
if (values.length !== headers.length) continue;
const row = {};
headers.forEach((header, idx) => {
row[header] = values[idx].trim();
});
data.push(row);
}
return data;
}
/**
* Check if cached data is fresh
* @param {string} key - Cache key
* @returns {boolean} True if data is fresh
*/
function isCacheFresh(key) {
try {
const cached = localStorage.getItem(CONFIG.cachePrefix + key);
if (!cached) return false;
const data = JSON.parse(cached);
const age = Date.now() - data.timestamp;
return age < CONFIG.freshnessThreshold;
} catch (e) {
console.error('Cache check error:', e);
return false;
}
}
/**
* Get cached data if fresh
* @param {string} key - Cache key
* @returns {*} Cached data or null
*/
function getCachedData(key) {
try {
if (!isCacheFresh(key)) return null;
const cached = localStorage.getItem(CONFIG.cachePrefix + key);
return cached ? JSON.parse(cached).data : null;
} catch (e) {
console.error('Cache retrieval error:', e);
return null;
}
}
/**
* Cache data with timestamp
* @param {string} key - Cache key
* @param {*} data - Data to cache
*/
function setCachedData(key, data) {
try {
const cacheObject = {
data: data,
timestamp: Date.now()
};
localStorage.setItem(CONFIG.cachePrefix + key, JSON.stringify(cacheObject));
} catch (e) {
console.error('Cache storage error:', e);
}
}
/**
* Fetch CSV data from GitHub with retry logic
* @param {string} filename - CSV filename
* @param {number} retryCount - Current retry attempt
* @returns {Promise<string>} CSV content
*/
async function fetchCSV(filename, retryCount = 0) {
const url = `${CONFIG.githubRawBase}/${filename}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.text();
} catch (error) {
console.error(`Fetch error for ${filename} (attempt ${retryCount + 1}):`, error);
if (retryCount < CONFIG.maxRetries) {
await new Promise(resolve => setTimeout(resolve, CONFIG.retryDelay));
return fetchCSV(filename, retryCount + 1);
}
throw error;
}
}
/**
* Load party summary data (cached)
* @returns {Promise<Array>} Party summary data
*/
async function loadPartySummary() {
const cacheKey = 'party_summary';
const cached = getCachedData(cacheKey);
if (cached) {
console.log('Using cached party summary data');
return cached;
}
console.log('Fetching party summary from GitHub...');
const csvText = await fetchCSV(CONFIG.dataSources.partySummary);
const data = parseCSV(csvText);
// Filter for active parties only
const activeParties = data.filter(row => row.active === 't');
setCachedData(cacheKey, activeParties);
return activeParties;
}
/**
* Load party role/leader data (cached)
* @returns {Promise<Array>} Party role data
*/
async function loadPartyRoles() {
const cacheKey = 'party_roles';
const cached = getCachedData(cacheKey);
if (cached) {
console.log('Using cached party roles data');
return cached;
}
console.log('Fetching party roles from GitHub...');
const csvText = await fetchCSV(CONFIG.dataSources.partyRoles);
const data = parseCSV(csvText);
// Filter for active party leaders and group leaders
const leaders = data.filter(row =>
row.active === 't' &&
(row.role_code === 'Partiledare' || row.role_code === 'Gruppledare')
);
setCachedData(cacheKey, leaders);
return leaders;
}
/**
* Load politician detailed data (cached)
* @returns {Promise<Array>} Politician data
*/
async function loadPoliticianData() {
const cacheKey = 'politician_data';
const cached = getCachedData(cacheKey);
if (cached) {
console.log('Using cached politician data');
return cached;
}
console.log('Fetching politician data from GitHub...');
const csvText = await fetchCSV(CONFIG.dataSources.politicianData);
const data = parseCSV(csvText);
setCachedData(cacheKey, data);
return data;
}
/**
* Load politician experience summary data (cached)
* @returns {Promise<Array>} Experience data
*/
async function loadExperienceData() {
const cacheKey = 'experience_data';
const cached = getCachedData(cacheKey);
if (cached) {
console.log('Using cached experience data');
return cached;
}
console.log('Fetching experience data from GitHub...');
const csvText = await fetchCSV(CONFIG.dataSources.experienceData);
const data = parseCSV(csvText);
setCachedData(cacheKey, data);
return data;
}
/**
* Get party leader name from role data
* @param {Array} roleData - Party role data
* @param {string} partyCode - Party code (e.g., 'M', 'SD')
* @returns {Object} Leader info { name, roleType: 'leader'|'groupLeader' }
*/
function getPartyLeader(roleData, partyCode) {
// Prioritize Partiledare (Party Leader) over Gruppledare (Group Leader)
const partyLeader = roleData.find(row =>
row.party === partyCode && row.role_code === 'Partiledare'
);
if (partyLeader) {
return {
name: `${partyLeader.first_name} ${partyLeader.last_name}`,
roleType: 'leader',
personId: partyLeader.person_id
};
}
const groupLeader = roleData.find(row =>
row.party === partyCode && row.role_code === 'Gruppledare'
);
if (groupLeader) {
return {
name: `${groupLeader.first_name} ${groupLeader.last_name}`,
roleType: 'groupLeader',
personId: groupLeader.person_id
};
}
return { name: 'Unknown', roleType: 'leader', personId: null };
}
/**
* Get enhanced leader information
* @param {Object} leader - Basic leader info
* @param {Array} politicianData - Politician data
* @param {Array} experienceData - Experience data
* @returns {Object} Enhanced leader info
*/
function getEnhancedLeaderInfo(leader, politicianData, experienceData) {
if (!leader.personId) return leader;
// Find politician data
const politician = politicianData.find(p => p.person_id === leader.personId);
if (!politician) return leader;
// Calculate years in politics
const firstDate = new Date(politician.first_assignment_date);
const yearsInPolitics = Math.floor((Date.now() - firstDate.getTime()) / (365.25 * 24 * 60 * 60 * 1000));
// Get document activity
const totalDocs = parseInt(politician.total_documents, 10) || 0;
const activityLevel = politician.doc_activity_level || 'Unknown';
// Determine specialization
const partyDocs = parseInt(politician.party_motions, 10) || 0;
const committeeDocs = parseInt(politician.committee_motions, 10) || 0;
let specialization = 'Balanced';
if (partyDocs > committeeDocs * 2) {
specialization = 'Party-focused';
} else if (committeeDocs > partyDocs * 2) {
specialization = 'Committee-focused';
}
return {
...leader,
yearsInPolitics,
totalDocuments: totalDocs,
activityLevel,
specialization
};
}
/**
* Render coalition cards
* @param {Array} partySummary - Party summary data
* @param {Array} partyRoles - Party role data
* @param {Array} politicianData - Politician data (optional)
* @param {Array} experienceData - Experience data (optional)
*/
function renderCoalition(partySummary, partyRoles, politicianData = [], experienceData = []) {
const container = document.getElementById('coalition-status');
if (!container) {
console.error('Coalition status container not found');
return;
}
const t = getTranslations();
const cardsContainer = container.querySelector('.cards');
if (!cardsContainer) {
console.error('Cards container not found');
return;
}
// Clear existing cards
cardsContainer.innerHTML = '';
// Filter to known parties only (have PARTY_INFO entry)
const knownParties = partySummary.filter(party => PARTY_INFO[party.party]);
// Sort parties by parliament seats (descending)
const sortedParties = [...knownParties].sort((a, b) => {
const seatsA = parseInt(a.total_active_parliament, 10) || 0;
const seatsB = parseInt(b.total_active_parliament, 10) || 0;
return seatsB - seatsA;
});
// Calculate total seats (only from known parties)
const totalSeats = sortedParties.reduce((sum, party) => {
return sum + (parseInt(party.total_active_parliament, 10) || 0);
}, 0);
// Render each party card
sortedParties.forEach(party => {
const partyCode = party.party;
const partyInfo = PARTY_INFO[partyCode];
if (!partyInfo) return; // Skip unknown parties (already filtered, but defensive)
const parliamentSeats = parseInt(party.total_active_parliament, 10) || 0;
const governmentMembers = parseInt(party.total_active_government, 10) || 0;
const partyAssignments = parseInt(party.current_party_assignments, 10) || 0;
const basicLeader = getPartyLeader(partyRoles, partyCode);
const leader = getEnhancedLeaderInfo(basicLeader, politicianData, experienceData);
const leaderLabel = t[leader.roleType] || t.leader; // Use roleType to select label
// Create card using safe DOM APIs (XSS prevention)
const card = document.createElement('div');
card.className = 'card';
// Scanner effect
const scanner = document.createElement('div');
scanner.className = 'scanner-effect';
card.appendChild(scanner);
// Party heading
const heading = document.createElement('h3');
heading.textContent = `${partyInfo.name} (${partyCode})`;
card.appendChild(heading);
// Party stats container
const partyStats = document.createElement('div');
partyStats.className = 'party-stats';
// Parliament seats
const seatsP = document.createElement('p');
const seatsStrong = document.createElement('strong');
seatsStrong.textContent = `${parliamentSeats} ${t.parliamentSeats}`;
seatsP.appendChild(seatsStrong);
partyStats.appendChild(seatsP);
// Government members (only if > 0)
if (governmentMembers > 0) {
const govP = document.createElement('p');
govP.textContent = `${governmentMembers} ${t.governmentMembers}`;
partyStats.appendChild(govP);
}
// Party assignments
const assignmentsP = document.createElement('p');
assignmentsP.textContent = `${partyAssignments} ${t.partyAssignments}`;
partyStats.appendChild(assignmentsP);
card.appendChild(partyStats);
// Party leader section
const leaderSection = document.createElement('div');
leaderSection.className = 'party-leader';
const leaderName = document.createElement('p');
const leaderStrong = document.createElement('strong');
leaderStrong.textContent = `${leaderLabel}:`;
leaderName.appendChild(leaderStrong);
leaderName.appendChild(document.createTextNode(` ${leader.name}`));
leaderSection.appendChild(leaderName);
// Enhanced leader information (if available)
if (leader.yearsInPolitics !== undefined) {
const leaderDetails = document.createElement('div');
leaderDetails.className = 'leader-details';
leaderDetails.style.fontSize = '0.9em';
leaderDetails.style.marginTop = '0.5rem';
// Years in politics
const yearsP = document.createElement('p');
yearsP.textContent = `${t.yearsInPolitics}: ${leader.yearsInPolitics}`;
yearsP.style.margin = '0.25rem 0';
leaderDetails.appendChild(yearsP);
// Documents authored
if (leader.totalDocuments > 0) {
const docsP = document.createElement('p');
docsP.textContent = `${t.totalDocuments}: ${leader.totalDocuments}`;
docsP.style.margin = '0.25rem 0';
leaderDetails.appendChild(docsP);
}
// Activity level
if (leader.activityLevel && leader.activityLevel !== 'Unknown') {
const activityP = document.createElement('p');
activityP.textContent = `${t.activityLevel}: ${leader.activityLevel}`;
activityP.style.margin = '0.25rem 0';
leaderDetails.appendChild(activityP);
}
// Specialization
if (leader.specialization && leader.specialization !== 'Balanced') {
const specP = document.createElement('p');
const specKey = leader.specialization === 'Party-focused' ? 'partyFocused' : 'committeeFocused';
specP.textContent = `${t.specialization}: ${t[specKey]}`;
specP.style.margin = '0.25rem 0';
leaderDetails.appendChild(specP);
}
leaderSection.appendChild(leaderDetails);
}
card.appendChild(leaderSection);
cardsContainer.appendChild(card);
});
// Update coalition status text
const statusP = container.querySelector('p');
if (statusP) {
statusP.textContent = `${t.coalitionStatus} | Total Seats: ${totalSeats} of 349`;
}
console.log(`Rendered ${sortedParties.length} active parties with ${totalSeats} total seats`);
}
/**
* Show loading state
*/
function showLoading() {
const container = document.getElementById('coalition-status');
if (!container) return;
const cardsContainer = container.querySelector('.cards');
if (cardsContainer) {
const t = getTranslations();
cardsContainer.innerHTML = `<p class="loading-message">${t.loadingMessage}</p>`;
}
}
/**
* Show error state
* @param {Error} error - Error object
*/
function showError(error) {
const container = document.getElementById('coalition-status');
if (!container) return;
const cardsContainer = container.querySelector('.cards');
if (cardsContainer) {
const t = getTranslations();
const errorP = document.createElement('p');
errorP.className = 'error-message';
errorP.textContent = `${t.errorMessage}: ${error.message}`;
cardsContainer.innerHTML = '';
cardsContainer.appendChild(errorP);
}
console.error('Coalition loader error:', error);
}
/**
* Initialize coalition loader
*/
async function init() {
try {
showLoading();
// Load data from CSV files (politician and experience data are optional for backward compatibility)
const [partySummary, partyRoles, politicianData, experienceData] = await Promise.all([
loadPartySummary(),
loadPartyRoles(),
loadPoliticianData().catch(err => {
console.warn('Could not load politician data:', err);
return [];
}),
loadExperienceData().catch(err => {
console.warn('Could not load experience data:', err);
return [];
})
]);
console.log('Loaded data:', {
parties: partySummary.length,
leaders: partyRoles.length,
politicians: politicianData.length,
experiences: experienceData.length
});
// Render coalition cards
renderCoalition(partySummary, partyRoles, politicianData, experienceData);
} catch (error) {
showError(error);
}
}
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Expose for manual refresh if needed
window.CoalitionLoader = {
refresh: init,
clearCache: function() {
Object.keys(localStorage).forEach(key => {
if (key.startsWith(CONFIG.cachePrefix)) {
localStorage.removeItem(key);
}
});
console.log('Coalition cache cleared');
}
};
})();