const { hostname, origin } = window.location;
const testDataPath = "/president_polls-2024.txt";
const liveDataPath = "/content/dam/common/exceltojson/president_polls.txt";
const data2024 =
origin + (hostname === "localhost" ? testDataPath : liveDataPath);
const fullPageLink = "/world/u-s-election-poll-tracker-how-kamala-harris-and-donald-trump-compare-1.6983378";
// const data2024 = "./president_polls-2024.txt";
//const data2024 =
// "/content/dam/common/exceltojson/president_polls.txt";
function makeInteractive() {
// const data2024 = "./president_polls-2024.txt";
const fullPageLink = "/world/u-s-election-poll-tracker-how-kamala-harris-and-donald-trump-compare-1.6983378";
//const data2024 =
// "/content/dam/common/exceltojson/president_polls.txt";
const parent = document.querySelector(".poll-page");
const userOptions = parent.dataset;
start(userOptions);
function createForm(parent, data, dateIndex, stateArray, candidates, options) {
const form = parent.append("form").attr("class", "poll-options");
const makeDropDown = (form, id, label) => {
const div = form.append("div");
div.append("label").attr("for", id).text(label);
return div.append("select").attr("name", id).attr("id", id);
};
const selectDem = makeDropDown(form, "dem", "Democrat");
const selectRep = makeDropDown(form, "rep", "Republican");
const selectLocation = makeDropDown(form, "location", "Poll region");
// const dropdown = page.append("div").attr("class", "poll-dropdown");
selectLocation.append("option").attr("value", "all").text("All polls");
stateArray.forEach((state) => {
const option = selectLocation
.append("option")
.attr("value", state)
.text(state);
if (state === "") {
option.text("National").attr("class", "bold").attr("selected", "");
}
});
const candidateData = {
candidates: {},
combos: {},
};
data.forEach((date) => {
date.polls.forEach((poll) => {
poll.questions.forEach((question) => {
question.results.forEach((result) => {
let party = candidateData.candidates[result.party];
if (!party) {
candidateData.candidates[result.party] = [];
party = candidateData.candidates[result.party];
}
let name = party.find((can) => can.name === result.answer);
if (!name) {
const candidate = {
name: result.answer,
party: result.party,
fullName: result.candidate_name,
count: 0,
};
party.push(candidate);
name = candidate;
}
name.count++;
});
const string = question.results
.map((d) => d.answer)
.sort()
.join("/");
if (candidateData.combos[string]) {
candidateData.combos[string] = candidateData.combos[string] + 1;
} else {
candidateData.combos[string] = 1;
}
});
});
});
// const form = d3.select("form.poll-options");
// const selectDem = form.select("select#dem");
// const selectRep = form.select("select#rep");
const minCount = 25;
function buildOptions(selection, party) {
selection
.selectAll("option")
.data(candidateData.candidates[party].filter((d) => d.count >= minCount))
.join("option")
.attr("value", (d) => d.name)
.property("selected", (d) =>
d.name === candidates.find((c) => c.party === party).name ? "true" : ""
)
.html((d) => d.fullName);
}
buildOptions(selectDem, "DEM");
buildOptions(selectRep, "REP");
function getCombo() {
const dem = getCandidate("DEM", selectDem.node().value);
const rep = getCandidate("REP", selectRep.node().value);
const candidateCombo = [dem.name, rep.name].sort().join("/");
}
function getCandidate(party, name) {
return candidateData.candidates[party].find((c) => c.name === name);
}
form.on("change", () => {
erase();
const dem = getCandidate("DEM", selectDem.node().value);
const rep = getCandidate("REP", selectRep.node().value);
const candidates = [dem, rep];
const location = selectLocation.node().value;
const filteredData = filterData(data, location, candidates);
build(candidates, filteredData, dateIndex, options);
});
}
async function start(userOptions) {
const defaultOptions = {
showPolls: true,
showSpark: true,
startDate: "1/1/24",
chartHeight: "250",
};
const options = { ...defaultOptions, ...userOptions };
Promise.all([d3.csv(data2024)]).then((files) => {
create(files, options);
/*
const [data, dateIndex, stateArray] = formatData(files[0], options);
console.log("Formated data:", data);
const page = d3.select(".poll-page");
const candidates = [
{ name: "Harris", party: "DEM" },
{ name: "Trump", party: "REP" },
];
createForm(page, data, dateIndex, stateArray, candidates, options);
page
.append("div")
.attr("class", "poll-chart-title bold")
.text("U.S. presidential polls");
page
.append("div")
.attr("class", "poll-chart-div")
.style("height", `${options.chartHeight}px`);
if (bool(options.showSpark)) {
page.append("div").attr("class", "poll-chart-hover");
}
if (bool(options.showPolls)) {
page
.append("div")
.attr("class", "poll-grid capped")
.append("div")
.attr("class", "poll-loading");
let button = page
.append("button")
.attr("class", "poll-button")
.text("Show all")
.on("click", () => {
d3.select(".poll-grid").classed("capped", false);
button.style("display", "none");
});
} else {
page
.append("a")
.attr("class", "full-page")
.attr("href", fullPageLink)
.text("Visit full polling page for details >");
}
erase();
const filteredData = filterData(data, "", candidates);
build(candidates, filteredData, dateIndex, options); // Filter value of "" for national polls
*/
});
}
function create(files, options) {
const [data, dateIndex, stateArray] = formatData(files[0], options);
const page = d3.select(".poll-page");
const candidates = [
{ name: "Harris", party: "DEM" },
{ name: "Trump", party: "REP" },
];
createForm(page, data, dateIndex, stateArray, candidates, options);
page
.append("div")
.attr("class", "poll-chart-title bold")
.text("U.S. presidential polls");
page
.append("div")
.attr("class", "poll-chart-div")
.style("height", `${options.chartHeight}px`);
if (bool(options.showSpark)) {
page.append("div").attr("class", "poll-chart-hover");
}
if (bool(options.showPolls)) {
page
.append("div")
.attr("class", "poll-grid-container")
.append("div")
.attr("class", "poll-grid")
.append("div")
.attr("class", "poll-loading");
if (bool(options.capPolls)) {
page.classed("capped", true);
const button = page
.append("button")
.attr("class", "poll-button")
.text("Show all")
.on("click", () => {
d3.select(".poll-grid").classed("capped", false);
button.style("display", "none");
});
}
}
page
.append("a")
.attr("class", "full-page")
.attr("href", fullPageLink)
.text("Visit full polling page for details >");
erase();
const filteredData = filterData(data, "", candidates);
build(candidates, filteredData, dateIndex, options); // Filter value of "" for national polls
}
function formatData(data, options) {
// Initialize array to get name of every state with poll
let stateArray = [];
// Group by Poll ID
let pollArray = [];
data.forEach((result) => {
let poll = pollArray.find((poll) => poll.id === result.poll_id);
if (poll) {
poll.results.push(result);
} else {
let newPoll = {
pollster: result.pollster,
rating: result.fte_grade,
dateCreated: result.created_at,
date: { start: result.start_date, end: result.end_date },
id: result.poll_id,
results: [result],
questions: [],
};
pollArray.push(newPoll);
}
});
// Group by Questions within poll
pollArray.forEach((poll) => {
poll.results.forEach((result) => {
let question = poll.questions.find(
(question) => question.id === result.question_id
);
if (question) {
question.results.push(result);
} else {
let newQuestion = {
id: result.question_id,
type: result.population,
sample_size: result.sample_size,
results: [result],
state: result.state,
url: result.url,
};
if (!stateArray.includes(newQuestion.state)) {
stateArray.push(newQuestion.state);
}
poll.questions.push(newQuestion);
}
});
});
// Polls are given one date (to plot on the chart and as headings in list)
// Two possible dates:
// - The end date of when the poll was run
// - The date the poll was published ("created_at")
//
// Currently using the end date
let pollsByDateArray = [];
pollArray.forEach((poll) => {
const dateCreated = poll.dateCreated.split(" ")[0];
const dateEnd = poll.date.end;
let date = pollsByDateArray.find((date) => date.date === dateEnd);
if (date) {
date.polls.push(poll);
} else {
let newDate = {
date: dateEnd,
polls: [poll],
};
pollsByDateArray.push(newDate);
}
});
const years = ["23", "24"];
//let oldDateArray = pollsByDateArray.map((date) => date.date);
const monthLengthArray = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const leapMonthLengthArray = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let dateArray = [];
years.forEach((year) => {
const array = year % 4 === 0 ? leapMonthLengthArray : monthLengthArray;
array.forEach((month, m) => {
for (let d = 1; d <= month; d++) {
dateArray.push(`${m + 1}/${d}/${year}`);
}
});
});
// const firstIndex = dateArray.indexOf(pollsByDateArray[0].date);
// pollsByDateArray = pollsByDateArray.filter((date) =>
// years.includes(date.date.split("/")[2])
// );
//TODO: Set start date for polls
const firstIndex = dateArray.indexOf(options.startDate);
const lastIndex = dateArray.indexOf(pollsByDateArray[0].date);
function getFirstPollIndex(date) {
const [M, D, Y] = date.split("/").map((d) => +d);
const firstPollIndex = pollsByDateArray.findIndex((d) => d.date === date);
if (firstPollIndex === -1) {
const nextDate = incrementDate(date);
if (!nextDate) {
console.log("Can't find start date, returning all polls");
return 0;
}
return getFirstPollIndex(nextDate);
}
return firstPollIndex;
}
function incrementDate(date) {
const dateIndex = dateArray.indexOf(date);
return dateArray[dateIndex + 1];
}
// console.log(incrementDate("1/1/23"));
// console.log(getFirstPollIndex("1/1/23"));
// const firstPollIndex = pollsByDateArray.findIndex(
// (d) => d.date === startDate
// );
const firstPollIndex = getFirstPollIndex(options.startDate);
pollsByDateArray = pollsByDateArray.filter((d, i) => i <= firstPollIndex);
dateArray = dateArray.filter((d, i) => i >= firstIndex && i <= lastIndex);
dateArray.reverse();
stateArray = stateArray.sort();
return [pollsByDateArray, dateArray, stateArray];
}
function erase() {
d3.select(".poll-button").style("display", "block");
let svg = d3.select(".poll-chart-div");
svg.selectAll("*").remove();
let parent = d3.select(".poll-grid");
parent.selectAll("*").remove();
// parent.append("div").attr("class", "poll-loading");
}
function build(candidates, dateArray, dateIndex, options) {
const filteredDateArray = dateArray
.filter((date) => date.filteredPolls.length > 0)
.sort((a, b) =>
dateWithZeroes(b.date).localeCompare(dateWithZeroes(a.date))
);
function dateWithZeroes(date) {
return date
.split("/")
.map((d) => d.padStart(2, "0"))
.join("/");
}
//CHART
let svgDiv = d3.select(".poll-chart-div");
let chartHover = d3.select(".poll-chart-hover");
chartHover.selectAll("*").remove();
// chartHover.classed("empty", true);
let chart = {
width: svgDiv.node().offsetWidth,
height: svgDiv.node().offsetHeight,
};
let svg = svgDiv.append("svg").attr("height", "100%").attr("width", "100%");
drawChart();
window.addEventListener(
"resize",
debounce(() => {
drawChart();
})
);
function debounce(func, delay = 100) {
var timer;
return function (event) {
if (timer) clearTimeout(timer);
timer = setTimeout(func, delay, event);
};
}
function drawChart() {
const chartWidth = svgDiv.node().offsetWidth;
chart.width = chartWidth;
svg.selectAll("*").remove();
let gridLayer = svg.append("g").attr("class", "grid-layer");
let circleLayer = svg.append("g").attr("class", "circle-layer");
let lineLayer = svg.append("g").attr("class", "line-layer");
let min = 30;
let max = 60;
let gap = 0;
filteredDateArray.forEach((date) => {
date.filteredPolls.forEach((poll) => {
poll.filteredQuestions.forEach((question) => {
const nums = question.results
.filter((d) => ["Harris", "Trump"].includes(d.answer))
.map((d) => +d.pct);
const pollGap = Math.abs(nums[0] - nums[1]);
nums.forEach((num) => {
min = Math.min(num, min);
max = Math.max(num, max);
gap = Math.max(pollGap, gap);
});
});
});
});
if (max - min < 10) {
max += 5;
min -= 5;
}
// console.log(min, max, gap);
let yScale = d3
.scaleLinear()
.domain([min, max])
.range([chart.height - 30, 10]);
let xScale = d3
.scaleLinear()
.domain([0, dateIndex.length - 1])
.range([30, chart.width - 50]);
let rScale = d3.scaleSqrt().domain([0, 20000]).range([1, 6]);
function mapGaps(array, max) {
array.forEach((date) => {
date.index =
dateIndex.length - dateIndex.findIndex((d) => d === date.date);
});
array.forEach((date, i) => {
let prev = i > 0 ? array[i - 1] : null;
let next = i < array.length - 1 ? array[i + 1] : null;
let a = prev ? (date.index + prev.index) / 2 : array[0].index + 10;
let b = next ? (date.index + next.index) / 2 : -10;
date.rectIndeces = [a, b];
});
}
function makeSparkChart(poll, parent = chartHover) {
let div = parent.append("div").attr("class", "spark-div");
div
.append("div")
.attr("class", "spark-pollster bold")
.text(poll.pollster);
poll.filteredQuestions.forEach((question) => {
let chart = div.append("div").attr("class", "spark-chart");
question.results
// .filter((r) => r.answer === "Biden" || r.answer === "Trump")
.forEach((result) => {
chart.append("div").attr("class", "spark-name").text(result.answer);
let lineDiv = chart.append("div").attr("class", "spark-line-div");
lineDiv
.append("div")
.attr("class", `spark-line ${result.answer} ${result.party}`)
.style("width", `calc(${result.pct}%)`);
lineDiv
.append("div")
.attr("class", `spark-number ${result.answer} ${result.party}`)
.text(`${Math.round(result.pct)}%`);
});
});
}
if (bool(options.showSpark)) {
const date = filteredDateArray[0];
if (!date) return;
chartHover.selectAll("*").remove();
chartHover
.append("div")
.attr("class", "spark-date")
.text(dateFromSlashes(date.date, true, true));
date.filteredPolls.forEach((poll) => {
makeSparkChart(poll);
});
}
mapGaps(filteredDateArray, 0);
filteredDateArray.forEach((date) => {
date.filteredPolls.forEach((poll) => {
poll.filteredQuestions.forEach((question) => {
question.results
// .filter((q) => q.answer === "Biden" || q.answer === "Trump")
.forEach((result) => {
let circle = circleLayer
.append("circle")
//.attr("r", rScale(Number(response.sample_size)))
.attr("r", 2.5)
.attr("cx", xScale(date.index))
.attr("cy", yScale(Number(result.pct)))
.attr("class", `chart-circle ${result.answer} ${result.party}`);
});
});
});
let rect = lineLayer.append("rect");
let line = lineLayer.append("line");
rect
.attr("x", xScale(date.rectIndeces[1]))
.attr("y", yScale(100))
.attr("width", () => {
return xScale(date.rectIndeces[0]) - xScale(date.rectIndeces[1]);
})
.attr("height", yScale(0) - yScale(100))
.attr("fill", "rgba(0,0,0,0)")
.on("mousemove", () => {
line.style("stroke", "rgba(0,0,0,0.4)");
})
.on("click", () => {
chartHover.selectAll("*").remove();
chartHover
.append("div")
.attr("class", "spark-date")
.text(dateFromSlashes(date.date, true, true));
if (bool(options.showSpark)) {
date.filteredPolls.forEach((poll) => {
makeSparkChart(poll);
});
}
chartHover.classed("empty", false);
})
.on("mouseout", () => {
line.style("stroke", "rgba(0,0,0,0)");
});
line
.attr("class", "hover-line")
.attr("x1", xScale(date.index))
.attr("y1", yScale(100))
.attr("x2", xScale(date.index))
.attr("y2", yScale(0))
.style("stroke", "rgba(0,0,0,0)")
.attr("pointer-events", "none");
});
for (let i = 0; i <= 100; i += 10) {
gridLayer
.append("line")
.attr("class", "chart-grid-line")
.attr("y1", yScale(i))
.attr("y2", yScale(i))
.attr("x1", xScale(1))
.attr("x2", chart.width - 40);
gridLayer
.append("text")
.attr("class", "chart-grid-text")
.text(`${i}%`)
.attr("text-anchor", "start")
.attr("x", chart.width - 36)
.attr("y", yScale(i) + 3);
}
dateIndex.forEach((date, i) => {
const [month, day, year] = date.split("/");
if (day === "1") {
gridLayer
.append("line")
.attr("class", "chart-grid-line")
.attr("y1", yScale(0))
.attr("y2", yScale(100))
.attr("x1", xScale(dateIndex.length - i))
.attr("x2", xScale(dateIndex.length - i));
gridLayer
.append("text")
.attr("class", "chart-grid-date")
.text(() => {
if (chartWidth < 500 && Number(month) % 2 === 0) {
return;
}
return dateFromSlashes(date, true, month === "1");
})
.attr("text-anchor", "middle")
.attr("x", xScale(dateIndex.length - i))
.attr("y", chart.height - 10);
}
});
}
// POLLS
if (!bool(options.showPolls)) {
return;
}
const parent = d3.select(".poll-grid");
// // For each poll...
filteredDateArray.forEach((date, i) => {
/* Cap date of displayed polls */
const dateContainer = parent.append("div").attr("class", "date-container");
const dateText = dateContainer
.append("div")
.attr("class", "date-text bold")
.text(dateFromSlashes(date.date, true, true));
date.filteredPolls.forEach((poll) => {
// ...add a div and the pollster's name...
const pollDiv = dateContainer.append("div").attr("class", "poll-div");
const pollName = pollDiv
.append("div")
.attr("class", "poll-name")
.html(poll.pollster);
const stateDate = pollDiv.append("div").attr("class", "state-date-div");
//State div
const stateDiv = stateDate
.append("div")
.attr("class", "state-div bold")
.text(
`${
poll.filteredQuestions[0].state === ""
? "National"
: poll.filteredQuestions[0].state
}`
);
//Date div
const dateDiv = stateDate
.append("div")
.attr("class", "date-div")
.text((d) => {
const start = dateFromSlashes(
poll.filteredQuestions[0].results[0].start_date,
true
);
const end = dateFromSlashes(
poll.filteredQuestions[0].results[0].end_date,
true,
true
);
return `${start} – ${end}`;
});
// ...and then one div for the questions within the poll
const allQuestionDiv = pollDiv
.append("div")
.attr("class", "all-question-div");
const headings = allQuestionDiv
.append("div")
.attr("class", "main-candidates headers");
const names = headings.append("div").attr("class", "poll-heading");
names
.selectAll("div.candidate-name")
.data(candidates)
.join("div")
.attr("class", "candidate-name")
.text((d) => d.name);
// names.append("div").attr("class", "candidate-name").text(candidate1);
// names.append("div").attr("class", "candidate-name").text(candidate2);
const lead = headings
.append("div")
.attr("class", "poll-heading")
.text("Point lead");
const sample = headings
.append("div")
.attr("class", "poll-heading")
.text("Sample size/type");
// Initialize a counter to see how many valid questions the poll has. If 0, we end up removing poll
let questionCount = 0;
// For each individual question...
poll.filteredQuestions.forEach((question) => {
// Check results objects for chosen candidates
const dem = question.results.find((r) =>
stringIsEqual(
r.answer,
candidates.find((c) => c.party === "DEM").name
)
);
const rep = question.results.find((r) =>
stringIsEqual(
r.answer,
candidates.find((c) => c.party === "REP").name
)
);
// If we don't have a result for both of them (some polls have Biden vs. Pence, etc.)
if (!dem || !rep) {
return;
} else {
questionCount++;
const otherCandidates = question.results.filter(
(result) => result !== dem && result !== rep
);
// ...add a results container div...
const questionDiv = allQuestionDiv
.append("div")
.attr("class", "question-div");
const mainDiv = questionDiv
.append("div")
.attr("class", "main-candidates");
if (otherCandidates.length > 0) {
questionDiv
.append("div")
.attr("class", "other-candidates")
.selectAll(".candidate")
.data(otherCandidates)
.join("div")
.attr("class", "candidate")
.text((d) => `${d.answer}: ${Math.round(+d.pct)}%`);
}
// ...a div for each result ...
const resultsDiv = mainDiv.append("div").attr("class", "results-div");
const c1Num = Math.round(dem.pct);
const c2Num = Math.round(rep.pct);
const diffNum = Math.round(rep.pct - dem.pct);
const c1NumberDiv = resultsDiv
.append("div")
.attr("class", `biden DEM results-number`);
c1NumberDiv
.html(`${c1Num}%`)
.style("background", c1Num > c2Num ? "#cfeeff" : "#fff");
const c2NumberDiv = resultsDiv
.append("div")
.attr("class", "trump REP results-number ");
c2NumberDiv
.html(`${c2Num}%`)
.style("background", c2Num > c1Num ? "#ffc4bd" : "#fff");
// Make the point lead difference div
const diffDiv = mainDiv.append("div").attr("class", "diff-div");
diffDiv.append("div").attr("class", "diff-left").html(" ");
diffDiv.append("div").attr("class", "diff-right").html(" ");
const factor = 1.5; //0.5 = max of 100, 1.0 = max of 50
diffDiv
.append("div")
.attr("class", "diff-line")
.style(
"border-top",
`2px solid ${
diffNum > 0 ? "#f04f3c" : diffNum < 0 ? "#3caef0" : "#777"
}`
)
.style(
"left",
`${Math.max(0, Math.min(50 + diffNum * factor, 50))}%`
)
.style(
"width",
`calc(${Math.min(50, Math.abs(diffNum * factor))}%)`
);
diffDiv
.append("div")
.attr("class", "diff-ball")
.text(Math.round(Math.abs(rep.pct - dem.pct))) //fix because -8.5 Math.rounds to -8 when we want -9, e.g.
.style(
"border",
`2px solid ${
diffNum > 0 ? "#f04f3c" : diffNum < 0 ? "#3caef0" : "#777"
}`
)
.style(
"background",
`${diffNum > 0 ? "#ffc4bd" : diffNum < 0 ? "#cfeeff" : "#fff"}`
)
.style(
"left",
`calc(${Math.max(0, 50 + diffNum * factor)}% - 11px)`
);
const sampleDiv = mainDiv.append("div").attr("class", "sample-div");
const sampleSize = sampleDiv
.append("div")
.attr("class", "sample-size")
.text(
question.sample_size
? Number(question.sample_size).toLocaleString()
: ""
);
const typeKey = {
lv: "likely voters",
rv: "registered voters",
a: "adults",
v: "voters",
};
const sampleType = sampleDiv
.append("div")
.attr("class", "sample-type")
.text(typeKey[question.type]);
const pollLink = mainDiv
.append("a")
.attr("href", question.url)
.attr("target", "_blank")
.attr("class", "poll-link bold")
.text("LINK");
}
});
// Remove poll if no questions meet criteria (SHow only filtered state & Trump vs. Biden)
if (questionCount === 0) {
pollDiv.remove();
}
});
});
}
// Data filtering functions
function bool(value) {
if (typeof value === "boolean") {
return value;
} else if (typeof value === "string") {
const string = value.trim().toLowerCase();
if (string === "false") {
return false;
} else if (string === "true") {
return true;
}
}
// console.warn(`${typeof value} "${value}" returned as false`);
return false;
}
function stringIsEqual(a, b) {
return a.trim().toLowerCase() === b.trim().toLowerCase();
}
function filterByLocation(data, location) {
if (location === "all") return data;
const filteredData = [];
data.forEach((pollGroup) => {
const date = {
date: pollGroup.date,
polls: [],
};
pollGroup.polls.forEach((poll) => {
if (
poll.questions.some((question) =>
stringIsEqual(question.state, location)
)
) {
date.polls.push(poll);
}
});
filteredData.push(date);
});
return filteredData;
}
function filterByCandidates(data, candidates) {
const filteredData = [];
data.forEach((pollGroup) => {
const date = {
date: pollGroup.date,
polls: pollGroup.polls,
filteredPolls: [],
};
pollGroup.polls.forEach((poll) => {
const filteredPoll = { ...poll, filteredQuestions: [] };
poll.questions.forEach((question) => {
// const isCorrectLength = question.results.length === 2;
const isCorrectLength = question.results.length >= 2;
let hasCandidates = true;
candidates.forEach((candidate) => {
const resultForCandidate = question.results.some((r) =>
stringIsEqual(r.answer, candidate.name)
);
if (!resultForCandidate) {
hasCandidates = false;
}
});
if (isCorrectLength && hasCandidates) {
filteredPoll.filteredQuestions.push(question);
}
});
if (filteredPoll.filteredQuestions.length > 0) {
date.filteredPolls.push(filteredPoll);
}
});
filteredData.push(date);
});
return filteredData;
}
function filterData(data, location, candidates) {
const locData = filterByLocation(data, location);
const candidatesData = filterByCandidates(locData, candidates);
return candidatesData;
}
function dateFromSlashes(slashDate, showDay, showYear = false) {
const monthArray = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const [monthNum, dayNum, yearNum] = slashDate
.split("/")
.map((num) => Number(num));
const justMonth = !showDay && !showYear;
const month = monthArray[monthNum - 1];
const monthPeriod = !justMonth ? "." : "";
const day = showDay ? ` ${dayNum}` : "";
const yearComma = showDay && showYear ? "," : "";
const year = showYear ? ` 20${yearNum}` : "";
return `${month}${monthPeriod}${day}${yearComma}${year}`;
}
}
function addInteractive(interactiveFunction) {
const root = document.querySelector(".root");
const article = document.querySelector(".articlefragment");
if (root && article) {
const mutationTarget = root;
const mutationObserverConfig = { attributes: true };
const callback = function (mutations) {
for (let mutation of mutations) {
if (mutation.attributeName === "data-v-app") {
interactiveFunction();
}
}
};
const observer = new MutationObserver(callback);
observer.observe(mutationTarget, mutationObserverConfig);
} else {
interactiveFunction();
}
}
addInteractive(makeInteractive);