<style> @import url(‘https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@600;700;800&family=DM+Sans:wght@300;400;500&display=swap&#8217;); #ma-income-wrap * { box-sizing: border-box; margin: 0; padding: 0; } #ma-income-wrap { font-family: ‘DM Sans’, sans-serif; padding: 1.5rem 0; } #ma-income-wrap h2 { font-family: ‘Barlow Condensed’, sans-serif; font-weight: 800; font-size: 2rem; letter-spacing: 0.04em; text-transform: uppercase; color: #1C1410; margin-bottom: 0.3rem; } #ma-income-wrap .subtitle { font-size: 0.82rem; color: #8A7B6E; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 1.4rem; } #ma-income-wrap .controls { display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; align-items: center; } #ma-income-wrap input[type=”text”] { padding: 0.5rem 0.9rem; border: 1.5px solid #C8BFB5; border-radius: 8px; background: #fff; font-family: ‘DM Sans’, sans-serif; font-size: 0.88rem; color: #1C1410; width: 220px; outline: none; } #ma-income-wrap input[type=”text”]:focus { border-color: #2E5F8A; } #ma-income-wrap select { padding: 0.5rem 0.9rem; border: 1.5px solid #C8BFB5; border-radius: 8px; background: #fff; font-family: ‘DM Sans’, sans-serif; font-size: 0.88rem; color: #1C1410; outline: none; cursor: pointer; } #ma-income-wrap .count { font-size: 0.82rem; color: #8A7B6E; margin-left: auto; } #ma-income-wrap .chart-wrap { background: #fff; border-radius: 16px; padding: 1.5rem 1.5rem 1rem; box-shadow: 0 4px 24px rgba(0,0,0,0.07); overflow-x: auto; } #ma-income-wrap .bar-row { display: flex; align-items: center; margin-bottom: 6px; gap: 0.7rem; } #ma-income-wrap .bar-label { font-size: 0.78rem; font-weight: 500; color: #3A3028; width: 160px; min-width: 160px; text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #ma-income-wrap .bar-track { flex: 1; height: 22px; background: #F2EDE6; border-radius: 4px; overflow: hidden; min-width: 200px; } #ma-income-wrap .bar-fill { height: 100%; border-radius: 4px; transition: width 0.4s ease; } #ma-income-wrap .bar-value { font-size: 0.75rem; color: #6B5E52; width: 70px; min-width: 70px; font-weight: 500; } #ma-income-wrap .legend { display: flex; gap: 1.2rem; margin-top: 1.2rem; flex-wrap: wrap; } #ma-income-wrap .legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.75rem; color: #6B5E52; } #ma-income-wrap .legend-swatch { width: 14px; height: 14px; border-radius: 3px; display: inline-block; } </style> <div id=”ma-income-wrap”> <h2>Massachusetts — Median Household Income</h2> <p class=”subtitle”>By Community · 2022 ACS 5-Year Estimates · U.S. Census Bureau</p> <div class=”controls”> <input type=”text” id=”mai-search” placeholder=”Search community…” oninput=”maiRender()”/> <select id=”mai-sort” onchange=”maiRender()”> <option value=”desc”>Highest first</option> <option value=”asc”>Lowest first</option> <option value=”alpha”>A – Z</option> </select> <select id=”mai-region” onchange=”maiRender()”> <option value=”all”>All regions</option> <option value=”Greater Boston”>Greater Boston</option> <option value=”MetroWest”>MetroWest</option> <option value=”North Shore”>North Shore</option> <option value=”South Shore”>South Shore</option> <option value=”Pioneer Valley”>Pioneer Valley</option> <option value=”Cape & Islands”>Cape &amp; Islands</option> <option value=”Central MA”>Central MA</option> <option value=”Merrimack Valley”>Merrimack Valley</option> <option value=”Berkshires”>Berkshires</option> <option value=”South Coast”>South Coast</option> </select> <span class=”count” id=”mai-count”></span> </div> <div class=”chart-wrap” id=”mai-chart”></div> </div> <script> (function() { const data = [ { name:”Weston”, income:215000, region:”Greater Boston” }, { name:”Wellesley”, income:199000, region:”Greater Boston” }, { name:”Dover”, income:210000, region:”Greater Boston” }, { name:”Lincoln”, income:196000, region:”Greater Boston” }, { name:”Lexington”, income:185000, region:”Greater Boston” }, { name:”Concord”, income:182000, region:”Greater Boston” }, { name:”Needham”, income:168000, region:”Greater Boston” }, { name:”Newton”, income:163000, region:”Greater Boston” }, { name:”Brookline”, income:128000, region:”Greater Boston” }, { name:”Arlington”, income:117000, region:”Greater Boston” }, { name:”Belmont”, income:145000, region:”Greater Boston” }, { name:”Winchester”, income:165000, region:”Greater Boston” }, { name:”Woburn”, income:89000, region:”Greater Boston” }, { name:”Cambridge”, income:95000, region:”Greater Boston” }, { name:”Boston”, income:76000, region:”Greater Boston” }, { name:”Quincy”, income:78000, region:”Greater Boston” }, { name:”Somerville”, income:88000, region:”Greater Boston” }, { name:”Malden”, income:72000, region:”Greater Boston” }, { name:”Medford”, income:91000, region:”Greater Boston” }, { name:”Waltham”, income:87000, region:”Greater Boston” }, { name:”Watertown”, income:102000, region:”Greater Boston” }, { name:”Milton”, income:142000, region:”Greater Boston” }, { name:”Canton”, income:118000, region:”Greater Boston” }, { name:”Dedham”, income:96000, region:”Greater Boston” }, { name:”Westwood”, income:152000, region:”Greater Boston” }, { name:”Norwood”, income:88000, region:”Greater Boston” }, { name:”Sherborn”, income:198000, region:”MetroWest” }, { name:”Southborough”, income:162000, region:”MetroWest” }, { name:”Hopkinton”, income:158000, region:”MetroWest” }, { name:”Westborough”, income:141000, region:”MetroWest” }, { name:”Medfield”, income:148000, region:”MetroWest” }, { name:”Medway”, income:114000, region:”MetroWest” }, { name:”Framingham”, income:83000, region:”MetroWest” }, { name:”Natick”, income:116000, region:”MetroWest” }, { name:”Ashland”, income:112000, region:”MetroWest” }, { name:”Holliston”, income:124000, region:”MetroWest” }, { name:”Milford”, income:81000, region:”MetroWest” }, { name:”Millis”, income:99000, region:”MetroWest” }, { name:”Franklin”, income:116000, region:”MetroWest” }, { name:”Manchester-by-the-Sea”, income:162000, region:”North Shore” }, { name:”Wenham”, income:155000, region:”North Shore” }, { name:”Hamilton”, income:138000, region:”North Shore” }, { name:”Topsfield”, income:140000, region:”North Shore” }, { name:”Boxford”, income:148000, region:”North Shore” }, { name:”Andover”, income:151000, region:”North Shore” }, { name:”North Andover”, income:122000, region:”North Shore” }, { name:”Beverly”, income:87000, region:”North Shore” }, { name:”Salem”, income:72000, region:”North Shore” }, { name:”Peabody”, income:77000, region:”North Shore” }, { name:”Gloucester”, income:67000, region:”North Shore” }, { name:”Lynn”, income:57000, region:”North Shore” }, { name:”Marblehead”, income:131000, region:”North Shore” }, { name:”Swampscott”, income:110000, region:”North Shore” }, { name:”Duxbury”, income:145000, region:”South Shore” }, { name:”Hingham”, income:148000, region:”South Shore” }, { name:”Cohasset”, income:162000, region:”South Shore” }, { name:”Scituate”, income:122000, region:”South Shore” }, { name:”Norwell”, income:134000, region:”South Shore” }, { name:”Marshfield”, income:102000, region:”South Shore” }, { name:”Hanover”, income:108000, region:”South Shore” }, { name:”Plymouth”, income:82000, region:”South Shore” }, { name:”Brockton”, income:58000, region:”South Shore” }, { name:”Weymouth”, income:80000, region:”South Shore” }, { name:”Rockland”, income:78000, region:”South Shore” }, { name:”Abington”, income:84000, region:”South Shore” }, { name:”Longmeadow”, income:112000, region:”Pioneer Valley” }, { name:”East Longmeadow”, income:86000, region:”Pioneer Valley” }, { name:”Wilbraham”, income:95000, region:”Pioneer Valley” }, { name:”Hampden”, income:92000, region:”Pioneer Valley” }, { name:”Amherst”, income:52000, region:”Pioneer Valley” }, { name:”Northampton”, income:68000, region:”Pioneer Valley” }, { name:”Hadley”, income:74000, region:”Pioneer Valley” }, { name:”Springfield”, income:43000, region:”Pioneer Valley” }, { name:”Holyoke”, income:38000, region:”Pioneer Valley” }, { name:”Chicopee”, income:52000, region:”Pioneer Valley” }, { name:”Westfield”, income:65000, region:”Pioneer Valley” }, { name:”Chatham”, income:89000, region:”Cape & Islands” }, { name:”Brewster”, income:82000, region:”Cape & Islands” }, { name:”Harwich”, income:74000, region:”Cape & Islands” }, { name:”Sandwich”, income:88000, region:”Cape & Islands” }, { name:”Falmouth”, income:72000, region:”Cape & Islands” }, { name:”Barnstable”, income:75000, region:”Cape & Islands” }, { name:”Dennis”, income:68000, region:”Cape & Islands” }, { name:”Yarmouth”, income:62000, region:”Cape & Islands” }, { name:”Provincetown”, income:54000, region:”Cape & Islands” }, { name:”Nantucket”, income:98000, region:”Cape & Islands” }, { name:”Edgartown”, income:86000, region:”Cape & Islands” }, { name:”Shrewsbury”, income:118000, region:”Central MA” }, { name:”Northborough”, income:124000, region:”Central MA” }, { name:”Grafton”, income:116000, region:”Central MA” }, { name:”Sutton”, income:108000, region:”Central MA” }, { name:”Auburn”, income:84000, region:”Central MA” }, { name:”Worcester”, income:54000, region:”Central MA” }, { name:”Leominster”, income:64000, region:”Central MA” }, { name:”Fitchburg”, income:48000, region:”Central MA” }, { name:”Gardner”, income:52000, region:”Central MA” }, { name:”Milbury”, income:82000, region:”Central MA” }, { name:”Haverhill”, income:72000, region:”Merrimack Valley” }, { name:”Lawrence”, income:41000, region:”Merrimack Valley” }, { name:”Methuen”, income:77000, region:”Merrimack Valley” }, { name:”Lowell”, income:52000, region:”Merrimack Valley” }, { name:”Chelmsford”, income:112000, region:”Merrimack Valley” }, { name:”Dracut”, income:88000, region:”Merrimack Valley” }, { name:”Tewksbury”, income:96000, region:”Merrimack Valley” }, { name:”Billerica”, income:98000, region:”Merrimack Valley” }, { name:”Pittsfield”, income:48000, region:”Berkshires” }, { name:”Lenox”, income:62000, region:”Berkshires” }, { name:”Stockbridge”, income:58000, region:”Berkshires” }, { name:”Great Barrington”, income:56000, region:”Berkshires” }, { name:”Adams”, income:44000, region:”Berkshires” }, { name:”North Adams”, income:40000, region:”Berkshires” }, { name:”New Bedford”, income:45000, region:”South Coast” }, { name:”Fall River”, income:43000, region:”South Coast” }, { name:”Dartmouth”, income:78000, region:”South Coast” }, { name:”Fairhaven”, income:66000, region:”South Coast” }, { name:”Westport”, income:82000, region:”South Coast” }, { name:”Seekonk”, income:84000, region:”South Coast” }, { name:”Attleboro”, income:74000, region:”South Coast” }, { name:”Taunton”, income:67000, region:”South Coast” }, ]; const regionColors = { “Greater Boston”: “#2E5F8A”, “MetroWest”: “#3A7D5C”, “North Shore”: “#7B5EA7”, “South Shore”: “#2E7A8A”, “Pioneer Valley”: “#8A5C2E”, “Cape & Islands”: “#2E8A6E”, “Central MA”: “#6B3A8A”, “Merrimack Valley”: “#8A3A3A”, “Berkshires”: “#5C6B2E”, “South Coast”: “#8A6B2E”, }; const maxIncome = Math.max(…data.map(d => d.income)); function fmt(n) { return ‘$’ + (n >= 1000 ? (n/1000).toFixed(0) + ‘k’ : n); } window.maiRender = function() { const q = document.getElementById(‘mai-search’).value.toLowerCase(); const sortVal = document.getElementById(‘mai-sort’).value; const region = document.getElementById(‘mai-region’).value; let filtered = data.filter(d => { return d.name.toLowerCase().includes(q) && (region === ‘all’ || d.region === region); }); if (sortVal === ‘desc’) filtered.sort((a,b) => b.income – a.income); else if (sortVal === ‘asc’) filtered.sort((a,b) => a.income – b.income); else filtered.sort((a,b) => a.name.localeCompare(b.name)); document.getElementById(‘mai-count’).textContent = filtered.length + ‘ communities’; const chart = document.getElementById(‘mai-chart’); chart.innerHTML = ”; filtered.forEach(d => { const pct = (d.income / maxIncome * 100).toFixed(1); const color = regionColors[d.region] || ‘#888’; const row = document.createElement(‘div’); row.className = ‘bar-row’; row.innerHTML = ` <div class=”bar-label” title=”${d.name}”>${d.name}</div> <div class=”bar-track”><div class=”bar-fill” style=”width:${pct}%;background:${color};”></div></div> <div class=”bar-value”>${fmt(d.income)}</div> `; chart.appendChild(row); }); const legendDiv = document.createElement(‘div’); legendDiv.className = ‘legend’; const regions = region === ‘all’ ? […new Set(filtered.map(d => d.region))] : [region]; regions.forEach(r => { legendDiv.innerHTML += `<div class=”legend-item”><span class=”legend-swatch” style=”background:${regionColors[r]}”></span><span>${r}</span></div>`; }); chart.appendChild(legendDiv); }; if (document.readyState === ‘loading’) { document.addEventListener(‘DOMContentLoaded’, maiRender); } else { maiRender(); } })(); </script>