From 66d5582ee64f386734e5a08a9819cb24d1edc314 Mon Sep 17 00:00:00 2001 From: Sam DeMarrais Date: Tue, 16 Jun 2026 20:05:20 -0400 Subject: [PATCH 1/5] Election scraping --- functions/src/legislators/elections.ts | 262 +++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 functions/src/legislators/elections.ts diff --git a/functions/src/legislators/elections.ts b/functions/src/legislators/elections.ts new file mode 100644 index 000000000..254b81308 --- /dev/null +++ b/functions/src/legislators/elections.ts @@ -0,0 +1,262 @@ +import { JSDOM } from 'jsdom' +import { Array as ArrayType, Runtype, Union, Literal, String, Boolean, Number, Optional, Static, Record } from 'runtypes' + +const officeIds = { + "President": 1, + "U.S. Senate": 6, + "U.S. House": 5, + "Governor": 3, + "Lieutenant Governor": 4, + "Attorney General": 12, + "Secretary of the Commonwealth": 45, + "Treasurer": 53, + "Auditor": 90, + "Governor's Council": 529, + "State Senate": 9, + "State Representative": 8, + "Party State Committee Man": 521, + "Party State Committee Woman": 522, + "Delegate to the National Convention": 543, + "Alternate Delegate to the National Convention": 544, + "District Attorney": 530, + "Clerk of Courts": 15, + "Clerk of Superior Court (Civil)": 534, + "Clerk of Superior Court (Criminal)": 535, + "Clerk of Supreme Judicial Court": 536, + "County Charter Commission": 532, + "Register of Deeds": 384, + "Sheriff": 386, + "County Treasurer": 389, + "Probate Judge": 434, + "Register of Probate": 537, + "Council of Governments Executive Committee": 531 +} as const +export const offices = Object.keys(officeIds) as (keyof typeof officeIds)[]; +export type Office = keyof typeof officeIds + +export const parties = [ + "General", + "American", + "Democratic", + "Green-Rainbow", + "Independent Voters", + "Libertarian", + "Republican", + "Working Families", + "United Independent Party", + "United Independent", + "Independent", + "Green", + "Workers Party" +] as const; +export type Party = (typeof parties)[number]; +export const Party = Union( + Literal(parties[0]), ...parties.slice(1).map(Literal) +); + +const stages = [ + "Primaries", + ...parties +] + +export const ElectionCandidate = Record({ + name: String, + writeIn: Boolean, + votes: Number, + percent: Number, + // Note: During a primary election, no candidate is assigned a party + party: Optional(String), +}); + +export type ElectionCandidate = Static; + +export const ElectionResult = Record({ + candidates: ArrayType(ElectionCandidate), + otherVotes: Number, + blankVotes: Number, + totalVotes: Number, + electionDetailsUrl: String, // If we want votes by town +}); + +export type ElectionStage = Static; + +export const ElectionStage = Record({ + party: Party, + special: Boolean, +}) + +export type ElectionResult = Static; + +export const ElectionInfo = Record({ + year: Number, + office: String, + districts: String, + stage: ElectionStage, + result: Optional(ElectionResult), +}); + +export type ElectionInfo = Static; + +const baseURL = 'https://electionstats.state.ma.us' + +function parseElectionStage(input: string): ElectionStage | null { + const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + + const partyPattern = parties + .map(escape) + .sort((a, b) => b.length - a.length) + .join("|"); + + const regex = new RegExp( + `^(Special )?(${partyPattern}) (Primary|Election)$` + ); + + const match = input.match(regex); + if (!match) { + return null; + } + + const [, special, party, stage] = match; + + // Only "General Election" is valid. + if (stage === "Election" && party !== "General") { + return null; + } + + // Only non-General parties have primaries. + if (stage === "Primary" && party === "General") { + return null; + } + + return { + party: Party.check(party), + special: special !== undefined, + }; +} + +function parseElectionTable(table: Element): ElectionResult { + const candidateRows = table.querySelectorAll( + ':scope > tr:not(.non_candidate):not(.more_info)' + ); + + const candidates = Array.from(candidateRows).map(row => { + const name = row.querySelector('.candidate .name a')?.textContent?.trim() || ''; + + let affiliationText = + row.querySelector('.candidate .party')?.textContent?.trim() || ''; + + const writeIn = /\bwrite-in\b/i.test(affiliationText); + + let party; + affiliationText = affiliationText.replace(/\(write-in\)/i, '').replace(/unenrolled/i, '').trim() + if (affiliationText) { + party = affiliationText + } + + const voteText = row.querySelector('td:nth-child(2)')?.textContent?.replace(/,/g, '') + const percentText = row.querySelector('td:nth-child(3)')?.textContent?.trim()?.slice(0, -1) + if (!name || !voteText || !percentText) { + throw new Error(row.outerHTML) + } + + const candidate = { + name, + writeIn, + votes: parseInt(voteText, 10), + percent: parseFloat(percentText), + ...(party ? { party } : {}) + }; + + return candidate; + }); + + candidates.sort((a,b) => b.votes - a.votes) + + const getSummaryValue = (selector: string): number => { + const row = table.querySelector(selector); + + const text = row?.querySelector('td:nth-child(2)')?.textContent?.replace(/,/g, ''); + if (!text) { + return 0 + } + + return parseInt(text, 10); + }; + + const link = table.querySelector('tr.more_info a')?.href + if (!link) { + throw new Error("Link not present") + } + + return { + candidates, + otherVotes: getSummaryValue('tr.n_all_other_votes'), + blankVotes: getSummaryValue('tr.n_blank_votes'), + totalVotes: getSummaryValue('tr.n_total_votes'), + electionDetailsUrl: `${baseURL}${link}` + }; +} + +function info(dom: JSDOM): ElectionInfo[] { + const elements = Array.from(dom.window.document.querySelectorAll( + '[id^="election-id-"]' + )) + const info = elements.map(electionElem => { + const electTDs = Array.from(electionElem.children).filter(child => child.tagName === 'TD') + const yearText = electTDs[0].textContent + if (!yearText) { + throw new Error("eh") + } + const year = parseInt(yearText, 10) + const office = electTDs[1].textContent?.trim() + const districts = electTDs[2].textContent?.trim() + const stage = parseElectionStage(electTDs[3].textContent?.trim() ?? '') + if (!stage) { + throw new Error(`${stage} is not a recognized election stage`) + } + if (electTDs[4].querySelector(':scope > .no_candidates')) { + return ElectionInfo.check({ + year, + office, + districts, + stage + }) + } + const candidateTable = electTDs[4].querySelector(':scope tbody') + if (!candidateTable) { + throw new Error("Election results expects table or no candidates") + } + const result = parseElectionTable(candidateTable) + + return ElectionInfo.check({ + year, + office, + districts, + stage, + result + }) + }) + return info +} + +export async function fetchElectionData( + startYear: number, + endYear: number, + office?: Office, + stage: null | string = "General" +): Promise { + if (stage !== null && !stages.includes(stage)) { + throw new Error("Unrecognized election stage") + } + const officeId = office ? `/office_id:${officeIds[office]}` : '' + const electionStage = stage ? `/stage:${stage}` : '' + const url = `${baseURL}/elections/search/year_from:${startYear}/year_to:${endYear}${officeId}${electionStage}` + const dom = new JSDOM(await (await fetch(url)).text()) + return info(dom) +} + +import { writeFileSync } from 'fs' + +(async () => { + writeFileSync('elections.json', JSON.stringify(await fetchElectionData(2022, 2023), undefined, 2)) +})() \ No newline at end of file From cdadf480178c6207ea9458eb76710e9da51377f9 Mon Sep 17 00:00:00 2001 From: Sam DeMarrais Date: Sat, 27 Jun 2026 22:40:52 -0400 Subject: [PATCH 2/5] Further election scraping --- functions/src/index.ts | 1 + functions/src/legislators/ElectionScraper.ts | 51 +++ functions/src/legislators/electionTypes.ts | 127 +++++++ functions/src/legislators/elections.ts | 262 --------------- functions/src/legislators/index.ts | 1 + functions/src/legislators/scrapeElections.ts | 331 +++++++++++++++++++ scripts/firebase-admin/backfillElections.ts | 22 ++ 7 files changed, 533 insertions(+), 262 deletions(-) create mode 100644 functions/src/legislators/ElectionScraper.ts create mode 100644 functions/src/legislators/electionTypes.ts delete mode 100644 functions/src/legislators/elections.ts create mode 100644 functions/src/legislators/index.ts create mode 100644 functions/src/legislators/scrapeElections.ts create mode 100644 scripts/firebase-admin/backfillElections.ts diff --git a/functions/src/index.ts b/functions/src/index.ts index 641255bf4..fc9a911ed 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -57,6 +57,7 @@ export { unfollowUser, getFollowers } from "./subscriptions" +export { scrapeElections } from "./legislators" export { transcription } from "./webhooks" diff --git a/functions/src/legislators/ElectionScraper.ts b/functions/src/legislators/ElectionScraper.ts new file mode 100644 index 000000000..9adcbbaf8 --- /dev/null +++ b/functions/src/legislators/ElectionScraper.ts @@ -0,0 +1,51 @@ +import { runWith, RuntimeOptions } from "firebase-functions" +import { db } from "../firebase" +import { electionId } from "./electionTypes" +import { fetchElectionsData } from "./scrapeElections" + +export class ElectionScraper { + private schedule + private timeout + private memory + + constructor( + schedule: string = "every 24 hours", + timeout: number = 480, + memory: RuntimeOptions["memory"] = "256MB" + ) { + this.schedule = schedule + this.timeout = timeout + this.memory = memory + } + + get function() { + return runWith({ + timeoutSeconds: this.timeout, + memory: this.memory, + maxInstances: 1 + }) + .pubsub.schedule(this.schedule) + .onRun(() => this.run()) + } + + private async run(yearTo?: number, yearFrom?: number) { + const date = new Date() + yearTo = yearTo ?? date.getFullYear() + yearFrom = yearFrom ?? (date.getMonth() < 6 ? yearTo - 1 : yearTo) + + const list = await fetchElectionsData(yearFrom, yearTo) + + if (!list) return + + const writer = db.bulkWriter() + + for (let item of list) { + const id = electionId(item) + writer.set(db.doc(`/electionResults/${id}`), item, { merge: true }) + } + + await writer.close() + } +} + +export const scrapeElections = new ElectionScraper().function diff --git a/functions/src/legislators/electionTypes.ts b/functions/src/legislators/electionTypes.ts new file mode 100644 index 000000000..9dd1ebfc2 --- /dev/null +++ b/functions/src/legislators/electionTypes.ts @@ -0,0 +1,127 @@ +import { sha256 } from "js-sha256" +import { + Array, + Union, + Literal, + String, + Boolean, + Number, + Optional, + Static, + Record +} from "runtypes" + +export const officeIds = { + President: 1, + "U.S. Senate": 6, + "U.S. House": 5, + Governor: 3, + "Lieutenant Governor": 4, + "Attorney General": 12, + "Secretary of the Commonwealth": 45, + Treasurer: 53, + Auditor: 90, + "Governor's Council": 529, + "State Senate": 9, + "State Representative": 8, + "Party State Committee Man": 521, + "Party State Committee Woman": 522, + "Delegate to the National Convention": 543, + "Alternate Delegate to the National Convention": 544, + "District Attorney": 530, + "Clerk of Courts": 15, + "Clerk of Superior Court (Civil)": 534, + "Clerk of Superior Court (Criminal)": 535, + "Clerk of Supreme Judicial Court": 536, + "County Charter Commission": 532, + "Register of Deeds": 384, + Sheriff: 386, + "County Treasurer": 389, + "Probate Judge": 434, + "Register of Probate": 537, + "Council of Governments Executive Committee": 531 +} as const +export const offices = Object.keys(officeIds) as (keyof typeof officeIds)[] +export type Office = keyof typeof officeIds + +export const parties = [ + "General", + "American", + "Democratic", + "Green-rainbow", // Green-rainbow has the case Green-Rainbow in some scenarios + "Independent Voters", + "Libertarian", + "Republican", + "Working Families", + "United Independent Party", + "United Independent", + "Independent", + "Green", + "Workers Party" +] as const +export type Party = (typeof parties)[number] +export const Party = Union( + Literal(parties[0]), + ...parties.slice(1).map(Literal) +) + +export const stages = ["Primaries", ...parties] +export type StageSelection = (typeof stages)[number] +export const StageSelection = Union( + Literal(stages[0]), + ...stages.slice(1).map(Literal) +) + +export const ElectionCandidate = Record({ + name: String, + writeIn: Boolean, + votes: Number, + // Note: During a primary election, no candidate is assigned a party + party: Optional(String) +}) + +export type ElectionCandidate = Static + +export const ElectionResult = Record({ + candidates: Array(ElectionCandidate), + otherVotes: Number, + blankVotes: Number, + noPreferenceVotes: Number.optional(), + totalVotes: Number, + electionDetailsUrl: String // Can also provide votes by town/ward +}) + +export type ElectionStage = Static + +export const ElectionStage = Record({ + party: Party, + special: Boolean +}) + +export type ElectionResult = Static + +export const ElectionInfo = Record({ + // Aligned with Candidates[], for use with Firestore array-contains + // More specific than name; for example, a dual election (such as for president/vice president) + // has a name "Harris and Walz", but the link is to the page for Kamala Harris + candidateUrls: Array(String), + // As far as I can tell, the only place exact date is shown + // is the search menu and PDFs + year: Number, + office: String, + // Seemingly non-standardized + districts: String, + // For general elections, party === "General" + party: Party, + special: Boolean, + // If this is missing, candidateUrls is deliberately [] + result: Optional(ElectionResult) +}) + +export type ElectionInfo = Static + +export function electionId(election: ElectionInfo): string { + return sha256( + `${election.office},${election.year},${election.special},${election.party},${election.districts}` + ) +} diff --git a/functions/src/legislators/elections.ts b/functions/src/legislators/elections.ts deleted file mode 100644 index 254b81308..000000000 --- a/functions/src/legislators/elections.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { JSDOM } from 'jsdom' -import { Array as ArrayType, Runtype, Union, Literal, String, Boolean, Number, Optional, Static, Record } from 'runtypes' - -const officeIds = { - "President": 1, - "U.S. Senate": 6, - "U.S. House": 5, - "Governor": 3, - "Lieutenant Governor": 4, - "Attorney General": 12, - "Secretary of the Commonwealth": 45, - "Treasurer": 53, - "Auditor": 90, - "Governor's Council": 529, - "State Senate": 9, - "State Representative": 8, - "Party State Committee Man": 521, - "Party State Committee Woman": 522, - "Delegate to the National Convention": 543, - "Alternate Delegate to the National Convention": 544, - "District Attorney": 530, - "Clerk of Courts": 15, - "Clerk of Superior Court (Civil)": 534, - "Clerk of Superior Court (Criminal)": 535, - "Clerk of Supreme Judicial Court": 536, - "County Charter Commission": 532, - "Register of Deeds": 384, - "Sheriff": 386, - "County Treasurer": 389, - "Probate Judge": 434, - "Register of Probate": 537, - "Council of Governments Executive Committee": 531 -} as const -export const offices = Object.keys(officeIds) as (keyof typeof officeIds)[]; -export type Office = keyof typeof officeIds - -export const parties = [ - "General", - "American", - "Democratic", - "Green-Rainbow", - "Independent Voters", - "Libertarian", - "Republican", - "Working Families", - "United Independent Party", - "United Independent", - "Independent", - "Green", - "Workers Party" -] as const; -export type Party = (typeof parties)[number]; -export const Party = Union( - Literal(parties[0]), ...parties.slice(1).map(Literal) -); - -const stages = [ - "Primaries", - ...parties -] - -export const ElectionCandidate = Record({ - name: String, - writeIn: Boolean, - votes: Number, - percent: Number, - // Note: During a primary election, no candidate is assigned a party - party: Optional(String), -}); - -export type ElectionCandidate = Static; - -export const ElectionResult = Record({ - candidates: ArrayType(ElectionCandidate), - otherVotes: Number, - blankVotes: Number, - totalVotes: Number, - electionDetailsUrl: String, // If we want votes by town -}); - -export type ElectionStage = Static; - -export const ElectionStage = Record({ - party: Party, - special: Boolean, -}) - -export type ElectionResult = Static; - -export const ElectionInfo = Record({ - year: Number, - office: String, - districts: String, - stage: ElectionStage, - result: Optional(ElectionResult), -}); - -export type ElectionInfo = Static; - -const baseURL = 'https://electionstats.state.ma.us' - -function parseElectionStage(input: string): ElectionStage | null { - const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - - const partyPattern = parties - .map(escape) - .sort((a, b) => b.length - a.length) - .join("|"); - - const regex = new RegExp( - `^(Special )?(${partyPattern}) (Primary|Election)$` - ); - - const match = input.match(regex); - if (!match) { - return null; - } - - const [, special, party, stage] = match; - - // Only "General Election" is valid. - if (stage === "Election" && party !== "General") { - return null; - } - - // Only non-General parties have primaries. - if (stage === "Primary" && party === "General") { - return null; - } - - return { - party: Party.check(party), - special: special !== undefined, - }; -} - -function parseElectionTable(table: Element): ElectionResult { - const candidateRows = table.querySelectorAll( - ':scope > tr:not(.non_candidate):not(.more_info)' - ); - - const candidates = Array.from(candidateRows).map(row => { - const name = row.querySelector('.candidate .name a')?.textContent?.trim() || ''; - - let affiliationText = - row.querySelector('.candidate .party')?.textContent?.trim() || ''; - - const writeIn = /\bwrite-in\b/i.test(affiliationText); - - let party; - affiliationText = affiliationText.replace(/\(write-in\)/i, '').replace(/unenrolled/i, '').trim() - if (affiliationText) { - party = affiliationText - } - - const voteText = row.querySelector('td:nth-child(2)')?.textContent?.replace(/,/g, '') - const percentText = row.querySelector('td:nth-child(3)')?.textContent?.trim()?.slice(0, -1) - if (!name || !voteText || !percentText) { - throw new Error(row.outerHTML) - } - - const candidate = { - name, - writeIn, - votes: parseInt(voteText, 10), - percent: parseFloat(percentText), - ...(party ? { party } : {}) - }; - - return candidate; - }); - - candidates.sort((a,b) => b.votes - a.votes) - - const getSummaryValue = (selector: string): number => { - const row = table.querySelector(selector); - - const text = row?.querySelector('td:nth-child(2)')?.textContent?.replace(/,/g, ''); - if (!text) { - return 0 - } - - return parseInt(text, 10); - }; - - const link = table.querySelector('tr.more_info a')?.href - if (!link) { - throw new Error("Link not present") - } - - return { - candidates, - otherVotes: getSummaryValue('tr.n_all_other_votes'), - blankVotes: getSummaryValue('tr.n_blank_votes'), - totalVotes: getSummaryValue('tr.n_total_votes'), - electionDetailsUrl: `${baseURL}${link}` - }; -} - -function info(dom: JSDOM): ElectionInfo[] { - const elements = Array.from(dom.window.document.querySelectorAll( - '[id^="election-id-"]' - )) - const info = elements.map(electionElem => { - const electTDs = Array.from(electionElem.children).filter(child => child.tagName === 'TD') - const yearText = electTDs[0].textContent - if (!yearText) { - throw new Error("eh") - } - const year = parseInt(yearText, 10) - const office = electTDs[1].textContent?.trim() - const districts = electTDs[2].textContent?.trim() - const stage = parseElectionStage(electTDs[3].textContent?.trim() ?? '') - if (!stage) { - throw new Error(`${stage} is not a recognized election stage`) - } - if (electTDs[4].querySelector(':scope > .no_candidates')) { - return ElectionInfo.check({ - year, - office, - districts, - stage - }) - } - const candidateTable = electTDs[4].querySelector(':scope tbody') - if (!candidateTable) { - throw new Error("Election results expects table or no candidates") - } - const result = parseElectionTable(candidateTable) - - return ElectionInfo.check({ - year, - office, - districts, - stage, - result - }) - }) - return info -} - -export async function fetchElectionData( - startYear: number, - endYear: number, - office?: Office, - stage: null | string = "General" -): Promise { - if (stage !== null && !stages.includes(stage)) { - throw new Error("Unrecognized election stage") - } - const officeId = office ? `/office_id:${officeIds[office]}` : '' - const electionStage = stage ? `/stage:${stage}` : '' - const url = `${baseURL}/elections/search/year_from:${startYear}/year_to:${endYear}${officeId}${electionStage}` - const dom = new JSDOM(await (await fetch(url)).text()) - return info(dom) -} - -import { writeFileSync } from 'fs' - -(async () => { - writeFileSync('elections.json', JSON.stringify(await fetchElectionData(2022, 2023), undefined, 2)) -})() \ No newline at end of file diff --git a/functions/src/legislators/index.ts b/functions/src/legislators/index.ts new file mode 100644 index 000000000..7cd688a9a --- /dev/null +++ b/functions/src/legislators/index.ts @@ -0,0 +1 @@ +export { scrapeElections } from "./ElectionScraper" diff --git a/functions/src/legislators/scrapeElections.ts b/functions/src/legislators/scrapeElections.ts new file mode 100644 index 000000000..8309bd32c --- /dev/null +++ b/functions/src/legislators/scrapeElections.ts @@ -0,0 +1,331 @@ +import { JSDOM, VirtualConsole } from "jsdom" +import { + ElectionStage, + parties, + Party, + ElectionInfo, + StageSelection, + ElectionResult, + ElectionCandidate, + Office, + officeIds +} from "./electionTypes" + +const baseURL = "https://electionstats.state.ma.us" + +function parsePartyString(affiliationText: string): { + writeIn: boolean + party?: string +} { + const writeIn = /\bwrite-in\b/i.test(affiliationText) + + let party + affiliationText = affiliationText + .replace(/\(?write-in\)?/i, "") + .replace(/unenrolled/i, "") + .trim() + if (affiliationText) { + party = affiliationText + } + + return { + writeIn, + ...(party ? { party } : {}) + } +} + +function precinctHeaderText(th: Element | undefined): string | undefined { + const a = th?.querySelector("a[title]") ?? th?.querySelector("a[oldtitle]") + return ( + a?.getAttribute("title") ?? + a?.getAttribute("oldtitle") ?? + th?.textContent?.trim() + ) +} + +async function fetchElectionData( + url: string +): Promise<[ElectionResult, string[]]> { + const text = await (await fetch(url)).text() + + const virtualConsole = new VirtualConsole() + virtualConsole.on("jsdomError", error => { + if (error.message.includes("Could not parse CSS stylesheet")) { + return + } + console.error(error) + }) + + const dom = new JSDOM(text, { virtualConsole }) + const document = dom.window.document + + const table = document.querySelector("table.precinct_data") + if (!table) { + throw new Error(`No result table in ${url}`) + } + const headers = Array.from(table.querySelectorAll("thead tr th")).map( + th => precinctHeaderText(th) ?? "" + ) + const totalRow = table.querySelector("tbody tr.total") + if (!totalRow) { + throw new Error(`${url} has no table row for 'total'`) + } + const cells = Array.from(totalRow.querySelectorAll("td")) + const values = new Map() + // Avoid leftward descriptive titles + headers.reverse() + cells.reverse() + headers.forEach((header, i) => { + const text = cells[i].textContent?.replace(/,/g, "").trim() + if (text && /^\d+$/.test(text)) { + values.set(header, parseInt(text)) + } + }) + + const candidates = Array.from( + document.querySelectorAll(".candidate_key .item") + ).map(item => { + const nameElem = item.querySelector(".display_name a") + const name = nameElem?.textContent?.trim() + const votes = values.get(name ?? "") + if (!nameElem || !name || !nameElem.href || !votes) { + throw new Error( + `${item.outerHTML} does not have one of ".display_name a", name, or votes (from ${values})` + ) + } + return { + name, + party: parsePartyString( + item.querySelector(".party")?.textContent?.trim() ?? "" + ), + votes, + candidateUrl: `${baseURL}${nameElem.href}` + } + }) + + candidates.sort((a, b) => b.votes - a.votes) + + const candidateVotes = candidates.map(candidate => { + return { + name: candidate.name, + votes: candidate.votes, + ...candidate.party + } + }) + + const noPreference = values.has("No Preference") + ? { noPreferenceVotes: values.get("No Preference") } + : {} + + const [otherVotes, blankVotes, totalVotes] = [ + values.get("All Others"), + values.get("Blanks"), + values.get("Total Votes Cast") + ] + if (!totalVotes) { + throw new Error(`${url} has no 'Total' column`) + } + return [ + { + candidates: candidateVotes, + otherVotes: otherVotes ?? 0, + blankVotes: blankVotes ?? 0, + totalVotes, + electionDetailsUrl: url, + ...noPreference + }, + candidates.map(candidate => candidate.candidateUrl) + ] +} + +function parseElectionStage(input: string): ElectionStage | null { + const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + + const partyPattern = parties + .map(escape) + .sort((a, b) => b.length - a.length) + .join("|") + + const regex = new RegExp(`^(Special )?(${partyPattern}) (Primary|Election)$`) + + const match = input.match(regex) + if (!match) { + return null + } + + const [, special, party, stage] = match + + // Only "General Election" is valid. + if (stage === "Election" && party !== "General") { + return null + } + + // Only non-General parties have primaries. + if (stage === "Primary" && party === "General") { + return null + } + + return { + party: Party.check(party), + special: special !== undefined + } +} + +async function parseElectionTable( + table: Element +): Promise<[ElectionResult, string[]]> { + if (table.querySelector(".other-candidates")) { + // The search page does not include all details on this election; secondary fetch + const link = table.querySelector("tr.more_info a")?.href + if (!link) { + throw new Error(`${table.outerHTML} has no 'more info' link`) + } + const electionDetailsUrl = `${baseURL}${link}` + return await fetchElectionData(electionDetailsUrl) + } + const candidateRows = table.querySelectorAll( + ":scope > tr:not(.non_candidate):not(.more_info)" + ) + + const candidates = Array.from(candidateRows).map(row => { + const nameElem = row.querySelector(".candidate .name a") + const name = nameElem?.textContent?.trim() || "" + const partyText = + row.querySelector(".candidate .party")?.textContent?.trim() || "" + const link = nameElem?.href + + const voteText = row + .querySelector("td:nth-child(2)") + ?.textContent?.replace(/,/g, "") + if (!name || !voteText || !link) { + throw new Error( + `One of name, voteText, or candidate link is missing from ${row.outerHTML}` + ) + } + + const candidate = { + name, + votes: parseInt(voteText, 10), + ...parsePartyString(partyText) + } + + const ret: [ElectionCandidate, string] = [candidate, `${baseURL}${link}`] + return ret + }) + + candidates.sort((a, b) => b[0].votes - a[0].votes) + + const getSummaryValue = (selector: string): number | null => { + const row = table.querySelector(selector) + + const text = row + ?.querySelector("td:nth-child(2)") + ?.textContent?.replace(/,/g, "") + if (!text) { + return null + } + + return parseInt(text, 10) + } + + const link = table.querySelector("tr.more_info a")?.href + if (!link) { + throw new Error(`More info link missing from ${table.outerHTML}`) + } + + const [otherVotes, blankVotes, totalVotes] = [ + getSummaryValue("tr.n_all_other_votes"), + getSummaryValue("tr.n_blank_votes"), + getSummaryValue("tr.n_total_votes") + ] + const noPreference = getSummaryValue("tr.n_no_preference_votes") + if (!totalVotes) { + throw new Error(`No total votes row in ${table.outerHTML}`) + } + return [ + { + candidates: candidates.map(item => item[0]), + otherVotes: otherVotes ?? 0, + blankVotes: blankVotes ?? 0, + totalVotes, + electionDetailsUrl: `${baseURL}${link}`, + ...(noPreference ? { noPreferenceVotes: noPreference } : {}) + }, + candidates.map(item => item[1]) + ] +} + +async function electionsPageInfo(dom: JSDOM): Promise { + const elements = Array.from( + dom.window.document.querySelectorAll('[id^="election-id-"]') + ) + const info = elements.map(async electionElem => { + const electTDs = Array.from(electionElem.children).filter( + child => child.tagName === "TD" + ) + const yearText = electTDs[0].textContent + if (!yearText) { + throw new Error(`Year not present in ${electionElem.outerHTML}`) + } + const year = parseInt(yearText, 10) + const office = electTDs[1].textContent?.trim() + const districts = electTDs[2].textContent?.trim() + if (!year || !office || !districts) { + throw new Error( + `Year, office, or districts not present in ${electionElem.outerHTML}` + ) + } + const stage = parseElectionStage(electTDs[3].textContent?.trim() ?? "") + if (!stage) { + throw new Error( + `${stage} is not a recognized election stage: ${electTDs[3].outerHTML}` + ) + } + if (electTDs[4].querySelector(":scope > .no_candidates")) { + return ElectionInfo.check({ + year, + office, + districts, + candidateUrls: [], + ...stage + }) + } + const candidateTable = electTDs[4].querySelector(":scope tbody") + if (!candidateTable) { + throw new Error(`No candidate table in ${electionElem.outerHTML}`) + } + const [result, candidateUrls] = await parseElectionTable(candidateTable) + + return ElectionInfo.check({ + year, + office, + districts, + ...stage, + candidateUrls, + result + }) + }) + return Promise.all(info) +} + +export async function fetchElectionsData( + startYear: number, + endYear: number, + office?: Office, + stage: StageSelection | null = "General" +): Promise { + const officeId = office ? `/office_id:${officeIds[office]}` : "" + const electionStage = stage ? `/stage:${stage}` : "" + const url = `${baseURL}/elections/search/year_from:${startYear}/year_to:${endYear}${officeId}${electionStage}` + const page = await fetch(url) + const text = await page.text() + const virtualConsole = new VirtualConsole() + virtualConsole.on("jsdomError", error => { + if (error.message.includes("Could not parse CSS stylesheet")) { + return + } + console.error(error) + }) + const dom = new JSDOM(text, { virtualConsole }) + return electionsPageInfo(dom) +} diff --git a/scripts/firebase-admin/backfillElections.ts b/scripts/firebase-admin/backfillElections.ts new file mode 100644 index 000000000..45ce63b18 --- /dev/null +++ b/scripts/firebase-admin/backfillElections.ts @@ -0,0 +1,22 @@ +import { Record, Number } from "runtypes" +import { Script } from "./types" +import { fetchElectionsData } from "functions/src/legislators/scrapeElections" +import { electionId } from "functions/src/legislators/electionTypes" + +const Args = Record({ + startYear: Number +}) + +export const script: Script = async ({ db, args }) => { + const { startYear } = Args.check(args) + const currentYear = new Date().getFullYear() + const writer = db.bulkWriter() + for (let year = startYear; year <= currentYear; year++) { + const data = await fetchElectionsData(year, year) + for (const item of data) { + const id = electionId(item) + writer.set(db.doc(`/electionResults/${id}`), item, { merge: true }) + } + } + await writer.close() +} From b48fa4559aa522272bc24f6b0d32a63af15ba54f Mon Sep 17 00:00:00 2001 From: Sam DeMarrais Date: Sat, 27 Jun 2026 23:01:43 -0400 Subject: [PATCH 3/5] More robust elections --- functions/src/legislators/scrapeElections.ts | 83 +++++++++++--------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/functions/src/legislators/scrapeElections.ts b/functions/src/legislators/scrapeElections.ts index 8309bd32c..fe7da8e63 100644 --- a/functions/src/legislators/scrapeElections.ts +++ b/functions/src/legislators/scrapeElections.ts @@ -255,55 +255,60 @@ async function parseElectionTable( ] } -async function electionsPageInfo(dom: JSDOM): Promise { +async function electionsPageInfo(dom: JSDOM): Promise<(ElectionInfo | null)[]> { const elements = Array.from( dom.window.document.querySelectorAll('[id^="election-id-"]') ) const info = elements.map(async electionElem => { - const electTDs = Array.from(electionElem.children).filter( - child => child.tagName === "TD" - ) - const yearText = electTDs[0].textContent - if (!yearText) { - throw new Error(`Year not present in ${electionElem.outerHTML}`) - } - const year = parseInt(yearText, 10) - const office = electTDs[1].textContent?.trim() - const districts = electTDs[2].textContent?.trim() - if (!year || !office || !districts) { - throw new Error( - `Year, office, or districts not present in ${electionElem.outerHTML}` - ) - } - const stage = parseElectionStage(electTDs[3].textContent?.trim() ?? "") - if (!stage) { - throw new Error( - `${stage} is not a recognized election stage: ${electTDs[3].outerHTML}` + try { + const electTDs = Array.from(electionElem.children).filter( + child => child.tagName === "TD" ) - } - if (electTDs[4].querySelector(":scope > .no_candidates")) { + const yearText = electTDs[0].textContent + if (!yearText) { + throw new Error(`Year not present in ${electionElem.outerHTML}`) + } + const year = parseInt(yearText, 10) + const office = electTDs[1].textContent?.trim() + const districts = electTDs[2].textContent?.trim() + if (!year || !office || !districts) { + throw new Error( + `Year, office, or districts not present in ${electionElem.outerHTML}` + ) + } + const stage = parseElectionStage(electTDs[3].textContent?.trim() ?? "") + if (!stage) { + throw new Error( + `${stage} is not a recognized election stage: ${electTDs[3].outerHTML}` + ) + } + if (electTDs[4].querySelector(":scope > .no_candidates")) { + return ElectionInfo.check({ + year, + office, + districts, + candidateUrls: [], + ...stage + }) + } + const candidateTable = electTDs[4].querySelector(":scope tbody") + if (!candidateTable) { + throw new Error(`No candidate table in ${electionElem.outerHTML}`) + } + const [result, candidateUrls] = await parseElectionTable(candidateTable) + return ElectionInfo.check({ year, office, districts, - candidateUrls: [], - ...stage + ...stage, + candidateUrls, + result }) + } catch (error) { + console.error(error) + return null } - const candidateTable = electTDs[4].querySelector(":scope tbody") - if (!candidateTable) { - throw new Error(`No candidate table in ${electionElem.outerHTML}`) - } - const [result, candidateUrls] = await parseElectionTable(candidateTable) - - return ElectionInfo.check({ - year, - office, - districts, - ...stage, - candidateUrls, - result - }) }) return Promise.all(info) } @@ -327,5 +332,5 @@ export async function fetchElectionsData( console.error(error) }) const dom = new JSDOM(text, { virtualConsole }) - return electionsPageInfo(dom) + return (await electionsPageInfo(dom)).filter((item): item is ElectionInfo => item !== null) } From 99af751c38caf92613e2de67b830eee04628f14d Mon Sep 17 00:00:00 2001 From: Sam DeMarrais Date: Sat, 27 Jun 2026 23:18:33 -0400 Subject: [PATCH 4/5] Prettier --- functions/src/legislators/scrapeElections.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/functions/src/legislators/scrapeElections.ts b/functions/src/legislators/scrapeElections.ts index fe7da8e63..be4010561 100644 --- a/functions/src/legislators/scrapeElections.ts +++ b/functions/src/legislators/scrapeElections.ts @@ -332,5 +332,7 @@ export async function fetchElectionsData( console.error(error) }) const dom = new JSDOM(text, { virtualConsole }) - return (await electionsPageInfo(dom)).filter((item): item is ElectionInfo => item !== null) + return (await electionsPageInfo(dom)).filter( + (item): item is ElectionInfo => item !== null + ) } From 2394712fbfa24713532efd04c7bcf6c24ba1b406 Mon Sep 17 00:00:00 2001 From: Sam DeMarrais Date: Tue, 14 Jul 2026 19:09:52 -0400 Subject: [PATCH 5/5] Various election bugfixes --- functions/src/legislators/ElectionScraper.ts | 3 +- functions/src/legislators/electionTypes.ts | 11 +- functions/src/legislators/scrapeElections.ts | 69 +- scripts/firebase-admin/backfillElections.ts | 6 +- test.log | 3759 ++++++++++++++++++ 5 files changed, 3815 insertions(+), 33 deletions(-) create mode 100644 test.log diff --git a/functions/src/legislators/ElectionScraper.ts b/functions/src/legislators/ElectionScraper.ts index 9adcbbaf8..0f2c33272 100644 --- a/functions/src/legislators/ElectionScraper.ts +++ b/functions/src/legislators/ElectionScraper.ts @@ -1,6 +1,5 @@ import { runWith, RuntimeOptions } from "firebase-functions" import { db } from "../firebase" -import { electionId } from "./electionTypes" import { fetchElectionsData } from "./scrapeElections" export class ElectionScraper { @@ -40,7 +39,7 @@ export class ElectionScraper { const writer = db.bulkWriter() for (let item of list) { - const id = electionId(item) + const id = item.id writer.set(db.doc(`/electionResults/${id}`), item, { merge: true }) } diff --git a/functions/src/legislators/electionTypes.ts b/functions/src/legislators/electionTypes.ts index 9dd1ebfc2..efc5824ac 100644 --- a/functions/src/legislators/electionTypes.ts +++ b/functions/src/legislators/electionTypes.ts @@ -1,4 +1,3 @@ -import { sha256 } from "js-sha256" import { Array, Union, @@ -87,8 +86,7 @@ export const ElectionResult = Record({ otherVotes: Number, blankVotes: Number, noPreferenceVotes: Number.optional(), - totalVotes: Number, - electionDetailsUrl: String // Can also provide votes by town/ward + totalVotes: Number }) export type ElectionStage = Static @@ -101,6 +99,7 @@ export const ElectionStage = Record({ export type ElectionResult = Static export const ElectionInfo = Record({ + id: Number, // Aligned with Candidates[], for use with Firestore array-contains // More specific than name; for example, a dual election (such as for president/vice president) // has a name "Harris and Walz", but the link is to the page for Kamala Harris @@ -119,9 +118,3 @@ export const ElectionInfo = Record({ }) export type ElectionInfo = Static - -export function electionId(election: ElectionInfo): string { - return sha256( - `${election.office},${election.year},${election.special},${election.party},${election.districts}` - ) -} diff --git a/functions/src/legislators/scrapeElections.ts b/functions/src/legislators/scrapeElections.ts index be4010561..8a506b596 100644 --- a/functions/src/legislators/scrapeElections.ts +++ b/functions/src/legislators/scrapeElections.ts @@ -1,4 +1,5 @@ import { JSDOM, VirtualConsole } from "jsdom" +import { logger } from "firebase-functions" import { ElectionStage, parties, @@ -11,6 +12,29 @@ import { officeIds } from "./electionTypes" +let queue: Promise = Promise.resolve() + +function waitAfterPrevious( + fn: () => Promise, + delayMs: number +): Promise { + const run = queue.then(async () => { + const result = await fn() + + await new Promise(resolve => setTimeout(resolve, delayMs)) + + return result + }) + + // Keep the queue alive even if this request fails + queue = run.catch(() => {}) + + return run +} + +const limitedFetch = (url: string) => + waitAfterPrevious(async () => (await fetch(url)).text(), 3000) + const baseURL = "https://electionstats.state.ma.us" function parsePartyString(affiliationText: string): { @@ -46,7 +70,7 @@ function precinctHeaderText(th: Element | undefined): string | undefined { async function fetchElectionData( url: string ): Promise<[ElectionResult, string[]]> { - const text = await (await fetch(url)).text() + const text = await limitedFetch(url) const virtualConsole = new VirtualConsole() virtualConsole.on("jsdomError", error => { @@ -131,7 +155,6 @@ async function fetchElectionData( otherVotes: otherVotes ?? 0, blankVotes: blankVotes ?? 0, totalVotes, - electionDetailsUrl: url, ...noPreference }, candidates.map(candidate => candidate.candidateUrl) @@ -171,17 +194,9 @@ function parseElectionStage(input: string): ElectionStage | null { } } -async function parseElectionTable( - table: Element -): Promise<[ElectionResult, string[]]> { +function parseElectionTable(table: Element): [ElectionResult, string[]] | null { if (table.querySelector(".other-candidates")) { - // The search page does not include all details on this election; secondary fetch - const link = table.querySelector("tr.more_info a")?.href - if (!link) { - throw new Error(`${table.outerHTML} has no 'more info' link`) - } - const electionDetailsUrl = `${baseURL}${link}` - return await fetchElectionData(electionDetailsUrl) + return null } const candidateRows = table.querySelectorAll( ":scope > tr:not(.non_candidate):not(.more_info)" @@ -248,7 +263,6 @@ async function parseElectionTable( otherVotes: otherVotes ?? 0, blankVotes: blankVotes ?? 0, totalVotes, - electionDetailsUrl: `${baseURL}${link}`, ...(noPreference ? { noPreferenceVotes: noPreference } : {}) }, candidates.map(item => item[1]) @@ -261,6 +275,13 @@ async function electionsPageInfo(dom: JSDOM): Promise<(ElectionInfo | null)[]> { ) const info = elements.map(async electionElem => { try { + const match = electionElem.id.match(/^election-id-(\d+)$/) + if (!match) { + throw new Error( + `In ${electionElem.id}, the id was not parseable as an integer` + ) + } + const electionId = parseInt(match[1], 10) const electTDs = Array.from(electionElem.children).filter( child => child.tagName === "TD" ) @@ -284,6 +305,7 @@ async function electionsPageInfo(dom: JSDOM): Promise<(ElectionInfo | null)[]> { } if (electTDs[4].querySelector(":scope > .no_candidates")) { return ElectionInfo.check({ + id: electionId, year, office, districts, @@ -295,9 +317,15 @@ async function electionsPageInfo(dom: JSDOM): Promise<(ElectionInfo | null)[]> { if (!candidateTable) { throw new Error(`No candidate table in ${electionElem.outerHTML}`) } - const [result, candidateUrls] = await parseElectionTable(candidateTable) + const [result, candidateUrls] = + parseElectionTable(candidateTable) ?? + (await (async () => { + const electionDetailsUrl = `${baseURL}/elections/view/${electionId}/` + return await fetchElectionData(electionDetailsUrl) + })()) return ElectionInfo.check({ + id: electionId, year, office, districts, @@ -322,8 +350,7 @@ export async function fetchElectionsData( const officeId = office ? `/office_id:${officeIds[office]}` : "" const electionStage = stage ? `/stage:${stage}` : "" const url = `${baseURL}/elections/search/year_from:${startYear}/year_to:${endYear}${officeId}${electionStage}` - const page = await fetch(url) - const text = await page.text() + const text = await limitedFetch(url) const virtualConsole = new VirtualConsole() virtualConsole.on("jsdomError", error => { if (error.message.includes("Could not parse CSS stylesheet")) { @@ -332,7 +359,11 @@ export async function fetchElectionsData( console.error(error) }) const dom = new JSDOM(text, { virtualConsole }) - return (await electionsPageInfo(dom)).filter( - (item): item is ElectionInfo => item !== null - ) + const elections: (ElectionInfo | null)[] = await electionsPageInfo(dom) + if (elections.length === 1200) { + logger.error( + `The url ${url} has reached the maximum number of election results provided, please use a more refined query` + ) + } + return elections.filter((item): item is ElectionInfo => item !== null) } diff --git a/scripts/firebase-admin/backfillElections.ts b/scripts/firebase-admin/backfillElections.ts index 45ce63b18..bb604a39a 100644 --- a/scripts/firebase-admin/backfillElections.ts +++ b/scripts/firebase-admin/backfillElections.ts @@ -1,7 +1,6 @@ import { Record, Number } from "runtypes" import { Script } from "./types" import { fetchElectionsData } from "functions/src/legislators/scrapeElections" -import { electionId } from "functions/src/legislators/electionTypes" const Args = Record({ startYear: Number @@ -14,8 +13,9 @@ export const script: Script = async ({ db, args }) => { for (let year = startYear; year <= currentYear; year++) { const data = await fetchElectionsData(year, year) for (const item of data) { - const id = electionId(item) - writer.set(db.doc(`/electionResults/${id}`), item, { merge: true }) + ;(await db.doc(`/electionResults/${item.id}`).get()).ref.set(item, { + merge: true + }) } } await writer.close() diff --git a/test.log b/test.log new file mode 100644 index 000000000..bc56b738f --- /dev/null +++ b/test.log @@ -0,0 +1,3759 @@ +yarn run v1.22.22 +$ ts-node --swc -P tsconfig.script.json scripts/firebase-admin run-script backfillElections --env local --startYear 2024 +https://electionstats.state.ma.us/elections/search/year_from:2024/year_to:2024 +
+
+
 
+
+
+ + + + + + + + +
+ + + Years:  +2026 +2025 +2024 +2023 +2022 +2021 +2020 +2018 +2017 +2016 +2015 +2014 +2013 +2012 +2011 +2010 +2009 +2008 +2007 +2006 +2005 +2004 +2003 +2002 +2001 +2000 +1999 +1998 +1997 +1996 +1995 +1994 +1993 +1992 +1991 +1990 +1988 +1987 +1986 +1985 +1984 +1983 +1982 +1981 +1980 +1979 +1978 +1977 +1976 +1975 +1974 +1972 +1971 +1970 +  to   +2026 +2025 +2024 +2023 +2022 +2021 +2020 +2018 +2017 +2016 +2015 +2014 +2013 +2012 +2011 +2010 +2009 +2008 +2007 +2006 +2005 +2004 +2003 +2002 +2001 +2000 +1999 +1998 +1997 +1996 +1995 +1994 +1993 +1992 +1991 +1990 +1988 +1987 +1986 +1985 +1984 +1983 +1982 +1981 +1980 +1979 +1978 +1977 +1976 +1975 +1974 +1972 +1971 +1970 +Date:   +Mar 31, 2026 +Mar 3, 2026 +Feb 3, 2026 +Jun 10, 2025 +May 13, 2025 +Apr 15, 2025 +Nov 5, 2024 +Sep 3, 2024 +Mar 5, 2024 +Feb 6, 2024 +Nov 7, 2023 +Oct 10, 2023 +May 30, 2023 +May 2, 2023 +Nov 8, 2022 +Sep 6, 2022 +Jan 11, 2022 +Dec 14, 2021 +Nov 30, 2021 +Nov 2, 2021 +Mar 30, 2021 +Mar 2, 2021 +Nov 3, 2020 +Sep 1, 2020 +Jun 2, 2020 +May 19, 2020 +Mar 3, 2020 +Feb 4, 2020 +Nov 6, 2018 +Sep 4, 2018 +Apr 3, 2018 +Mar 6, 2018 +Feb 6, 2018 +Dec 5, 2017 +Nov 7, 2017 +Oct 17, 2017 +Oct 10, 2017 +Sep 19, 2017 +Jul 25, 2017 +Jun 27, 2017 +Nov 8, 2016 +Sep 8, 2016 +May 10, 2016 +Apr 12, 2016 +Mar 1, 2016 +Feb 2, 2016 +Nov 3, 2015 +Oct 6, 2015 +Mar 31, 2015 +Mar 3, 2015 +Nov 4, 2014 +Sep 9, 2014 +Apr 29, 2014 +Apr 1, 2014 +Mar 4, 2014 +Jan 7, 2014 +Dec 10, 2013 +Nov 5, 2013 +Oct 15, 2013 +Oct 8, 2013 +Sep 10, 2013 +Aug 13, 2013 +Jun 25, 2013 +May 28, 2013 +Apr 30, 2013 +Apr 2, 2013 +Mar 5, 2013 +Nov 6, 2012 +Sep 6, 2012 +Mar 6, 2012 +Jan 10, 2012 +Dec 13, 2011 +Oct 18, 2011 +Sep 20, 2011 +Aug 23, 2011 +May 10, 2011 +Apr 12, 2011 +Nov 2, 2010 +Sep 14, 2010 +Jun 15, 2010 +May 18, 2010 +May 11, 2010 +Apr 13, 2010 +Jan 19, 2010 +Dec 8, 2009 +Jun 16, 2009 +May 19, 2009 +Nov 4, 2008 +Sep 16, 2008 +Mar 4, 2008 +Feb 5, 2008 +Dec 11, 2007 +Nov 13, 2007 +Oct 23, 2007 +Oct 16, 2007 +Oct 9, 2007 +Sep 25, 2007 +Sep 11, 2007 +Sep 4, 2007 +Jun 26, 2007 +May 29, 2007 +May 15, 2007 +Apr 17, 2007 +Mar 20, 2007 +Nov 7, 2006 +Sep 19, 2006 +Feb 7, 2006 +Jan 10, 2006 +Sep 27, 2005 +Aug 30, 2005 +Apr 12, 2005 +Mar 15, 2005 +Nov 2, 2004 +Sep 14, 2004 +Mar 2, 2004 +Feb 3, 2004 +May 13, 2003 +Apr 1, 2003 +Nov 5, 2002 +Sep 17, 2002 +Apr 23, 2002 +Mar 12, 2002 +Jan 8, 2002 +Dec 11, 2001 +Oct 23, 2001 +Oct 16, 2001 +Sep 25, 2001 +May 22, 2001 +Apr 24, 2001 +Nov 7, 2000 +Mar 7, 2000 +Feb 8, 2000 +Dec 21, 1999 +Nov 23, 1999 +Oct 26, 1999 +Aug 31, 1999 +Jul 6, 1999 +Jun 8, 1999 +Jun 1, 1999 +May 25, 1999 +May 11, 1999 +May 4, 1999 +Apr 27, 1999 +Apr 13, 1999 +Mar 16, 1999 +Nov 3, 1998 +Sep 15, 1998 +Mar 10, 1998 +Feb 10, 1998 +Jan 6, 1998 +Dec 9, 1997 +Apr 8, 1997 +Mar 11, 1997 +Nov 5, 1996 +Apr 23, 1996 +Apr 16, 1996 +Mar 26, 1996 +Mar 19, 1996 +Mar 5, 1996 +Jan 9, 1996 +Dec 12, 1995 +Dec 10, 1995 +Nov 7, 1995 +Sep 22, 1995 +Sep 19, 1995 +Sep 12, 1995 +Nov 8, 1994 +Jun 7, 1994 +May 10, 1994 +Mar 1, 1994 +Feb 15, 1994 +Feb 1, 1994 +Jan 18, 1994 +Nov 30, 1993 +Nov 2, 1993 +Oct 12, 1993 +Sep 14, 1993 +Aug 17, 1993 +May 11, 1993 +Nov 3, 1992 +Sep 15, 1992 +Apr 7, 1992 +Mar 17, 1992 +Mar 10, 1992 +Feb 11, 1992 +Jan 7, 1992 +Nov 12, 1991 +Oct 22, 1991 +Oct 15, 1991 +Sep 24, 1991 +Sep 17, 1991 +Aug 27, 1991 +Jun 4, 1991 +Apr 30, 1991 +Nov 6, 1990 +May 1, 1990 +Apr 3, 1990 +Nov 8, 1988 +Mar 8, 1988 +Sep 15, 1987 +Aug 18, 1987 +Nov 4, 1986 +Jun 4, 1985 +May 7, 1985 +Apr 9, 1985 +Mar 12, 1985 +Nov 6, 1984 +May 29, 1984 +May 1, 1984 +Mar 13, 1984 +Feb 28, 1984 +Jan 31, 1984 +Sep 20, 1983 +Sep 13, 1983 +Aug 16, 1983 +Aug 13, 1983 +May 10, 1983 +May 3, 1983 +Apr 19, 1983 +Apr 12, 1983 +Apr 5, 1983 +Mar 22, 1983 +Nov 2, 1982 +Jun 16, 1981 +May 26, 1981 +Apr 28, 1981 +Nov 4, 1980 +Apr 15, 1980 +Mar 18, 1980 +Mar 4, 1980 +Feb 19, 1980 +Feb 5, 1980 +Jan 22, 1980 +Jan 8, 1980 +Oct 16, 1979 +Sep 18, 1979 +Sep 19, 1978 +Nov 1, 1977 +Oct 4, 1977 +Aug 16, 1977 +Aug 11, 1977 +Aug 2, 1977 +Jul 19, 1977 +Jun 21, 1977 +Jun 14, 1977 +May 24, 1977 +May 10, 1977 +Apr 26, 1977 +Sep 14, 1976 +Nov 18, 1975 +Aug 26, 1975 +Feb 25, 1975 +Sep 10, 1974 +Sep 19, 1972 +Apr 25, 1972 +Apr 18, 1972 +Mar 28, 1972 +Mar 21, 1972 +Feb 29, 1972 +Jul 27, 1971 +Jun 29, 1971 +Sep 15, 1970 +Office: +All Offices + +President +U.S. Senate +U.S. House + + +Governor +Lieutenant Governor +Attorney General +Secretary of the Commonwealth +Treasurer +Auditor +Governor's Council +State Senate +State Representative + + +Party State Committee Man +Party State Committee Woman +Delegate to the National Convention +Alternate Delegate to the National Convention +District Attorney +Clerk of Courts +Clerk of Superior Court (Civil) +Clerk of Superior Court (Criminal) +Clerk of Supreme Judicial Court +County Charter Commission +Register of Deeds +Sheriff +County Treasurer +Probate Judge +Register of Probate +Council of Governments Executive Committee + + District: +All Districts +Berkshire, Hampden, Franklin & Hampshire +Berkshire, Hampshire, Franklin & Hampden +First Plymouth & Norfolk +Hampden, Hampshire & Worcester +Norfolk & Middlesex +Norfolk, Plymouth & Bristol +Norfolk, Worcester & Middlesex +Second Plymouth & Norfolk +Third Bristol & Plymouth +Worcester & Hampden +Worcester & Hampshire +Berkshire +Berkshire, Hampshire and Franklin +Berkshire, Hampden, Hampshire and Franklin +Bristol +1st Bristol +2nd Bristol +Bristol and Norfolk +Bristol and Plymouth +1st Bristol and Plymouth +2nd Bristol and Plymouth +3rd Bristol +Bristol, Plymouth, and Norfolk +Cape and Islands +Cape, Plymouth, and Islands +1st Essex +2nd Essex +3rd Essex +4th Essex +5th Essex +1st Essex and Middlesex +2nd Essex and Middlesex +3rd Essex and Middlesex +Franklin and Hampshire +Hampden +Hampden and Berkshire +Hampden and Hampshire +1st Hampden +2nd Hampden +1st Hampden and Hampshire +2nd Hampden and Hampshire +Hampshire and Franklin +Hampshire, Franklin and Worcester +1st Middlesex +2nd Middlesex +3rd Middlesex +4th Middlesex +6th Middlesex +7th Middlesex +8th Middlesex +5th Middlesex +Middlesex and Essex +1st Middlesex and Norfolk +2nd Middlesex and Norfolk +3rd Middlesex and Norfolk +Middlesex and Norfolk +Middlesex, Norfolk & Worcester +Middlesex and Worcester +Middlesex and Suffolk +Middlesex, Suffolk and Essex +Norfolk +Norfolk and Bristol +Norfolk, Bristol and Middlesex +Norfolk, Bristol and Plymouth +Norfolk and Plymouth +Norfolk and Suffolk +Plymouth +1st Plymouth +2nd Plymouth +Plymouth and Barnstable +Plymouth and Norfolk +1st Plymouth and Bristol +2nd Plymouth and Bristol +1st Suffolk +2nd Suffolk +3rd Suffolk +4th Suffolk +5th Suffolk +6th Suffolk +Suffolk and Middlesex +Suffolk and Norfolk +1st Suffolk and Middlesex +1st Suffolk and Norfolk +2nd Suffolk and Middlesex +2nd Suffolk and Norfolk +Suffolk, Essex and Middlesex +Worcester +Worcester and Middlesex +Worcester and Norfolk +Worcester, Hampden and Hampshire +Worcester, Hampden, Hampshire and Franklin +Worcester, Hampden, Hampshire and Middlesex +1st Worcester +2nd Worcester +3rd Worcester +4th Worcester +1st Worcester and Middlesex +2nd Worcester and Middlesex + District: +All Districts +Berkshire, Hampden, Franklin & Hampshire +Berkshire, Hampshire, Franklin & Hampden +First Plymouth & Norfolk +Hampden, Hampshire & Worcester +Norfolk & Middlesex +Norfolk, Plymouth & Bristol +Norfolk, Worcester & Middlesex +Second Plymouth & Norfolk +Third Bristol & Plymouth +Worcester & Hampden +Worcester & Hampshire +Berkshire +Berkshire, Hampshire and Franklin +Berkshire, Hampden, Hampshire and Franklin +Bristol +1st Bristol +2nd Bristol +Bristol and Norfolk +Bristol and Plymouth +1st Bristol and Plymouth +2nd Bristol and Plymouth +3rd Bristol +Bristol, Plymouth, and Norfolk +Cape and Islands +Cape, Plymouth, and Islands +1st Essex +2nd Essex +3rd Essex +4th Essex +5th Essex +1st Essex and Middlesex +2nd Essex and Middlesex +3rd Essex and Middlesex +Franklin and Hampshire +Hampden +Hampden and Berkshire +Hampden and Hampshire +1st Hampden +2nd Hampden +1st Hampden and Hampshire +2nd Hampden and Hampshire +Hampshire and Franklin +Hampshire, Franklin and Worcester +1st Middlesex +2nd Middlesex +3rd Middlesex +4th Middlesex +6th Middlesex +7th Middlesex +8th Middlesex +5th Middlesex +Middlesex and Essex +1st Middlesex and Norfolk +2nd Middlesex and Norfolk +3rd Middlesex and Norfolk +Middlesex and Norfolk +Middlesex, Norfolk & Worcester +Middlesex and Worcester +Middlesex and Suffolk +Middlesex, Suffolk and Essex +Norfolk +Norfolk and Bristol +Norfolk, Bristol and Middlesex +Norfolk, Bristol and Plymouth +Norfolk and Plymouth +Norfolk and Suffolk +Plymouth +1st Plymouth +2nd Plymouth +Plymouth and Barnstable +Plymouth and Norfolk +1st Plymouth and Bristol +2nd Plymouth and Bristol +1st Suffolk +2nd Suffolk +3rd Suffolk +4th Suffolk +5th Suffolk +6th Suffolk +Suffolk and Middlesex +Suffolk and Norfolk +1st Suffolk and Middlesex +1st Suffolk and Norfolk +2nd Suffolk and Middlesex +2nd Suffolk and Norfolk +Suffolk, Essex and Middlesex +Worcester +Worcester and Middlesex +Worcester and Norfolk +Worcester, Hampden and Hampshire +Worcester, Hampden, Hampshire and Franklin +Worcester, Hampden, Hampshire and Middlesex +1st Worcester +2nd Worcester +3rd Worcester +4th Worcester +1st Worcester and Middlesex +2nd Worcester and Middlesex + District: +All Districts +1st Congressional +2nd Congressional +3rd Congressional +4th Congressional +5th Congressional +6th Congressional +7th Congressional +8th Congressional +9th Congressional +10th Congressional +11th Congressional +12th Congressional + District: +All Districts + 1st + 2nd + 3rd + 4th + 5th + 6th + 7th + 8th + District: +All Districts +Berkshire, Hampden, Franklin & Hampshire +Berkshire, Hampshire, Franklin & Hampden +First Plymouth & Norfolk +Hampden, Hampshire & Worcester +Norfolk & Middlesex +Norfolk, Plymouth & Bristol +Norfolk, Worcester & Middlesex +Second Plymouth & Norfolk +Third Bristol & Plymouth +Worcester & Hampden +Worcester & Hampshire +Berkshire +Berkshire, Hampshire and Franklin +Berkshire, Hampden, Hampshire and Franklin +Bristol +1st Bristol +2nd Bristol +Bristol and Norfolk +Bristol and Plymouth +1st Bristol and Plymouth +2nd Bristol and Plymouth +3rd Bristol +Bristol, Plymouth, and Norfolk +Cape and Islands +Cape and Plymouth +Cape, Plymouth, and Islands +1st Essex +2nd Essex +3rd Essex +4th Essex +5th Essex +1st Essex and Middlesex +2nd Essex and Middlesex +3rd Essex and Middlesex +Franklin and Hampshire +Franklin, Hampden, and Hampshire +Hampden +Hampden and Berkshire +Hampden and Hampshire +1st Hampden +2nd Hampden +1st Hampden and Hampshire +2nd Hampden and Hampshire +Hampshire +Hampshire and Franklin +Hampshire, Franklin and Worcester +1st Middlesex +2nd Middlesex +3rd Middlesex +4th Middlesex +6th Middlesex +7th Middlesex +8th Middlesex +5th Middlesex +Middlesex and Essex +1st Middlesex and Norfolk +2nd Middlesex and Norfolk +3rd Middlesex and Norfolk +Middlesex and Norfolk +Middlesex, Norfolk & Worcester +Middlesex and Worcester +Middlesex and Suffolk +Middlesex, Suffolk and Essex +Norfolk +Norfolk and Bristol +Norfolk, Bristol and Middlesex +Norfolk, Bristol and Plymouth +Norfolk and Plymouth +Norfolk and Suffolk +Plymouth +1st Plymouth +2nd Plymouth +Plymouth and Barnstable +Plymouth and Norfolk +1st Plymouth and Bristol +2nd Plymouth and Bristol +1st Suffolk +2nd Suffolk +3rd Suffolk +4th Suffolk +5th Suffolk +6th Suffolk +Suffolk and Middlesex +Suffolk and Norfolk +1st Suffolk and Middlesex +1st Suffolk and Norfolk +2nd Suffolk and Middlesex +2nd Suffolk and Norfolk +Suffolk, Essex and Middlesex +Worcester +Worcester and Middlesex +Worcester and Norfolk +Worcester, Hampden and Hampshire +Worcester, Hampden, Hampshire and Franklin +Worcester, Hampden, Hampshire and Middlesex +1st Worcester +2nd Worcester +3rd Worcester +4th Worcester +1st Worcester and Middlesex +2nd Worcester and Middlesex + District: +All Districts +1st Barnstable +2nd Barnstable +3rd Barnstable +4th Barnstable +5th Barnstable +Barnstable, Dukes and Nantucket +1st Berkshire +2nd Berkshire +3rd Berkshire +4th Berkshire +5th Berkshire +6th Berkshire +1st Bristol +2nd Bristol +3rd Bristol +4th Bristol +5th Bristol +6th Bristol +7th Bristol +8th Bristol +9th Bristol +10th Bristol +11th Bristol +12th Bristol +13th Bristol +14th Bristol +15th Bristol +16th Bristol +17th Bristol +18th Bristol +Cape and Islands +1st Dukes +1st Essex +2nd Essex +3rd Essex +4th Essex +5th Essex +6th Essex +7th Essex +8th Essex +9th Essex +10th Essex +11th Essex +12th Essex +13th Essex +14th Essex +15th Essex +16th Essex +17th Essex +18th Essex +19th Essex +20th Essex +21st Essex +22nd Essex +23rd Essex +24th Essex +25th Essex +26th Essex +27th Essex +1st Franklin +2nd Franklin +3rd Franklin +1st Hampden +2nd Hampden +3rd Hampden +4th Hampden +5th Hampden +6th Hampden +7th Hampden +8th Hampden +9th Hampden +10th Hampden +11th Hampden +12th Hampden +13th Hampden +14th Hampden +15th Hampden +16th Hampden +17th Hampden +18th Hampden +19th Hampden +20th Hampden +1st Hampshire +2nd Hampshire +3rd Hampshire +4th Hampshire +1st Middlesex +2nd Middlesex +3rd Middlesex +4th Middlesex +5th Middlesex +6th Middlesex +7th Middlesex +8th Middlesex +9th Middlesex +10th Middlesex +11th Middlesex +12th Middlesex +13th Middlesex +14th Middlesex +15th Middlesex +16th Middlesex +17th Middlesex +18th Middlesex +19th Middlesex +20th Middlesex +21st Middlesex +22nd Middlesex +23rd Middlesex +24th Middlesex +25th Middlesex +26th Middlesex +27th Middlesex +28th Middlesex +29th Middlesex +30th Middlesex +31st Middlesex +32nd Middlesex +33rd Middlesex +34th Middlesex +35th Middlesex +36th Middlesex +37th Middlesex +38th Middlesex +39th Middlesex +40th Middlesex +41st Middlesex +42nd Middlesex +43rd Middlesex +44th Middlesex +45th Middlesex +46th Middlesex +47th Middlesex +48th Middlesex +49th Middlesex +50th Middlesex +51st Middlesex +52nd Middlesex +53rd Middlesex +54th Middlesex +55th Middlesex +56th Middlesex +57th Middlesex +58th Middlesex +59th Middlesex +1st Nantucket +1st Norfolk +2nd Norfolk +3rd Norfolk +4th Norfolk +5th Norfolk +6th Norfolk +7th Norfolk +8th Norfolk +9th Norfolk +10th Norfolk +11th Norfolk +12th Norfolk +13th Norfolk +14th Norfolk +15th Norfolk +16th Norfolk +17th Norfolk +18th Norfolk +19th Norfolk +20th Norfolk +21st Norfolk +22nd Norfolk +23rd Norfolk +24th Norfolk +1st Plymouth +2nd Plymouth +3rd Plymouth +4th Plymouth +5th Plymouth +6th Plymouth +7th Plymouth +8th Plymouth +9th Plymouth +10th Plymouth +11th Plymouth +12th Plymouth +13th Plymouth +14th Plymouth +15th Plymouth +1st Suffolk +2nd Suffolk +3rd Suffolk +4th Suffolk +5th Suffolk +6th Suffolk +7th Suffolk +8th Suffolk +9th Suffolk +10th Suffolk +11th Suffolk +12th Suffolk +13th Suffolk +14th Suffolk +15th Suffolk +16th Suffolk +17th Suffolk +18th Suffolk +19th Suffolk +20th Suffolk +21st Suffolk +22nd Suffolk +23rd Suffolk +24th Suffolk +25th Suffolk +26th Suffolk +27th Suffolk +28th Suffolk +29th Suffolk +30th Suffolk +31st Suffolk +1st Worcester +2nd Worcester +3rd Worcester +4th Worcester +5th Worcester +6th Worcester +7th Worcester +8th Worcester +9th Worcester +10th Worcester +11th Worcester +12th Worcester +13th Worcester +14th Worcester +15th Worcester +16th Worcester +17th Worcester +18th Worcester +19th Worcester +20th Worcester +21st Worcester +22nd Worcester +23rd Worcester +24th Worcester +25th Worcester +26th Worcester +27th Worcester + District: +All Districts +Berkshire +Bristol +Cape and Islands +Eastern +Hampden +Middle +Norfolk +Northern +Northwestern +Plymouth +Southern +Suffolk +Western + County: +All Counties +Barnstable +Berkshire +Bristol +Dukes +Essex +Franklin +Hampden +Hampshire +Middlesex +Nantucket +Norfolk +Plymouth +Suffolk +Worcester + County: +All Counties +Suffolk + County: +All Counties +Suffolk + County: +All Counties +Suffolk + District: +All Districts +Barnstable County - Barnstable +Barnstable County - Bourne +Barnstable County - Brewster +Barnstable County - Chatham +Barnstable County - Dennis +Barnstable County - Eastham +Barnstable County - Falmouth +Barnstable County - Harwich +Barnstable County - Mashpee +Barnstable County - Orleans +Barnstable County - Provincetown +Barnstable County - Sandwich +Barnstable County - Truro +Barnstable County - Wellfleet +Barnstable County - Yarmouth +Berkshire County - At Large +Berkshire County (8th) +Berkshire County (5th) +Berkshire County (1st) +Berkshire County (4th) +Berkshire County (9th) +Berkshire County (2nd) +Berkshire County (7th) +Berkshire County (6th) +Berkshire County (10th) +Berkshire County (3rd) +Bristol County (8th) +Bristol County (11th) +Bristol County (15th) +Bristol County (5th) +Bristol County (1st) +Bristol County (4th) +Bristol County (9th) +Bristol County (2nd) +Bristol County (7th) +Bristol County (6th) +Bristol County (10th) +Bristol County (3rd) +Bristol County (13th) +Bristol County (12th) +Dukes County at Large +Essex County (8th) +Essex County (11th) +Essex County (15th) +Essex County (5th) +Essex County (1st) +Essex County (14th) +Essex County (4th) +Essex County (9th) +Essex County (2nd) +Essex County (7th) +Essex County (6th) +Essex County (10th) +Essex County (3rd) +Essex County (13th) +Essex County (12th) +Franklin County (8th) +Franklin County (5th) +Franklin County (1st) +Franklin County (4th) +Franklin County (9th) +Franklin County (2nd) +Franklin County (7th) +Franklin County (6th) +Franklin County (10th) +Franklin County (3rd) +Franklin County at Large +Hampshire County - At Large +Hampshire County (8th) +Hampshire County (11th) +Hampshire County (15th) +Hampshire County (5th) +Hampshire County (1st) +Hampshire County (14th) +Hampshire County (4th) +Hampshire County (9th) +Hampshire County (2nd) +Hampshire County (7th) +Hampshire County (6th) +Hampshire County (10th) +Hampshire County (3rd) +Hampshire County (13th) +Hampshire County (12th) +Nantucket County at Large +Norfolk County (8th) +Norfolk County (11th) +Norfolk County (15th) +Norfolk County (5th) +Norfolk County (1st) +Norfolk County (14th) +Norfolk County (4th) +Norfolk County (9th) +Norfolk County (2nd) +Norfolk County (7th) +Norfolk County (6th) +Norfolk County (10th) +Norfolk County (3rd) +Norfolk County (12th) +Plymouth County (8th) +Plymouth County (11th) +Plymouth County (15th) +Plymouth County (5th) +Plymouth County (1st) +Plymouth County (14th) +Plymouth County (4th) +Plymouth County (9th) +Plymouth County (2nd) +Plymouth County (7th) +Plymouth County (6th) +Plymouth County (10th) +Plymouth County (3rd) +Plymouth County (13th) +Plymouth County (12th) +Worcester County (8th) +Worcester County (11th) +Worcester County (15th) +Worcester County (5th) +Worcester County (1st) +Worcester County (14th) +Worcester County (4th) +Worcester County (9th) +Worcester County (2nd) +Worcester County (7th) +Worcester County (6th) +Worcester County (10th) +Worcester County (3rd) +Worcester County (13th) +Worcester County (12th) + District: +All Districts +Barnstable +Berkshire Middle +Berkshire Northern +Berkshire Southern +Bristol Fall River +Bristol Northern +Bristol Southern +Dukes +Essex Northern +Essex Southern +Fall River +Franklin +Hampden +Hampshire +Middlesex Northern +Middlesex Southern +Nantucket +Norfolk +Plymouth +Suffolk +Worcester +Worcester Northern + County: +All Counties +Barnstable +Berkshire +Bristol +Dukes +Essex +Franklin +Hampden +Hampshire +Middlesex +Nantucket +Norfolk +Plymouth +Suffolk +Worcester + County: +All Counties +Barnstable +Berkshire +Bristol +Dukes +Essex +Franklin +Hampden +Hampshire +Middlesex +Norfolk +Plymouth +Worcester + County: +All Counties +Franklin + County: +All Counties +Barnstable +Berkshire +Bristol +Dukes +Essex +Franklin +Hampden +Hampshire +Middlesex +Nantucket +Norfolk +Plymouth +Suffolk +Worcester + County: +All Counties +Franklin + Stage: +All Elections +All General Elections +All Primary Elections +American Primaries +Democratic Primaries +Green-Rainbow Primaries +Independent Voters Primaries +Libertarian Primaries +Republican Primaries +United Independent Primaries +Working Families Primaries + Stage: +All Elections +American Party +Democratic Party +Green-Rainbow Party +Libertarian Party +Republican Party +United Independent Party +Working Families Party + Stage: +All Elections +American Party +Democratic Party +Green-Rainbow Party +Libertarian Party +Republican Party +United Independent Party +Working Families Party + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Republican Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Republican Primaries + Stage: +All Elections +All General Elections +All Primary Elections +American Primaries +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries +Working Families Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries + Stage: +All Elections +All General Elections +All Primary Elections +American Primaries +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries +United Independent Party Primaries +Working Families Primaries +Independent Primaries + Stage: +All Elections +All General Elections +All Primary Elections +American Primaries +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries +United Independent Party Primaries +Working Families Primaries + Stage: +All Elections +All General Elections +All Primary Elections +American Primaries +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries +United Independent Party Primaries +Working Families Primaries +Independent Primaries +United Independent Primaries +Independent Voters Primaries + Stage: +All Elections +All General Elections +All Primary Elections +American Primaries +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries +United Independent Party Primaries +Working Families Primaries +Independent Voters Primaries +Independent Primaries +United Independent Primaries +Green Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries +United Independent Party Primaries + Stage: +All Elections +All General Elections +All Primary Elections +American Primaries +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries +Working Families Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries + Stage: +All Elections +All General Elections +All Primary Elections + Stage: +All Elections +All General Elections +All Primary Elections +American Primaries +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries +United Independent Party Primaries +Working Families Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries +United Independent Party Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Republican Primaries +Green-Rainbow Primaries +Libertarian Primaries +Working Families Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries + Stage: +All Elections +All General Elections +All Primary Elections +Democratic Primaries +Green-Rainbow Primaries +Libertarian Primaries +Republican Primaries +Working Families Primaries + Stage: +All Elections +All General Elections +All Primary Elections + Stage: +All Elections +All General Elections +All Primary Elections +American Primaries +Democratic Primaries +Green-Rainbow Primaries +Independent Voters Primaries +Libertarian Primaries +Republican Primaries +United Independent Primaries +Working Families Primaries +United Independent Party Primaries +Independent Primaries +Green Primaries +Advanced Search Special Elections Only? # Candidates: + += +>= +<= + +any +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 + % Victory: + +>= +<= + +any +10 +20 +30 +40 +50 +60 +70 +80 +90 +% % Closeness: + +>= +<= + +any +1 +3 +5 +10 +20 +30 +40 +50 +60 +70 +80 +90 +% Display OptionsShow candidatesEnlarge data visualizations + + + + + + // Global + var E_DATE_LIST_HTML = ''; + + e_init_date_year_toggle(); + e_adjustYearSelect($('#SearchOfficeId').val()); + + + function e_enableDistrictSelect(office_id) { + $("#search_form_elections .wrapper_for_district_list").hide(); + $("#search_form_elections .district_list").hide(); + $("#search_form_elections .district_list").each(function(n,v) { + $(this).attr("disabled",true); //"district_id_off"; + }); + var find_target = "#search_form_elections #districtListForOffice"+office_id; + + if($(find_target).length) { + + $("#search_form_elections #districtListForOffice"+office_id).attr("disabled",false); + $("#search_form_elections #districtListForOffice"+office_id).show() + $("#search_form_elections #wrapperForDistrictListForOffice"+office_id).show(); + } + } + + function e_enableStageSelect(office_id) { + $("#search_form_elections .wrapper_for_stage_list").hide(); + $("#search_form_elections .stage_list").hide(); + $("#search_form_elections .stage_list").each(function(n,v) { + $(this).attr("disabled",true); //"district_id_off"; + }); + var find_target = "#search_form_elections #stageListForOffice"+office_id; + + if($(find_target).length) { + + $("#search_form_elections #stageListForOffice"+office_id).attr("disabled",false); + $("#search_form_elections #stageListForOffice"+office_id).show() + $("#search_form_elections #wrapperForStageListForOffice"+office_id).show(); + } + } + + /** + * Choices of years in year dropdowns change according to the office selected. + * @param oid + * @return {boolean} + */ + + + + function e_adjustYearSelect(oid) { + + if(!E_DATE_LIST_HTML) { E_DATE_LIST_HTML = $('#SearchDate').html(); } + + if(!oid) { return false; } + + var valid_years = {"2026":"2026","2025":"2025","2024":"2024","2023":"2023","2022":"2022","2021":"2021","2020":"2020","2018":"2018","2017":"2017","2016":"2016","2015":"2015","2014":"2014","2013":"2013","2012":"2012","2011":"2011","2010":"2010","2009":"2009","2008":"2008","2007":"2007","2006":"2006","2005":"2005","2004":"2004","2003":"2003","2002":"2002","2001":"2001","2000":"2000","1999":"1999","1998":"1998","1997":"1997","1996":"1996","1995":"1995","1994":"1994","1993":"1993","1992":"1992","1991":"1991","1990":"1990","1988":"1988","1987":"1987","1986":"1986","1985":"1985","1984":"1984","1983":"1983","1982":"1982","1981":"1981","1980":"1980","1979":"1979","1978":"1978","1977":"1977","1976":"1976","1975":"1975","1974":"1974","1972":"1972","1971":"1971","1970":"1970"}; + var office_year_ranges = {"1":["1972","2024"],"521":["1972","2024"],"522":["1972","2024"],"543":["1972","1972"],"544":["1972","1972"],"6":["1970","2024"],"3":["1970","2022"],"4":["1970","2022"],"12":["1970","2022"],"45":["1970","2022"],"53":["1970","2022"],"90":["1970","2022"],"5":["1970","2024"],"529":["1970","2024"],"9":["1970","2026"],"8":["1970","2026"],"530":["1970","2022"],"15":["1970","2024"],"534":["1970","2024"],"535":["1970","2024"],"536":["1970","2024"],"532":["1986","2010"],"384":["1970","2024"],"386":["1970","2022"],"389":["1972","2020"],"434":["1974","1974"],"537":["1970","2024"],"531":["1998","2024"]}; + var target_year_range = office_year_ranges[oid]; + var current_selected_from = $('#SearchYearFrom').val(); + var current_selected_to = $('#SearchYearTo').val(); + var current_selected_date = $('#SearchDate').val(); + + var options_html = ''; + var has_currently_selected_from = false; + var has_currently_selected_to = false; + + var date_options = $(window.E_DATE_LIST_HTML); + + var target_date_options = []; + + for(y=target_year_range[1];y>=target_year_range[0];y--) { + if(typeof valid_years[y] == 'undefined') { continue; } + + options_html += "\n"+''; + + y == current_selected_from? has_currently_selected_from = true: ''; + y == current_selected_to? has_currently_selected_to = true: ''; + + date_options.each(function(k,v) { if($(v).val().substr(0,4) == y) { target_date_options.push(v); } }); + } + + // console.log(target_date_options.length); + + $('#SearchDate').html(target_date_options); + $('#SearchDate').val(current_selected_date).change(); + + $('#SearchYearFrom').html(options_html); + $('#SearchYearTo').html(options_html); + + $('#SearchYearFrom').val(has_currently_selected_from? current_selected_from: target_year_range[0]).change(); + $('#SearchYearTo').val(has_currently_selected_to? current_selected_to: target_year_range[1]).change(); + + + return true; + + } + + function e_sanitizeYearSelect(year) { + if($("#search_form_elections #SearchYearFrom").val() > $("#search_form_elections #SearchYearTo").val()) { + $("#search_form_elections #SearchYearTo").val( $("#search_form_elections #SearchYearFrom").val()); + } + } + // Turn on the correct District and Stage form on page load, if applicable + e_enableDistrictSelect($("#search_form_elections #SearchOfficeId").val()); + e_enableStageSelect($("#search_form_elections #SearchOfficeId").val()); + + // Bind event handler to office select + $("#search_form_elections #SearchOfficeId").change(function() { + + e_enableDistrictSelect($(this).val()); + e_enableStageSelect($(this).val()); + e_adjustYearSelect($(this).val()); + + }); + + $("#search_form_elections .search_controls form").submit(function() { + e_sanitizeYearSelect($(this).val()); + loadingSearchResults(); + return true; + }); + + + function e_init_date_year_toggle() { + // Toggle between inputs + var sel_year = $('#search_form_elections .input.select-year-range'); + var sel_date = $('#search_form_elections .input.select-date'); + sel_year.before(''); + sel_date.before(''); + + // Default + sel_date.hide().appendTo($('body')); + + if(sel_year.length && sel_date.length) { + if(!sel_year.find('.toggle').length) { + sel_year.append('By Date'); + sel_year.find('.toggle').on('click',this,function(e) { + sel_year.hide().appendTo($('body')); + sel_date.insertAfter($('a.e.keep-sel-date')); + sel_date.show(); + }); + } + if(!sel_date.find('.toggle').length) { + sel_date.append('By Years'); + sel_date.find('.toggle').on('click',this,function(e) { + sel_date.hide().appendTo($('body')); + sel_year.insertAfter($('a.e.keep-sel-year')); + sel_year.show(); + }); + } + } + + } // END function init_date_year_toggle() + + + + + + + + + //$.cookie.defaults.path = '/'; + //$.cookie.defaults.domain = '.electionstats.state.ma.us'; + + // Not working (from Cake Cookie component) + //var msg = $.cookie('CakeCookie[Flash][message]'); + //var key = $.cookie('CakeCookie[Flash][key]'); + + + + //console.log(document.cookie); + + + var msg = $.cookie('elstats_flash_message'); + var key = $.cookie('elstats_flash_key'); + + if(msg) { + key = $.trim(key.replace(/"/g,'')); + msg = msg.replace(/^"/,'').replace(/"$/,''); + + //.replace(/'/g, "\\'") + + var data = '
'+msg+'
'; + $('#flash_from_js_98523').after(data); + + // Optional: jQueryUI effects + if(msg.search(/denied/i) > -1) { + $('#'+key+'.flash-message-98523').delay(5000).fadeOut(); + } + + if(key == 'flash_error_fixed_to_top') { + window.setTimeout(function() { $('#'+key).fadeOut(3000); }, 10000); + } + + + // Now delete them. (this was set from AccessComponent::setFlash()) + $.removeCookie('elstats_flash_message',{domain:'.electionstats.state.ma.us',path:'/'}); + $.removeCookie('elstats_flash_key',{domain:'.electionstats.state.ma.us',path:'/'}); + + // Not working (from Cake Cookie component) + //$.removeCookie('CakeCookie[Flash][message]',{domain:'.electionstats.state.ma.us',path:'/'}); + //$.removeCookie('CakeCookie[Flash][key]',{domain:'.electionstats.state.ma.us',path:'/'}); + + + + //console.log('after flash:'); + //console.log(document.cookie); + + } + + + /** + $.post('/flash_from_js/',{},function(data) { + if(data) { + $('#flash_from_ajax_98523').after(data); + } + }); + **/ + + + +https://electionstats.state.ma.us/elections/view/165300/ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207