UK Constituency Explorer
  • Hex Map
  • Geographic Map
  • Scatterplot
  • Histogram
  • Box Plot
  • Heatmap
  • Constituency Profile
  • Data Table

Constituency Profile

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()

Search below, or click a constituency on either map to jump directly here.

constFromUrl = {
  const params = new URLSearchParams(window.location.search);
  const v = params.get("const");
  return v ? decodeURIComponent(v) : allConsts[0];
}
viewof profileSearch = Inputs.select(allConsts, {label: "Constituency:", value: constFromUrl})
profileConst = profileSearch
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>`;
}