/**
* @module IndividualIntelligence/PoliticianProfiling
* @category Intelligence Analysis - Individual Politician Risk Assessment & Career Analytics
*
* @description
* **Individual Politician Career Analytics & Risk Intelligence Dashboard**
*
* Advanced intelligence profiling platform providing **micro-level politician assessment**
* across 349 Swedish members of parliament. Implements comprehensive risk scoring,
* influence hierarchy measurement, behavioral pattern analysis, and career trajectory
* forecasting using Chart.js and D3.js visualization. Monitors individual politician
* performance, behavioral anomalies, and career risk factors.
*
* ## Intelligence Methodology
*
* This module implements **individual-level political intelligence profiling**:
* - **Target Population**: 349 Swedish parliamentarians (Riksdagen members)
* - **Analysis Dimensions**: Risk, influence, behavioral patterns, career trajectory
* - **Temporal Coverage**: Full parliamentary career from first election to present
* - **Granularity**: Individual-level assessment with peer comparison context
*
* ## Individual Politician Intelligence Framework
*
* **Five-Dimensional Analysis Taxonomy**:
*
* 1. **Risk Assessment** (Individual Threat Profile)
* - Ethics violations and conduct concerns
* - Electoral vulnerability and reelection risk
* - Political isolation and coalition weakness
* - Career stability and burnout indicators
* - Personal scandal and reputation exposure
*
* 2. **Influence Measurement** (Individual Power Assessment)
* - Committee assignments and leadership roles
* - Speaking frequency and discourse leadership
* - Coalition-building capability and ally networks
* - Media prominence and public visibility
* - Decision-making authority in party structures
*
* 3. **Behavioral Pattern Analysis** (Anomaly Detection)
* - Voting deviation from party discipline
* - Attendance and participation consistency
* - Speech content and rhetoric evolution
* - Committee engagement and activity levels
* - Coalition alliance volatility and shifts
*
* 4. **Career Trajectory** (Professional Development)
* - Years of service and experience levels
* - Role progression (backbencher → committee → leadership)
* - Electoral performance trends
* - Party assignments and responsibilities
* - Generational cohort and peer advancement
*
* 5. **Influence Bucket Classification** (Politician Tiers)
* - Leadership tier (party leaders, cabinet ministers)
* - Influential tier (committee chairs, opinion leaders)
* - Standard tier (regular parliamentarians)
* - New/junior tier (first-term or new assignments)
*
* ## Data Sources (CIA Platform)
*
* **Primary Intelligence Feeds**:
* - `view_politician_risk_summary_sample.csv`
* * Fields: politician_id, name, party, risk_score (0-10), risk_level, risk_categories
* * Scope: Risk assessment for 349 individual MPs
* * Use: Risk profiling, threat identification, risk-based sorting
*
* - `view_riksdagen_politician_influence_metrics_sample.csv`
* * Fields: politician_id, name, influence_score (0-100), leadership_roles, speech_frequency
* * Scope: Individual influence measurement with component breakdown
* * Use: Influence hierarchy visualization, power assessment
*
* - `view_politician_behavioral_trends_sample.csv`
* * Fields: politician_id, year, voting_deviation_pct, attendance_rate, speech_sentiment
* * Scope: Annual behavioral metrics for anomaly detection
* * Use: Behavioral pattern recognition, consistency assessment
*
* - `distribution_experience_levels.csv`
* * Fields: politician_id, years_service, parliament_term_count, experience_level, avg_roles
* * Scope: Career experience and tenure statistics
* * Use: Experience profiling, junior/senior categorization
*
* - `distribution_influence_buckets.csv`
* * Fields: politician_id, influence_bucket (leadership/influential/standard/junior), bucket_rank
* * Scope: Categorization of 349 MPs into influence tiers
* * Use: Tier-based analysis, leadership pipeline tracking
*
* - `distribution_assignment_roles.csv`
* * Fields: politician_id, role_type, role_count, committee_assignments, leadership_count
* * Scope: Individual role assignments and responsibilities
* * Use: Role trajectory tracking, responsibility assessment
*
* ## OSINT Collection Strategy
*
* **Multi-Layer Individual Intelligence**:
* 1. **Parliamentary Records**: Voting records, speeches, committee participation
* 2. **Media Monitoring**: Coverage volume, sentiment, scandal tracking
* 3. **Social Media**: Engagement metrics, online presence, supporter networks
* 4. **Personal Background**: Declared conflicts, financial interests, organizational affiliations
* 5. **Electoral History**: Campaign performance, vote trends, constituency dynamics
* 6. **Network Analysis**: Coalition patterns, ally/rival relationships, influence circles
* 7. **Behavioral Metrics**: Speech analysis, consistency assessment, sentiment tracking
*
* ## Visualization Intelligence
*
* **Chart.js Risk Summary** (Primary):
* - **Risk Distribution Chart**: Population-wide risk distribution
* * Histogram showing risk score distribution across 349 MPs
* * Color-coded risk levels (green/yellow/orange/red)
* * Shows critical/high-risk outliers
*
* **Chart.js Influence Metrics** (Power Assessment):
* - **Influence Ranking Chart**: Top 50 most influential politicians
* * Horizontal bar chart ranked by influence score
* * Color segments for influence dimensions
* * Identifies power concentration vs. distributed influence
*
* **Chart.js Behavioral Trends** (Anomaly Detection):
* - **Behavioral Pattern Timeline**: Individual politician behavior over time
* * Multi-line chart showing voting deviation and participation trends
* * Identifies consistency, volatility, or anomalies
* * Flags behavioral changes and pattern breaks
*
* **Chart.js Experience Distribution** (Career):
* - **Experience Levels**: Distribution across experience categories
* * Grouped bar chart showing tenure statistics
* * Identifies junior/senior ratios and generational balance
* * Shows parliamentary turnover rates
*
* **Chart.js Role Distribution** (Responsibility):
* - **Assignment Roles**: Distribution of parliamentary roles
* * Stacked bar showing committee, leadership, and regular roles
* * Highlights responsibility concentration
* * Shows role progression paths
*
* ## Intelligence Analysis Frameworks Applied
*
* @intelligence
* - **Individual Risk Profiling**: Multi-factor individual threat assessment
* - **Behavioral Deviation Analysis**: Voting discipline and participation consistency
* - **Network Analysis**: Alliance patterns and influence circle mapping
* - **Career Trajectory Modeling**: Role progression and seniority assessment
* - **Anomaly Detection**: Unusual behavior and pattern deviation identification
*
* @osint
* - **Speech Analysis**: Rhetoric patterns and sentiment evolution tracking
* - **Voting Pattern Recognition**: Party discipline and coalition deviation detection
* - **Media Intelligence**: Coverage patterns and scandal accumulation
* - **Network Intelligence**: Influence propagation and coalition networks
*
* @risk
* - **Individual Vulnerability**: Reelection risk and scandal exposure
* - **Behavioral Anomalies**: Voting deviations and coalition instability
* - **Career Discontinuity**: Sudden role changes or influence loss
* - **Network Risk**: Contagion through ally networks and coalition vulnerability
*
* ## GDPR Compliance
*
* @gdpr Individual politician assessment uses only public information (Article 9(2)(e)):
* - Parliamentary voting records (public official acts)
* - Committee participation and role assignments (public record)
* - Speeches and public statements (public domain)
* - Media coverage (published information)
* - Electoral results (public official data)
* No private personal data, health information, or family details.
* No processing of political beliefs beyond official voting records.
* No predictive profiling for targeting or manipulation.
*
* ## Security Architecture
*
* @security Chart.js rendering with XSS-safe data binding
* @security No individual-level personal data exposure
* @security CSV data validation with type checking and range enforcement
* @security Risk assessment algorithm assumptions disclosed transparently
* @security No authentication required; all data is public record
* @risk Medium - Individual risk scores may be used to identify "political risks"
*
* ## Performance Characteristics
*
* - **Data Volume**: 349 politicians × 5-10 data attributes = ~2,000 data points
* - **Risk Assessment**: 349 individual risk profiles with score distribution
* - **Influence Metrics**: 349 individual influence measurements with component breakdown
* - **Behavioral Trends**: 349 × 5-10 years = ~3,500+ behavioral data points
* - **Memory**: <3MB for complete politician intelligence dataset
* - **Rendering**: Chart.js with 5-6 separate visualizations
*
* ## Data Transformation Pipeline
*
* **Load Strategy**:
* 1. Attempt local cache load (`cia-data/politician/`)
* 2. Parse CSV files into politician-centric structure
* 3. Fallback to remote GitHub repository if local unavailable
* 4. Consolidate multiple sources by politician_id
* 5. Cache results with 24-hour expiry
* 6. Render visualizations with aggregated/transformed data
*
* **Data Aggregation**:
* - Risk: Single score from risk_summary source
* - Influence: Normalize component scores and aggregate (0-100 scale)
* - Behavioral: Time-series aggregation by politician per year
* - Experience: Tenure calculation from election history
* - Roles: Count and categorize assignments by type
*
* @author Hack23 AB - Individual 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://www.riksdagen.se|Riksdag Official Site}
* @see {@link ./THREAT_MODEL.md|Threat Model Documentation}
* @see {@link ./SECURITY_ARCHITECTURE.md|Security Architecture}
*/
// Data source base paths
const LOCAL_DATA_BASE = 'cia-data';
const CIA_DATA_BASE_URL = 'https://raw.githubusercontent.com/Hack23/cia/master/service.data.impl/sample-data';
const DATA_SOURCES = {
riskSummary: [
`${LOCAL_DATA_BASE}/politician/view_politician_risk_summary_sample.csv`,
`${CIA_DATA_BASE_URL}/politician/view_politician_risk_summary_sample.csv`
],
influenceMetrics: [
`${LOCAL_DATA_BASE}/politician/view_riksdagen_politician_influence_metrics_sample.csv`,
`${CIA_DATA_BASE_URL}/politician/view_riksdagen_politician_influence_metrics_sample.csv`
],
behavioralTrends: [
`${LOCAL_DATA_BASE}/politician/view_politician_behavioral_trends_sample.csv`,
`${CIA_DATA_BASE_URL}/politician/view_politician_behavioral_trends_sample.csv`
],
experienceLevels: [
`${LOCAL_DATA_BASE}/politician/distribution_experience_levels.csv`,
`${CIA_DATA_BASE_URL}/politician/distribution_experience_levels.csv`
],
influenceBuckets: [
`${LOCAL_DATA_BASE}/politician/distribution_influence_buckets.csv`,
`${CIA_DATA_BASE_URL}/politician/distribution_influence_buckets.csv`
],
assignmentRoles: [
`${LOCAL_DATA_BASE}/politician/distribution_assignment_roles.csv`,
`${CIA_DATA_BASE_URL}/politician/distribution_assignment_roles.csv`
]
};
// Data cache
const dataCache = {
riskSummary: null,
influenceMetrics: null,
behavioralTrends: null,
experienceLevels: null,
influenceBuckets: null,
assignmentRoles: null
};
/**
* Fetch CSV data with local-first, remote-fallback strategy
* @param {string[]} urls - Array of [localUrl, remoteUrl]
* @returns {Promise<Array>} Parsed CSV data
*/
async function fetchCIAData(urls) {
const urlList = Array.isArray(urls) ? urls : [urls];
let lastError = null;
for (const url of urlList) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
}
const text = await response.text();
const data = parseCSV(text);
if (data.length > 0) return data;
} catch (error) {
lastError = error;
console.warn(`Fetch failed for ${url}, trying next source...`);
}
}
console.error('All data sources failed:', lastError);
return [];
}
/**
* Parse CSV text to array of objects
* @param {string} csvText - CSV text content
* @returns {Array<Object>} Parsed data
*/
function parseCSV(csvText) {
const lines = csvText.trim().split('\n');
if (lines.length < 2) return [];
const rawHeaders = parseCSVLine(lines[0]);
const headers = rawHeaders.map(h =>
h.trim().replace(/^\uFEFF?"/, '').replace(/"$/, '')
);
const data = [];
for (let i = 1; i < lines.length; i++) {
const values = parseCSVLine(lines[i]);
if (values.length !== headers.length) continue;
const row = {};
headers.forEach((header, index) => {
row[header] = values[index];
});
data.push(row);
}
return data;
}
/**
* Parse a single CSV line handling quoted fields
* @param {string} line - CSV line
* @returns {Array<string>} Parsed values
*/
function parseCSVLine(line) {
const values = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
const nextChar = line[i + 1];
if (char === '"') {
if (inQuotes && nextChar === '"') {
current += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
values.push(current.trim());
current = '';
} else {
current += char;
}
}
values.push(current.trim());
return values;
}
/**
* Render Top 10 list
* @param {string} containerId - Container element ID
* @param {Array} data - Top 10 data
* @param {string} scoreLabel - Label for score column
*/
function renderTop10List(containerId, data, scoreLabel = 'Score') {
const container = document.getElementById(containerId);
if (!container) return;
if (!data || data.length === 0) {
container.textContent = '';
const errorElement = document.createElement('div');
errorElement.className = 'error-message';
errorElement.textContent = 'No data available';
container.appendChild(errorElement);
return;
}
const ul = document.createElement('ul');
ul.className = 'top10-list';
ul.setAttribute('role', 'list');
data.slice(0, 10).forEach((item, index) => {
const li = document.createElement('li');
li.setAttribute('role', 'listitem');
const rank = document.createElement('span');
rank.className = 'rank';
rank.textContent = `${index + 1}`;
rank.setAttribute('aria-label', `Rank ${index + 1}`);
const name = document.createElement('span');
name.className = 'name';
name.textContent = item.name || item.politician || 'Unknown';
const party = document.createElement('span');
party.className = 'party';
party.textContent = item.party || '';
const score = document.createElement('span');
score.className = 'score';
score.textContent = item.score || item.value || '0';
score.setAttribute('aria-label', `${scoreLabel}: ${item.score || item.value || '0'}`);
li.appendChild(rank);
li.appendChild(name);
if (item.party) li.appendChild(party);
li.appendChild(score);
ul.appendChild(li);
});
container.innerHTML = '';
container.appendChild(ul);
}
/**
* Create career trajectory line chart from behavioral trends data
* Shows attendance, effectiveness, and discipline trends over time
* @param {Array} data - Behavioral trends data from CIA CSV
*/
function createCareerTrajectoryChart(data) {
const canvas = document.getElementById('career-trajectory-chart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let chartData;
if (data && data.length > 0) {
// Group data by time_bucket and compute averages
const byPeriod = {};
data.forEach(row => {
const period = (row.time_bucket || row.period_start || '').substring(0, 7); // YYYY-MM
if (!period) return;
if (!byPeriod[period]) {
byPeriod[period] = { absence: [], winRate: [], rebelRate: [], count: 0 };
}
byPeriod[period].absence.push(parseFloat(row.avg_absence_rate) || 0);
byPeriod[period].winRate.push(parseFloat(row.avg_win_rate) || 0);
byPeriod[period].rebelRate.push(parseFloat(row.avg_rebel_rate) || 0);
byPeriod[period].count++;
});
const periods = Object.keys(byPeriod).sort();
const avg = arr => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
chartData = {
labels: periods,
datasets: [
{
label: 'Avg Win Rate (%)',
data: periods.map(p => avg(byPeriod[p].winRate).toFixed(1)),
borderColor: '#00d9ff',
backgroundColor: 'rgba(0, 217, 255, 0.1)',
tension: 0.4,
fill: true
},
{
label: 'Avg Absence Rate (%)',
data: periods.map(p => avg(byPeriod[p].absence).toFixed(1)),
borderColor: '#ff006e',
backgroundColor: 'rgba(255, 0, 110, 0.1)',
tension: 0.4,
fill: true
},
{
label: 'Avg Rebel Rate (%)',
data: periods.map(p => avg(byPeriod[p].rebelRate).toFixed(1)),
borderColor: '#ffbe0b',
backgroundColor: 'rgba(255, 190, 11, 0.1)',
tension: 0.4,
fill: true
}
]
};
} else {
// Fallback with empty state
chartData = {
labels: ['No Data'],
datasets: [{
label: 'No behavioral data available',
data: [0],
borderColor: '#00d9ff',
backgroundColor: 'rgba(0, 217, 255, 0.1)'
}]
};
}
new Chart(ctx, {
type: 'line',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: {
color: '#e0e0e0',
font: {
family: "'Inter', sans-serif"
}
}
},
title: {
display: false
},
tooltip: {
backgroundColor: 'rgba(26, 30, 61, 0.95)',
titleColor: '#00d9ff',
bodyColor: '#e0e0e0',
borderColor: '#00d9ff',
borderWidth: 1
}
},
scales: {
x: {
ticks: {
color: '#e0e0e0',
font: {
family: "'Inter', sans-serif"
}
},
grid: {
color: 'rgba(0, 217, 255, 0.1)'
}
},
y: {
ticks: {
color: '#e0e0e0',
font: {
family: "'Inter', sans-serif"
}
},
grid: {
color: 'rgba(0, 217, 255, 0.1)'
}
}
}
}
});
}
/**
* Create productivity vs influence scatter chart from real CIA data
* Uses risk_summary (productivity proxy via documents/votes) and influence_metrics
* @param {Array} riskData - Risk summary data with vote counts and documents
* @param {Array} influenceData - Influence metrics with network connections
*/
function createProductivityInfluenceChart(riskData, influenceData) {
const canvas = document.getElementById('productivity-influence-chart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let chartData;
if (riskData && riskData.length > 0 && influenceData && influenceData.length > 0) {
// Build influence lookup by person_id
const influenceLookup = {};
influenceData.forEach(row => {
influenceLookup[row.person_id] = {
connections: parseInt(row.network_connections) || 0,
classification: row.influence_classification || 'UNKNOWN'
};
});
// Color by party
const partyColors = {
'S': 'rgba(237, 28, 36, 0.6)',
'M': 'rgba(0, 106, 179, 0.6)',
'SD': 'rgba(221, 221, 0, 0.6)',
'C': 'rgba(0, 153, 68, 0.6)',
'V': 'rgba(218, 41, 28, 0.6)',
'KD': 'rgba(0, 95, 164, 0.6)',
'L': 'rgba(0, 106, 180, 0.6)',
'MP': 'rgba(83, 160, 39, 0.6)'
};
// Group data by party for datasets
const byParty = {};
riskData.forEach(row => {
if (row.status !== 'Tjänstgörande riksdagsledamot') return;
const party = row.party || 'Unknown';
const influence = influenceLookup[row.person_id];
const productivity = parseInt(row.annual_vote_count) || 0;
const connections = influence ? influence.connections : 0;
const riskScore = parseFloat(row.risk_score) || 10;
if (!byParty[party]) byParty[party] = [];
byParty[party].push({
x: productivity,
y: connections,
r: Math.max(3, riskScore / 5),
name: `${row.first_name} ${row.last_name}`,
party: party,
riskLevel: row.risk_level
});
});
chartData = {
datasets: Object.entries(byParty).map(([party, points]) => ({
label: party,
data: points,
backgroundColor: partyColors[party] || 'rgba(128, 128, 128, 0.5)',
borderColor: (partyColors[party] || 'rgba(128,128,128,0.5)').replace('0.6', '1'),
borderWidth: 1
}))
};
} else {
// Fallback with empty state
chartData = {
datasets: [{
label: 'No data available',
data: [{ x: 0, y: 0, r: 5 }],
backgroundColor: 'rgba(0, 217, 255, 0.5)',
borderColor: '#00d9ff',
borderWidth: 1
}]
};
}
new Chart(ctx, {
type: 'bubble',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: {
color: '#e0e0e0',
font: { family: "'Inter', sans-serif" }
}
},
tooltip: {
backgroundColor: 'rgba(26, 30, 61, 0.95)',
titleColor: '#00d9ff',
bodyColor: '#e0e0e0',
borderColor: '#00d9ff',
borderWidth: 1,
callbacks: {
label: function(context) {
const raw = context.raw;
return [
raw.name ? `${raw.name} (${raw.party})` : '',
`Votes: ${context.parsed.x}`,
`Connections: ${context.parsed.y}`,
raw.riskLevel ? `Risk: ${raw.riskLevel}` : ''
].filter(Boolean);
}
}
}
},
scales: {
x: {
title: {
display: true,
text: 'Annual Vote Count',
color: '#e0e0e0',
font: { family: "'Inter', sans-serif" }
},
ticks: { color: '#e0e0e0', font: { family: "'Inter', sans-serif" } },
grid: { color: 'rgba(0, 217, 255, 0.1)' }
},
y: {
title: {
display: true,
text: 'Network Connections',
color: '#e0e0e0',
font: { family: "'Inter', sans-serif" }
},
ticks: { color: '#e0e0e0', font: { family: "'Inter', sans-serif" } },
grid: { color: 'rgba(0, 217, 255, 0.1)' }
}
}
}
});
}
/**
* Create experience distribution bar chart from real CIA data
* @param {Array} data - Experience distribution data from distribution_experience_levels.csv
*/
function createExperienceDistributionChart(data) {
const canvas = document.getElementById('experience-distribution-chart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let labels, counts;
if (data && data.length > 0) {
// Format labels nicely from CSV values like ACTIVE_COMMITTEES → Active Committees
const formatLabel = (s) => s.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()).toLowerCase().replace(/^\w/, c => c.toUpperCase());
labels = data.map(row => formatLabel(row.experience_level || ''));
counts = data.map(row => parseInt(row.politician_count) || 0);
} else {
labels = ['No Data'];
counts = [0];
}
const colors = [
'rgba(0, 217, 255, 0.7)',
'rgba(0, 217, 255, 0.6)',
'rgba(0, 217, 255, 0.5)',
'rgba(255, 190, 11, 0.6)',
'rgba(255, 0, 110, 0.5)',
'rgba(255, 0, 110, 0.6)',
'rgba(255, 0, 110, 0.7)'
];
const borderColors = [
'#00d9ff', '#00d9ff', '#00d9ff',
'#ffbe0b',
'#ff006e', '#ff006e', '#ff006e'
];
const chartData = {
labels: labels,
datasets: [{
label: 'Number of Politicians',
data: counts,
backgroundColor: labels.map((_, i) => colors[i % colors.length]),
borderColor: labels.map((_, i) => borderColors[i % borderColors.length]),
borderWidth: 2
}]
};
new Chart(ctx, {
type: 'bar',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(26, 30, 61, 0.95)',
titleColor: '#00d9ff',
bodyColor: '#e0e0e0',
borderColor: '#00d9ff',
borderWidth: 1
}
},
scales: {
x: {
ticks: {
color: '#e0e0e0',
font: { family: "'Inter', sans-serif" },
maxRotation: 45
},
grid: { color: 'rgba(0, 217, 255, 0.1)' }
},
y: {
ticks: {
color: '#e0e0e0',
font: { family: "'Inter', sans-serif" }
},
grid: { color: 'rgba(0, 217, 255, 0.1)' }
}
}
}
});
}
/**
* Load all dashboard data from real CIA CSV files
* Uses local-first with remote-fallback for each data source
*/
async function loadDashboardData() {
try {
// Fetch all data sources in parallel
const [riskData, influenceData, behavioralData, experienceData] = await Promise.all([
fetchCIAData(DATA_SOURCES.riskSummary),
fetchCIAData(DATA_SOURCES.influenceMetrics),
fetchCIAData(DATA_SOURCES.behavioralTrends),
fetchCIAData(DATA_SOURCES.experienceLevels)
]);
// Cache data
dataCache.riskSummary = riskData;
dataCache.influenceMetrics = influenceData;
dataCache.behavioralTrends = behavioralData;
dataCache.experienceLevels = experienceData;
// --- Top 10 Most Productive (by annual_vote_count) ---
const activeRiskData = riskData.filter(r => r.status === 'Tjänstgörande riksdagsledamot');
const top10Productive = [...activeRiskData]
.sort((a, b) => (parseInt(b.annual_vote_count) || 0) - (parseInt(a.annual_vote_count) || 0))
.slice(0, 10)
.map(r => ({
name: `${r.first_name} ${r.last_name}`,
party: r.party,
score: r.annual_vote_count || '0'
}));
// --- Top 10 Most Influential (by network_connections) ---
const top10Influential = [...influenceData]
.sort((a, b) => (parseInt(b.network_connections) || 0) - (parseInt(a.network_connections) || 0))
.slice(0, 10)
.map(r => ({
name: `${r.first_name} ${r.last_name}`,
party: r.party,
score: r.network_connections || '0'
}));
// --- Top 10 Rising Stars (lowest risk among active - new effective MPs) ---
const top10RisingStars = [...activeRiskData]
.filter(r => parseInt(r.annual_vote_count) > 0)
.sort((a, b) => {
// Best combination: low risk score + high vote count
const scoreA = (parseFloat(a.risk_score) || 50) - ((parseInt(a.annual_vote_count) || 0) / 100);
const scoreB = (parseFloat(b.risk_score) || 50) - ((parseInt(b.annual_vote_count) || 0) / 100);
return scoreA - scoreB;
})
.slice(0, 10)
.map(r => ({
name: `${r.first_name} ${r.last_name}`,
party: r.party,
score: r.risk_score || '0'
}));
// --- Top 10 Controversial (highest risk_score) ---
const top10Controversial = [...activeRiskData]
.sort((a, b) => (parseFloat(b.risk_score) || 0) - (parseFloat(a.risk_score) || 0))
.slice(0, 10)
.map(r => ({
name: `${r.first_name} ${r.last_name}`,
party: r.party,
score: r.risk_score || '0'
}));
// Render Top 10 lists with real data
renderTop10List('top10-productive-container', top10Productive, 'Votes');
renderTop10List('top10-influential-container', top10Influential, 'Connections');
renderTop10List('top10-rising-stars-container', top10RisingStars, 'Risk Score');
renderTop10List('top10-controversial-container', top10Controversial, 'Risk Score');
// Create charts with real data
createCareerTrajectoryChart(behavioralData);
createProductivityInfluenceChart(riskData, influenceData);
createExperienceDistributionChart(experienceData);
} catch (error) {
console.error('Error loading dashboard data:', error);
showError('Failed to load dashboard data. Please try again later.');
}
}
/**
* Show error message
* @param {string} message - Error message
*/
function showError(message) {
const containers = [
'top10-productive-container',
'top10-influential-container',
'top10-rising-stars-container',
'top10-controversial-container'
];
containers.forEach(id => {
const container = document.getElementById(id);
if (container) {
// Clear existing content safely
container.textContent = '';
// Create error message element with safe text insertion
const errorElement = document.createElement('div');
errorElement.className = 'error-message';
errorElement.textContent = message;
container.appendChild(errorElement);
}
});
}
// Initialize dashboard when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadDashboardData);
} else {
loadDashboardData();
}