{
let landguzzlersViz = d3.select(svgContainer);
const xScale = d3.scaleLinear()
.domain([-78.53, -78.44])
.range([-100, 2200]);
const yScale = d3.scaleLinear()
.domain([38.00, 38.07])
.range([1350, 0]);
const colorScale = d3.scaleOrdinal()
.domain(crimedata.map(d => d.Offense))
.range(d3.schemeCategory10);
landguzzlersViz.selectAll("circle")
.data(crimedata)
.enter()
.append("circle")
.attr("cx", d => xScale(d.longitude))
.attr("cy", d => yScale(d.latitude))
.attr("r", 5)
.style("fill", d => colorScale(d.Offense));
const legend = landguzzlersViz.append("g")
.attr("class", "legend")
.attr("transform", "translate(15, 100)");
const uniqueOffenses = Array.from(new Set(crimedata.map(d => d.Offense)));
legend.selectAll(".legend-item")
.data(uniqueOffenses)
.enter().append("g")
.attr("class", "legend-item")
.attr("transform", (d, i) => "translate(0," + (i * 20) + ")")
.each(function (d) {
const g = d3.select(this);
g.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 10)
.attr("height", 10)
.style("fill", colorScale(d));
g.append("text")
.attr("x", 15)
.attr("y", 5)
.attr("dy", "0.7em")
.style("fill", "white")
.text(d);
});
landguzzlersViz.append("text")
.attr("x", xScale(-78.4767))
.attr("y", yScale(38.0293) - 15)
.text("Downtown")
.style("fill", "white")
.style("font-weight", "bold")
.style("font-size", "25px")
.style("text-anchor", "middle");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.4989))
.attr("cy", yScale(38.0312))
.attr("r", 7)
.style("fill", "#E57373")
.style("stroke", "red");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.4989))
.attr("cy", yScale(38.0312))
.attr("r", 80)
.style("fill", "none")
.style("stroke", "#E57373");
landguzzlersViz.append("text")
.attr("x", xScale(-78.4989))
.attr("y", yScale(38.0312) - 15)
.text("The Hospital")
.style("fill", "#E57373")
.style("font-size", "25px")
.style("font-weight", "bold")
.style("text-anchor", "middle");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.4944033))
.attr("cy", yScale(38.0430252))
.attr("r", 7)
.style("fill", "#E57373")
.style("stroke", "#E57373");
landguzzlersViz.append("text")
.attr("x", xScale(-78.4944033))
.attr("y", yScale(38.0430252) - 15)
.text("Apartment 2022")
.style("fill", "#E57373")
.style("font-size", "25px")
.style("font-weight", "bold")
.style("text-anchor", "middle");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.512216))
.attr("cy", yScale(38.0284118))
.attr("r", 7)
.style("fill", "#E57373")
.style("stroke", "#E57373");
landguzzlersViz.append("text")
.attr("x", xScale(-78.512216))
.attr("y", yScale(38.0284118) - 15)
.text("Apartment 2020")
.style("fill", "#E57373")
.style("font-size", "25px")
.style("font-weight", "bold")
.style("text-anchor", "middle");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.4933017))
.attr("cy", yScale(38.0320438))
.attr("r", 7)
.style("fill", "#E57373")
.style("stroke", "#E57373");
landguzzlersViz.append("text")
.attr("x", xScale(-78.4933017))
.attr("y", yScale(38.0320438) - 15)
.text("Apartment 2018")
.style("fill", "#E57373")
.style("font-size", "25px")
.style("font-weight", "bold")
.style("text-anchor", "middle");
landguzzlersViz.append("text")
.attr("x", xScale(-78.5050))
.attr("y", yScale(38.0528) - 15)
.text("Barracks Road")
.style("fill", "white")
.style("font-weight", "bold")
.style("font-size", "25px")
.style("text-anchor", "middle");
landguzzlersViz.append("text")
.attr("x", xScale(-78.510640))
.attr("y", yScale(38.014700) - 15)
.text("Fry's Spring")
.style("fill", "white")
.style("font-weight", "bold")
.style("font-size", "25px")
.style("text-anchor", "middle");
landguzzlersViz.append("text")
.attr("x", xScale(-78.495830))
.attr("y", yScale(38.026180) - 15)
.text("Fifeville")
.style("fill", "white")
.style("font-weight", "bold")
.style("font-size", "25px")
.style("text-anchor", "middle");
landguzzlersViz.append("text")
.attr("x", xScale(-78.473340))
.attr("y", yScale(38.022920) - 15)
.text("Belmont")
.style("fill", "white")
.style("font-weight", "bold")
.style("font-size", "25px")
.style("text-anchor", "middle");
landguzzlersViz.append("text")
.attr("x", xScale(-78.513837))
.attr("y", yScale(38.026459) - 15)
.text("JPA")
.style("fill", "white")
.style("font-weight", "bold")
.style("font-size", "25px")
.style("text-anchor", "middle");
landguzzlersViz.append("text")
.attr("x", xScale(-78.4908))
.attr("y", yScale(38.0357) - 15)
.text("10th and Page")
.style("fill", "white")
.style("font-weight", "bold")
.style("font-size", "25px")
.style("text-anchor", "middle");
const lineGenerator = d3.line()
.x(d => d.x)
.y(d => d.y);
const linesData = [
{ start: { x: xScale(-78.4989), y: yScale(38.0312) }, end: { x: xScale(-78.4944033), y: yScale(38.0430252) } },
{ start: { x: xScale(-78.4989), y: yScale(38.0312) }, end: { x: xScale(-78.512216), y: yScale(38.0284118) } },
{ start: { x: xScale(-78.4989), y: yScale(38.0312) }, end: { x: xScale(-78.4933017), y: yScale(38.0320438) } }
];
landguzzlersViz.selectAll(".line")
.data(linesData)
.enter().append("path")
.attr("class", "line")
.attr("d", d => lineGenerator([d.start, d.end]))
.attr("stroke", "#E57373")
.attr("stroke-width", 2)
.attr("fill", "none");
landguzzlersViz.append("text")
.attr("x", 15)
.attr("y", 1060)
.style("fill", "#BDBDBD")
.text("Offense Filter Controls");
const offenseFilterForeignObject = landguzzlersViz.append("foreignObject")
.attr("x", 15)
.attr("y", 1075)
.attr("width", 200)
.attr("height", 100);
const offenseFilterDiv = offenseFilterForeignObject.append("xhtml:div");
const offenseFilterSelect = offenseFilterDiv.append("xhtml:select")
.attr("id", "offense-filter-select")
.attr("class", "input-field");
offenseFilterSelect.selectAll("option")
.data([...new Set(crimedata.map(d => d.Offense))])
.enter().append("xhtml:option")
.attr("value", d => d)
.text(d => d);
offenseFilterDiv.append("xhtml:button")
.attr("id", "offense-filter-button")
.text("Filter");
const filterByOffense = () => {
const selectedOffense = document.getElementById("offense-filter-select").value;
const filteredData = crimedata.filter(d => d.Offense === selectedOffense);
landguzzlersViz.selectAll("circle").remove();
landguzzlersViz.selectAll("circle")
.data(filteredData)
.enter()
.append("circle")
.attr("cx", d => xScale(d.longitude))
.attr("cy", d => yScale(d.latitude))
.attr("r", 5)
.style("fill", colorScale(selectedOffense));
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.4989))
.attr("cy", yScale(38.0312))
.attr("r", 7)
.style("fill", "#E57373")
.style("stroke", "red");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.4989))
.attr("cy", yScale(38.0312))
.attr("r", 80)
.style("fill", "none")
.style("stroke", "#E57373");
landguzzlersViz.append("text")
.attr("x", xScale(-78.4989))
.attr("y", yScale(38.0312) - 15)
.text("The Hospital")
.style("fill", "#E57373")
.style("font-size", "25px")
.style("font-weight", "bold")
.style("text-anchor", "middle");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.4944033))
.attr("cy", yScale(38.0430252))
.attr("r", 7)
.style("fill", "#E57373")
.style("stroke", "#E57373");
landguzzlersViz.append("text")
.attr("x", xScale(-78.4944033))
.attr("y", yScale(38.0430252) - 15)
.text("Apartment 2022")
.style("fill", "#E57373")
.style("font-size", "25px")
.style("font-weight", "bold")
.style("text-anchor", "middle");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.512216))
.attr("cy", yScale(38.0284118))
.attr("r", 7)
.style("fill", "#E57373")
.style("stroke", "#E57373");
landguzzlersViz.append("text")
.attr("x", xScale(-78.512216))
.attr("y", yScale(38.0284118) - 15)
.text("Apartment 2020")
.style("fill", "#E57373")
.style("font-size", "25px")
.style("font-weight", "bold")
.style("text-anchor", "middle");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.4933017))
.attr("cy", yScale(38.0320438))
.attr("r", 7)
.style("fill", "#E57373")
.style("stroke", "#E57373");
landguzzlersViz.append("text")
.attr("x", xScale(-78.4933017))
.attr("y", yScale(38.0320438) - 15)
.text("Apartment 2018")
.style("fill", "#E57373")
.style("font-size", "25px")
.style("font-weight", "bold")
.style("text-anchor", "middle");
};
const offenseFilterButton = document.getElementById("offense-filter-button");
offenseFilterButton.addEventListener("click", filterByOffense);
landguzzlersViz.append("text")
.attr("x", 15)
.attr("y", 1140)
.style("fill", "#BDBDBD")
.text("Hour Filter Controls");
const hourFilterForeignObject = landguzzlersViz.append("foreignObject")
.attr("x", 15)
.attr("y", 1150)
.attr("width", 200)
.attr("height", 100);
const hourFilterDiv = hourFilterForeignObject.append("xhtml:div");
hourFilterDiv.append("xhtml:input")
.attr("type", "number")
.attr("id", "start-hour-input")
.attr("class", "input-field")
.attr("placeholder", "Start Hour");
hourFilterDiv.append("xhtml:input")
.attr("type", "number")
.attr("id", "end-hour-input")
.attr("class", "input-field")
.attr("placeholder", "End Hour");
hourFilterDiv.append("xhtml:button")
.attr("id", "hour-filter-button")
.text("Filter");
const hourFilterButton = document.getElementById("hour-filter-button");
hourFilterButton.addEventListener("click", () => {
const startHour = parseInt(document.getElementById("start-hour-input").value);
const endHour = parseInt(document.getElementById("end-hour-input").value);
if (isNaN(startHour) || isNaN(endHour) || startHour > endHour) {
alert("Please enter a valid hour range.");
return;
}
const filteredData = crimedata.filter(d => d['HourReported'] >= startHour && d['HourReported'] <= endHour);
landguzzlersViz.selectAll("circle").remove();
landguzzlersViz.selectAll("circle")
.data(filteredData)
.enter()
.append("circle")
.attr("cx", d => xScale(d.longitude))
.attr("cy", d => yScale(d.latitude))
.attr("r", 5)
.style("fill", d => colorScale(d.Offense));
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.4989))
.attr("cy", yScale(38.0312))
.attr("r", 7)
.style("fill", "#E57373")
.style("stroke", "red");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.4989))
.attr("cy", yScale(38.0312))
.attr("r", 80)
.style("fill", "none")
.style("stroke", "#E57373");
landguzzlersViz.append("text")
.attr("x", xScale(-78.4989))
.attr("y", yScale(38.0312) - 15)
.text("The Hospital")
.style("fill", "#E57373")
.style("font-size", "25px")
.style("font-weight", "bold")
.style("text-anchor", "middle");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.4944033))
.attr("cy", yScale(38.0430252))
.attr("r", 7)
.style("fill", "#E57373")
.style("stroke", "#E57373");
landguzzlersViz.append("text")
.attr("x", xScale(-78.4944033))
.attr("y", yScale(38.0430252) - 15)
.text("Apartment 2022")
.style("fill", "#E57373")
.style("font-size", "25px")
.style("font-weight", "bold")
.style("text-anchor", "middle");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.512216))
.attr("cy", yScale(38.0284118))
.attr("r", 7)
.style("fill", "#E57373")
.style("stroke", "#E57373");
landguzzlersViz.append("text")
.attr("x", xScale(-78.512216))
.attr("y", yScale(38.0284118) - 15)
.text("Apartment 2020")
.style("fill", "#E57373")
.style("font-size", "25px")
.style("font-weight", "bold")
.style("text-anchor", "middle");
landguzzlersViz.append("circle")
.attr("cx", xScale(-78.4933017))
.attr("cy", yScale(38.0320438))
.attr("r", 7)
.style("fill", "#E57373")
.style("stroke", "#E57373");
landguzzlersViz.append("text")
.attr("x", xScale(-78.4933017))
.attr("y", yScale(38.0320438) - 15)
.text("Apartment 2018")
.style("fill", "#E57373")
.style("font-size", "25px")
.style("font-weight", "bold")
.style("text-anchor", "middle");
});
landguzzlersViz.append("text")
.attr("x", 15)
.attr("y", 1240)
.style("fill", "#BDBDBD")
.text("Longitude and Latitude Filter Content");
const foreignObject = landguzzlersViz.append("foreignObject")
.attr("x", 15)
.attr("y", 1250)
.attr("width", 200)
.attr("height", 100);
const div = foreignObject.append("xhtml:div");
div.append("xhtml:input")
.attr("type", "number")
.attr("id", "latitude-input")
.attr("class", "input-field")
.attr("placeholder", "Latitude");
div.append("xhtml:input")
.attr("type", "number")
.attr("id", "longitude-input")
.attr("class", "input-field")
.attr("placeholder", "Longitude");
div.append("xhtml:button")
.attr("id", "update-button")
.text("Update");
const latitudeInput = document.getElementById("latitude-input");
const longitudeInput = document.getElementById("longitude-input");
const updateButton = document.getElementById("update-button");
updateButton.addEventListener("click", () => {
const latitude = parseFloat(latitudeInput.value);
const longitude = parseFloat(longitudeInput.value);
if (isNaN(latitude) || isNaN(longitude)) {
alert("Please enter valid latitude and longitude values.");
return;
}
landguzzlersViz.append("circle")
.attr("id", "home-circle")
.attr("cx", xScale(longitude))
.attr("cy", yScale(latitude))
.attr("r", 7)
.style("fill", "red");
landguzzlersViz.append("circle")
.attr("id", "home-area")
.attr("cx", xScale(longitude))
.attr("cy", yScale(latitude))
.attr("r", 60)
.style("fill", "none")
.style("stroke", "red");
landguzzlersViz.append("text")
.attr("id", "home-label")
.attr("x", xScale(longitude))
.attr("y", yScale(latitude) - 15)
.text("home")
.style("fill", "white")
.style("font-size", "12px")
.style("text-anchor", "middle");
});
const narrativeText = `
To provide context to this visual, I would like to share my own personal narrative working at the hospital as a student and registered nurse. While the data highlights the apartments I resided in when I worked as a nurse at the hospital, this narrative resonates with many new graduate nurses embarking on their journey at UVA Medical Center. My journey as a nurse began in 2020 when I joined the hospital's medical intensive care unit. At the onset, newly graduated nurses like myself were compensated at a rate of $25 per hour to navigate the complexities of caring for critically ill patients reliant on various forms of life support. My schedule entailed rotating between day and night shifts, commencing or concluding at either 0700 or 1930. Much like my peers, I often found myself commuting to work during the early hours of 0600-0700 or returning home between 1930-2030.
<br><br>
One notable aspect of this commute was the transportation dilemma faced by nurses. While we had the option to obtain a parking pass, the designated parking lot for nurses was situated at JPJ, necessitating a bus commute to the hospital. This not only extended the duration and cost of an already demanding 12.5 hour shift but also underscored a concerning trend: UVA requires nurses to pay for parking permits and requires them to use distant lots despite closer options being available. This trend incentivizes nurses to find free street parking elsewhere to avoid this cost, sometimes exposing them them to unsafe circumstances. Such circumstances accentuate safety risks during the early morning and late evening hours and exemplify yet another instance of UVA's undervaluation of nurses' contributions at the hospital.
<br><br>
Turning to the visual representation, this abstract map illustrates crime data in Charlottesville, Virginia, spanning from 05/04/2019 to 01/04/2024. By filtering the data to identify the twelve most extreme/most frequent crimes and pinpointing those closest to my walking route to and from the hospital, we can better understand the risks faced by nurses during their daily commute. Additionally, you can input the longitude and latitude of your own residence to identify prevalent crimes in your area, thereby gaining insight into the broader safety landscape. <span style="color:#E57373;">This comprehensive approach underscores the urgent need to address transportation options for nurses, recognizing their pivotal role as frontline healthcare professionals within the hospital and advocating for their safety and well-being during their daily commute.</span>
`;
const narrativeForeignObject = landguzzlersViz.append("foreignObject")
.attr("x", 1680)
.attr("y", 15)
.attr("width", 500)
.attr("height", 1350);
const narrativeDiv = narrativeForeignObject.append("xhtml:div")
.style("font-size", "18px")
.style("padding", "10px")
.style("color", "#BDBDBD")
.style("background-color", "black")
.style("font-family", "'Rajdhani', sans-serif")
.style("text-align", "right");
narrativeDiv.html(narrativeText);
}