rawData = FileAttachment("data/constituencies.json").json()
partyColors = ({
"Con": "#0087DC", "Lab": "#DC241f", "LD": "#FAA61A",
"Green": "#6AB023", "RUK": "#12B6CF", "SNP": "#FDF38E",
"PC": "#005B54", "Ind": "#999999", "Other": "#AAAAAA"
})
allVarsMap = [
{label: "Conservative 2024", value: "Con24"},
{label: "Labour 2024", value: "Lab24"},
{label: "Lib Dem 2024", value: "LD24"},
{label: "Reform 2024", value: "RUK24"},
{label: "Green 2024", value: "Green24"},
{label: "SNP 2024", value: "SNP24"},
{label: "Plaid Cymru 2024", value: "PC24"},
{label: "Other 2024", value: "Other24"},
{label: "Conservative 2019", value: "Con19"},
{label: "Labour 2019", value: "Lab19"},
{label: "Lib Dem 2019", value: "LD19"},
{label: "Reform/Brexit 2019", value: "Brexit19"},
{label: "Green 2019", value: "Green19"},
{label: "SNP 2019", value: "SNP19"},
{label: "Plaid Cymru 2019", value: "PC19"},
{label: "Turnout 2024", value: "Turnout24"},
{label: "Turnout 2019", value: "Turnout19"},
{label: "Majority 2024", value: "Majority24"},
{label: "Brexit Leave (Hanretty)", value: "HanrettyLeave"},
{label: "Population Density", value: "c21PopulationDensity"},
{label: "Age: Under 15", value: "AgeUnder15"},
{label: "Age: 16-24", value: "Age16to24"},
{label: "Age: 25-34", value: "Age25to34"},
{label: "Age: 35-44", value: "Age35to44"},
{label: "Age: 45-54", value: "Age45to54"},
{label: "Age: 55-64", value: "Age55to64"},
{label: "Age: Over 65", value: "AgeOver65"},
{label: "Ethnicity: White", value: "c21EthnicityWhite"},
{label: "Ethnicity: Asian", value: "c21EthnicityAsian"},
{label: "Ethnicity: Black", value: "c21EthnicityBlack"},
{label: "Ethnicity: Mixed", value: "c21EthnicityMixed"},
{label: "Born in UK", value: "born_uk"},
{label: "Religion: Christian", value: "c21Christian"},
{label: "Religion: Muslim", value: "c21Muslim"},
{label: "Religion: No Religion", value: "c21NoReligion"},
{label: "Qualification: None", value: "c21QualNone"},
{label: "Qualification: Level 4+", value: "c21QualLevel4"},
{label: "Housing: Owned Outright", value: "c21HouseOutright"},
{label: "Housing: Mortgage", value: "c21HouseMortgage"},
{label: "Housing: Social Rent", value: "c21HouseSocialLA"},
{label: "Housing: Private Rent", value: "c21HousePrivateLandlord"},
{label: "No Car", value: "c21CarsNone"},
{label: "Health: Very Good", value: "c21HealthVeryGood"},
{label: "Health: Bad/Very Bad", value: "c21HealthBad"},
{label: "Employment: Unemployed", value: "c21Unemployed"},
{label: "Deprivation: None", value: "c21DeprivedNone"},
{label: "Deprivation: 3+ dims", value: "c21Deprived3"}
]
mapVarsMap = [
{label: "2024 Winner", value: "Winner24"},
{label: "2019 Winner", value: "Winner19"},
...allVarsMap
]
allVarLabels = Object.fromEntries(allVarsMap.map(v => [v.value, v.label]))
mapVarLabels = Object.fromEntries(mapVarsMap.map(v => [v.value, v.label]))
allRegions = [...new Set(rawData.map(d => d.Region))].filter(Boolean).sort()
allWinners = [...new Set(rawData.map(d => d.Winner24))].filter(Boolean).sort()
allConsts = rawData.map(d => d.ConstituencyName).filter(Boolean).sort()Constituency Profile
Search below, or click a constituency on either map to jump directly here.
profileData = rawData.find(d => d.ConstituencyName === profileConst)
profileHeader = {
if (!profileData) return html`<p>No data found.</p>`;
const d = profileData;
return html`
<div class="profile-card" style="border-top:4px solid ${partyColors[d.Winner24]||'#ccc'}">
<h4 style="margin:0 0 0.75rem">${d.ConstituencyName}</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
<table class="profile-table">
<tr><td>Region</td><td>${d.Region}</td></tr>
<tr><td>Country</td><td>${d.Country}</td></tr>
<tr><td>Type</td><td>${d.ConstituencyType||"—"}</td></tr>
<tr><td>MP (2024)</td><td>${d.MPFirstName24||""} ${d.MPSurname24||""}</td></tr>
<tr><td>Winner 2024</td><td><strong style="color:${partyColors[d.Winner24]||'#333'}">${d.Winner24}</strong></td></tr>
<tr><td>Winner 2019</td><td>${d.Winner19||"—"}</td></tr>
<tr><td>Majority 2024</td><td>${(+d.Majority24||0).toFixed(1)}%</td></tr>
<tr><td>Turnout 2024</td><td>${(+d.Turnout24||0).toFixed(1)}%</td></tr>
<tr><td>Electorate</td><td>${(+d.Electorate24||0).toLocaleString()}</td></tr>
</table>
<table class="profile-table">
<tr><td>Brexit Leave (est.)</td><td>${(+d.HanrettyLeave||0).toFixed(1)}%</td></tr>
<tr><td>Brexit Remain (est.)</td><td>${(+d.HanrettyRemain||0).toFixed(1)}%</td></tr>
${+d.ScotRefYes>0?`<tr><td>Scot Ref Yes</td><td>${(+d.ScotRefYes).toFixed(1)}%</td></tr><tr><td>Scot Ref No</td><td>${(+d.ScotRefNo).toFixed(1)}%</td></tr>`:""}
</table>
</div>
</div>`;
}
profileVotes = {
if (!profileData) return;
const d = profileData;
const pts24 = [{p:"Con",s:+d.Con24,c:partyColors.Con},{p:"Lab",s:+d.Lab24,c:partyColors.Lab},{p:"LD",s:+d.LD24,c:partyColors.LD},{p:"Green",s:+d.Green24,c:partyColors.Green},{p:"RUK",s:+d.RUK24,c:partyColors.RUK},{p:"SNP",s:+d.SNP24,c:partyColors.SNP},{p:"PC",s:+d.PC24,c:partyColors.PC},{p:"Other",s:+d.Other24,c:partyColors.Other}].filter(p=>p.s>0);
const pts19 = [{p:"Con",s:+d.Con19,c:partyColors.Con},{p:"Lab",s:+d.Lab19,c:partyColors.Lab},{p:"LD",s:+d.LD19,c:partyColors.LD},{p:"Green",s:+d.Green19,c:partyColors.Green},{p:"Brexit",s:+d.Brexit19,c:partyColors.RUK},{p:"SNP",s:+d.SNP19,c:partyColors.SNP},{p:"PC",s:+d.PC19,c:partyColors.PC},{p:"Other",s:+d.Other19,c:partyColors.Other}].filter(p=>p.s>0);
const c24 = Plot.plot({marks:[Plot.barY(pts24,{x:"p",y:"s",fill:d=>d.c,tip:true})],x:{label:null,domain:pts24.map(p=>p.p)},y:{label:"Vote share (%)"},title:"2024 Vote Shares",width:430,height:230});
const c19 = Plot.plot({marks:[Plot.barY(pts19,{x:"p",y:"s",fill:d=>d.c,tip:true})],x:{label:null,domain:pts19.map(p=>p.p)},y:{label:"Vote share (%)"},title:"2019 Vote Shares",width:430,height:230});
return html`<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem"><div>${c24}</div><div>${c19}</div></div>`;
}
profileDemog = {
if (!profileData) return;
const d = profileData;
const age = Plot.plot({marks:[Plot.barY([{g:"<15",v:+d.AgeUnder15},{g:"16-24",v:+d.Age16to24},{g:"25-34",v:+d.Age25to34},{g:"35-44",v:+d.Age35to44},{g:"45-54",v:+d.Age45to54},{g:"55-64",v:+d.Age55to64},{g:"65+",v:+d.AgeOver65}],{x:"g",y:"v",fill:"#3498db",tip:true})],x:{label:null},y:{label:"%"},title:"Age Profile",width:280,height:210});
const eth = Plot.plot({marks:[Plot.barY([{g:"White",v:+d.c21EthnicityWhite},{g:"Asian",v:+d.c21EthnicityAsian},{g:"Black",v:+d.c21EthnicityBlack},{g:"Mixed",v:+d.c21EthnicityMixed},{g:"Other",v:+d.c21EthnicityOther}],{x:"g",y:"v",fill:"#9b59b6",tip:true})],x:{label:null},y:{label:"%"},title:"Ethnicity",width:280,height:210});
const hous = Plot.plot({marks:[Plot.barY([{g:"Owned",v:+d.c21HouseOutright},{g:"Mortgage",v:+d.c21HouseMortgage},{g:"Social",v:+d.c21HouseSocialLA},{g:"Private",v:+d.c21HousePrivateLandlord}],{x:"g",y:"v",fill:"#e67e22",tip:true})],x:{label:null},y:{label:"%"},title:"Housing Tenure",width:280,height:210});
const quals = Plot.plot({marks:[Plot.barY([{g:"None",v:+d.c21QualNone},{g:"L1",v:+d.c21QualLevel1},{g:"L2",v:+d.c21QualLevel2},{g:"App.",v:+d.c21QualApprentice},{g:"L3",v:+d.c21QualLevel3},{g:"L4+",v:+d.c21QualLevel4},{g:"Other",v:+d.c21QualOther}],{x:"g",y:"v",fill:"#27ae60",tip:true})],x:{label:null},y:{label:"%"},title:"Qualifications",width:420,height:210});
const health= Plot.plot({marks:[Plot.barY([{g:"V.Good",v:+d.c21HealthVeryGood,c:"#27ae60"},{g:"Good",v:+d.c21HealthGood,c:"#2ecc71"},{g:"Fair",v:+d.c21HealthFair,c:"#f39c12"},{g:"Bad",v:+d.c21HealthBad,c:"#e74c3c"},{g:"V.Bad",v:+d.c21HealthVeryBad,c:"#c0392b"}],{x:"g",y:"v",fill:d=>d.c,tip:true})],x:{label:null},y:{label:"%"},title:"Health",width:420,height:210});
return html`<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;margin-bottom:1rem"><div>${age}</div><div>${eth}</div><div>${hous}</div></div><div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem"><div>${quals}</div><div>${health}</div></div>`;
}