Public
Edited
Aug 19, 2024
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
houseMap = makeMap(
simplified_fixedboundaries_state_house,
houseWinners,
gradientBool
)
Insert cell
Insert cell
Insert cell
// variableColor is a flag for whether to show the gradient based on vote, true turns on gradient

function makeMap(map, mapData, variableColor, title, subtitle) {
const nudgeDown = 20;
const nudgeRight = 20;
var squareSizeScale = d3.scaleLinear().domain([300, 1100]).range([22, 40]);
var legendFontSize = d3.scaleLinear().domain([22, 44]).range([12, 17]);

var squareSize = squareSizeScale(width);
var halfSquare = squareSize / 2;

var height = width / 2.2;
const svg = d3.select(DOM.svg(width, height));

// const svg = d3.select(DOM.svg(width, height));

// Use Mercator projection
var projection = d3
.geoMercator()
// but .fitExtent() fits the dataset to a specified canvas
.fitExtent(
[
[20, 20],
[width, height]
],
map
);

var path = d3.geoPath().projection(projection);

// var barChart = createBarChart(
// svg,
// parseHouseJSON(aggregateResults),
// 0,
// 30,
// 20,
// width,
// 100
// );

// height = 100,
// barHeight = 20,
// halfBarHeight = barHeight / 2,
// f = d3.format(".1f"),
// margin = { top: 20, right: 10, bottom: 20, left: 10 },
// w = width - margin.left - margin.right,
// h = height * 0.5

// Draw each district as a path
svg
.append("g")
.selectAll("path")
.data(map.features)
.enter()
.append("path")
.attr("d", path)
// Styling
.style("fill", (d) =>
color(
mapData[d.properties.DISTRICT]["winnerParty"],
mapData[d.properties.DISTRICT]["votePerc"],
variableColor
)
)
.style("stroke", "white")
.style("stroke-width", 0.5);

// Adds title
svg
.append("text")
.text(title)
.attr("x", 20)
.attr("y", 40)
.attr("font-size", 36);

// Adds subtitle
svg
.append("text")
.text(subtitle)
.attr("x", 30)
.attr("y", 70)
.attr("font-size", 20);

// Create legend if variable colors are on.
if (variableColor == true) {
var legend = createLegend(
svg,
20,
20,
1,
squareSize,
nudgeRight,
nudgeDown,
legendFontSize
);
}

return svg.node();
}
Insert cell
function createLegend(
svg,
x,
y,
scale,
squareSize,
nudgeRight,
nudgeDown,
legendFontSize
) {
var group = svg.append("g");
var titleGap = 5;
var halfSquare = squareSize / 2;

var scaleDataDEM = [
["DEM", ".5", "True"],
["DEM", ".6", "True"],
["DEM", ".7", "True"],
["DEM", ".8", "True"],
["DEM", ".9", "True"],
["DEM", "1.0", "True"]
];
var scaleDataGOP = [
["GOP", ".5", "True"],
["GOP", ".6", "True"],
["GOP", ".7", "True"],
["GOP", ".8", "True"],
["GOP", ".9", "True"],
["GOP", "1.0", "True"]
];

// Democrat squares
group
.selectAll("rect-dem")
.data(scaleDataDEM)
.enter()
.append("rect")
.attr("class", "scale-dem")
.attr("x", (d, i) => i * squareSize + nudgeRight)
.attr("y", nudgeDown + titleGap)
.attr("height", squareSize)
.attr("width", squareSize)
.style("fill", (d) => color(d[0], d[1], d[2]))
.style("stroke", "black")
.style("stroke-width", 0);

// Republican squares
group
.selectAll("rect-rep")
.data(scaleDataGOP)
.enter()
.append("rect")
.attr("class", "scale-rep")
.attr("x", (d, i) => i * squareSize + nudgeRight)
.attr("y", nudgeDown + squareSize + titleGap)
.attr("height", squareSize)
.attr("width", squareSize)
.style("fill", (d) => color(d[0], d[1], d[2]))
.style("stroke", "black")
.style("stroke-width", 0);

// Legend labels
group
.selectAll("text-legend")
.data(scaleDataGOP)
.enter()
.append("text")
.attr("class", "text-legend")
.attr("x", (d, i) => i * squareSize + halfSquare + nudgeRight)
.attr("y", nudgeDown + squareSize + titleGap - halfSquare)
.text((d) => d[1] * 100)
.style("font-size", legendFontSize(squareSize))
.style("fill", "white")
.style("stroke-width", 0)
.style("text-anchor", "middle")
.style("dominant-baseline", "middle");

// Legend labels
group
.selectAll("text-legend")
.data(scaleDataGOP)
.enter()
.append("text")
.attr("class", "text-legend")
.attr("x", (d, i) => i * squareSize + halfSquare + nudgeRight)
.attr("y", nudgeDown + squareSize * 2 + titleGap - halfSquare)
.text((d) => d[1] * 100)
.style("font-size", legendFontSize(squareSize))
.style("fill", "white")
.style("stroke-width", 0)
.style("text-anchor", "middle")
.style("dominant-baseline", "middle");

// Apply a transformation to move and scale the entire group
group.attr("transform", `translate(${x}, ${y}) scale(${scale})`);

return group;
}
Insert cell
function createCheckmarkGroup(svg, x, y, scale, text, fill) {
// Create a group element
var group = svg.append("g");

// Append main text to the group
group
.append("text")
.attr("class", "label")
.text(text)
.attr("x", 20)
.attr("y", 40)
.attr("font-size", 36)
.style("text-anchor", "end");

// Append checkmark to the group
group
.append("path")
.attr("d", checkmark)
.attr("class", "checkmark")
.attr("transform", "translate(23, 25) scale(1.7)")
.attr("fill", fill || "blue"); // Use the provided fill or default to "blue"

// Apply a transformation to move and scale the entire group
group.attr("transform", `translate(${x}, ${y}) scale(${scale})`);

return group;
}
Insert cell
function createBarChart(svg, data, x, y, barHeight, w, height) {
var group = svg.append("g");
var halfBarHeight = barHeight / 2;
const h = height * 0.5;

const total = d3.sum(data, (d) => d.value);

// set up scales for horizontal placement
const xScale = d3.scaleLinear().domain([0, total]).range([0, w]);

// Format the data (instead of using d3.stack()) and filter out 0 values:
function groupDataFunc(data) {
// use a scale to get percentage values
const percent = d3.scaleLinear().domain([0, total]).range([0, 100]);
// filter out data that has zero values
// also get mapping for next placement
// (save having to format data for d3 stack)
let cumulative = 0;
const _data = data
.map((d) => {
cumulative += d.value;
return {
value: d.value,
name: d.name,
// want the cumulative to prior value (start of rect)
cumulative: cumulative - d.value,
label: d.label,
color: d.color,
percent: percent(d.value)
};
})
.filter((d) => d.value > 0);
return _data;
}

// stack rect for each data value
group
.append("rect")
.attr("class", "rect-stacked")
.attr("x", (d) => xScale(d.cumulative))
.attr("y", h / 2 - halfBarHeight)
.attr("height", barHeight)
.attr("width", (d) => xScale(d.value))
.style("fill", (d) => d.color)
.attr("selected", "false")
.style("stroke", "black")
.style("stroke-width", 0);

// add dashed line in the middle
group
.append("path")
.attr(
"d",
d3.line()([
[width / 2 - 10, h / 2 - halfBarHeight - 10],
[width / 2 - 10, h / 2 + halfBarHeight + 10]
])
)
.attr("stroke", "white")
.attr("stroke-width", 3);
// .style("stroke-dasharray", "7, 2");

// Democrat Label
group
.append("text")
.text(data[0].name + "s")
.attr("x", 0)
.attr("y", 50)
.attr("text-anchor", "start");

// Republican Label
group
.append("text")
.text(data[6].name + "s")
.attr("x", w)
.attr("y", 50)
.attr("text-anchor", "end");

//Rep seats won
group
.append("text")
.text(data[6].value)
.attr("x", w)
.attr("y", 10)
.attr("text-anchor", "end");

//Dem seats won
group
.append("text")
.text(data[0].value)
.attr("x", 0)
.attr("y", 10)
.attr("text-anchor", "start");

// var checkMark1 = createCheckmarkGroup(sel, 87, 50, 0.5, "Democrats");

// checkMark1.select(".checkmark").style("fill", "#0571b0");
// scheckMark1.select(".label").style("fill", "#0571b0");

return group;
}
Insert cell
function stackedBarHouse(
data,
{
height = 100,
barHeight = 20,
halfBarHeight = barHeight / 2,
f = d3.format(".1f"),
margin = { top: 20, right: 10, bottom: 20, left: 10 },
w = width - margin.left - margin.right,
h = height * 0.5
} = {}
) {
// Have a total of values for reference from the data:
const total = d3.sum(data, (d) => d.value);
console.info("total", total);

// Format the data (instead of using d3.stack()) and filter out 0 values:
function groupDataFunc(data) {
// use a scale to get percentage values
const percent = d3.scaleLinear().domain([0, total]).range([0, 100]);
// filter out data that has zero values
// also get mapping for next placement
// (save having to format data for d3 stack)
let cumulative = 0;
const _data = data
.map((d) => {
cumulative += d.value;
return {
value: d.value,
name: d.name,
// want the cumulative to prior value (start of rect)
cumulative: cumulative - d.value,
label: d.label,
color: d.color,
percent: percent(d.value)
};
})
.filter((d) => d.value > 0);
return _data;
}

console.log("this is data", data);

const groupData = groupDataFunc(data);
console.info("groupData", groupData);

const svg = DOM.svg(width, height);
const sel = d3.select(svg);

// set up scales for horizontal placement
const xScale = d3.scaleLinear().domain([0, total]).range([0, w]);

const join = sel
.selectAll("g")
.data(groupData)
.join("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// stack rect for each data value
join
.append("rect")
.attr("class", "rect-stacked")
.attr("x", (d) => xScale(d.cumulative))
.attr("y", h / 2 - halfBarHeight)
.attr("height", barHeight)
.attr("width", (d) => xScale(d.value))
.style("fill", (d) => d.color)
.attr("selected", "false")
.style("stroke", "black")
.style("stroke-width", 0);
// .on("click", function () {
// const target = d3.select(this);
// const selected = target.attr("selected");
// console.log(selected);
// console.log(selected != "false");
// if (selected == "false") {
// target.style("stroke-width", 2).attr("selected", "true");
// } else {
// target.style("stroke-width", 0).attr("selected", "false");
// }
// target.raise();
// });

// add dashed line in the middle
join
.append("path")
.attr(
"d",
d3.line()([
[width / 2 - 10, h / 2 - halfBarHeight - 10],
[width / 2 - 10, h / 2 + halfBarHeight + 10]
])
)
.attr("stroke", "white")
.attr("stroke-width", 3);
// .style("stroke-dasharray", "7, 2");

// // Democrat Label
// join
// .append("text")
// .text(data[0].name + "s")
// .attr("x", 0)
// .attr("y", 50)
// .attr("text-anchor", "start");

// Republican Label
join
.append("text")
.text(data[6].name + "s")
.attr("x", w)
.attr("y", 50)
.attr("text-anchor", "end");

//Rep seats won
join
.append("text")
.text(data[6].value)
.attr("x", w)
.attr("y", 10)
.attr("text-anchor", "end");

//Dem seats won
join
.append("text")
.text(data[0].value)
.attr("x", 0)
.attr("y", 10)
.attr("text-anchor", "start");

// Geets the number of seats
var numberSeats =
data[0].value +
data[1].value +
data[2].value +
data[3].value +
data[4].value +
data[5].value +
data[6].value;

var checkMark1 = createCheckmarkGroup(sel, 87, 50, 0.5, "Democrats");

checkMark1.select(".checkmark").style("fill", "#0571b0");
checkMark1.select(".label").style("fill", "#0571b0");

// Sets the mark needed for someone to win.
// var winThresh = numberSeats / 2;
// join
// .append("path")
// .attr("d", checkmark)
// // Moves the check to a different side dependent on who won
// .attr("transform", () => {
// if (data[0].value > winThresh) {
// // Dems Won
// console.log("dems won");
// return "translate(85, 40)";
// }
// if (data[6].value > winThresh) {
// // Reps Won
// console.log("reps  won");
// return "translate(1020, 40)";
// }
// if (
// data[0].value + data[6].value == numberSeats &&
// data[0].value == data[6].value
// ) {
// // Nobody Won
// return "translate(-100, -100)";
// } else {
// // Others
// return "translate(-100, -100)";
// }
// })
// // Set the color of the check based on the winner of chamber
// .attr("fill", () => {
// if (data[0].value > winThresh) {
// // Dems Won
// console.log("dems won");
// return "#0571b0";
// }
// if (data[6].value > winThresh) {
// // Reps Won
// console.log("reps  won");
// return "#cc3c28";
// }
// if (
// data[0].value + data[6].value == numberSeats &&
// data[0].value == data[6].value
// ) {
// // Nobody Won
// return "white";
// } else {
// // Others
// return "white";
// }
// });

// join
// .append("text")
// .text("House")
// .attr("x", w / 2)
// .attr("y", h / 2 + halfBarHeight + 22)
// .attr("text-anchor", "middle");

return svg;
}
Insert cell
Insert cell
function color(party, votePerc, variable) {
if (party == "GOP") {
if (variable == false || votePerc == 1) {
return "#cc3c28";
} else {
return colorScaleGOP(votePerc);
}
}
if (party == "DEM") {
if (variable == false || votePerc == 1) {
return "#0571b0";
} else {
return colorScaleDem(votePerc);
}
} else {
return "purple";
}
}
Insert cell
colorScaleDem = d3.scaleLog().domain([.35, 1]).range(["white", "#0571b0"])

//"#82a6c7", "#0571b0"
Insert cell
colorScaleGOP = d3.scaleLog().domain([0.35, 1]).range(["white", "#cc3c28"])

// "#e59d93", "#cc3c28"
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function getWinnersVotePerc(race) {
// Returns object with the vote percents of candidates
var candidateVotePerc = getCandidatesVotePerc(race);
// Get the name of the winner
var winnerName = race["outcome"]["won"][0];
var votePerc;

for (const d in candidateVotePerc) {
// Check and see if there is a winner, if so pass back the party id, if there isn't return "no winner"
if (d == winnerName) {
votePerc = candidateVotePerc[d];
}
}

return votePerc;
}
Insert cell
function getCandidatesVotePerc(race) {
var totalVotes = race["top_reporting_unit"]["total_votes"];
if (totalVotes == 0) {
return 1;
}
return getVotePerc(getRaceCandidates(race), totalVotes);
}
Insert cell
function getVotePerc(candidates, totalVotes) {
var votes = {};
for (const d in candidates) {
// Check and see if there is a winner, if so pass back the party id, if there isn't return "no winner"
votes[candidates[d]["nyt_id"]] =
candidates[d]["votes"]["total"] / totalVotes;
}
return votes;
}
Insert cell
function getRaceCandidates(race) {
return race["top_reporting_unit"]["candidates"];
}
Insert cell
function getRaceInfo(race) {
var output = {};
// Name of winner
//output["winnerName"] = "None";
if (race["outcome"]["won"].length != 0) {
output["winnerName"] = race["outcome"]["won"][0];
} else {
output["winnerName"] = "No Winner";
}
// Party of winner

// move the find winners name function into winnerParty
output["winnerParty"] = winnerParty(race, output["winnerName"]);
console.log("details", getWinnersVotePerc(race));
if (getWinnersVotePerc(race) != undefined) {
output["votePerc"] = getWinnersVotePerc(race);
} else {
output["votePerc"] = 1;
}

// Returns if there is a winner, the name of the winner, party of winner(Also, eventually the race percentage)
return output;
}
Insert cell
function winnerParty(race, winnerName) {
var candidates = race["candidate_metadata"];
for (const d in candidates) {
// Check and see if there is a winner, if so pass back the party id, if there isn't return "no winner"
if (d == winnerName) {
return candidates[d]["party"]["nyt_id"];
}
}
return "No Winner";
}
Insert cell
function listWinners(races) {
var winners = {};
// Goes through the races and creates a new {} with all of the races in it.
for (const d in races) {
winners[races[d]["seat_number"]] = getRaceInfo(races[d]);
}
return winners;
}
Insert cell
Insert cell
houseWinners = listWinners(houseData)
Insert cell
senateWinners = listWinners(senateData)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
senateData = data["races"].filter((d) => d.office == "State Senate")
Insert cell
houseData = data["races"].filter((d) => d.office == "House of Delegates")
Insert cell
Insert cell
Insert cell
simplifiedStateSenate = FileAttachment("simplified state senate.geojson").json()
Insert cell
simplifiedstatehouse = FileAttachment("simplifiedStateHouse.geojson").json()
Insert cell
Insert cell
Insert cell
houseMapGEOJSON = FileAttachment("HOD Final.geojson").json()
Insert cell
Insert cell
simplified_fixedboundaries_state_house = FileAttachment("simplified_fixedboundaries_state_house.geojson").json()
Insert cell
simplified_fixedborders_state_senate = FileAttachment("simplified_fixedborders_state_senate.geojson").json()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more