Public
Edited
Apr 14
1 fork
Insert cell
Insert cell
import {vl} from '@vega/vega-lite-api-v5'
Insert cell
import {printTable} from "@uwdata/data-utilities"
Insert cell
suicideData = FileAttachment("full_data.csv").csv(d3.autoType)
Insert cell
vl.markArea({ opacity: 0.6 })
.data(suicideData)
.params([
{
name: "intentFilter",
bind: {
input: "select",
options: ["All", "Suicide", "Homicide"],
name: "Intent: "
},
value: "All"
},
{
name: "ageRange",
bind: {
input: "range",
min: 0,
max: 100,
step: 1,
name: "Minimum Age: "
},
value: 0
}
])
.transform([
{
filter: "datum.place === 'School/instiution' && (datum.intent === 'Suicide' || datum.intent === 'Homicide') && isValid(datum.age)"
},
{
filter: "intentFilter === 'All' || datum.intent === intentFilter"
},
{
filter: "datum.age >= ageRange"
},
{
density: "age",
groupby: ["intent"],
extent: [0, 100],
steps: 100,
as: ["age", "density"]
}
])
.encode(
vl.x({ field: "age", type: "quantitative", title: "Age of Victim" }),
vl.y({
field: "density",
type: "quantitative",
title: "Density",
axis: { titleFontWeight: "bold", grid: true }
}),
vl.color({ field: "intent", type: "nominal", title: "Intent" }),
vl.tooltip([
{ field: "intent", title: "Intent", type: "nominal" },
{ field: "age", title: "Age", type: "quantitative" },
{ field: "density", title: "Density", type: "quantitative", format: ".4f" }
])
)
.width(900)
.height(500)
.title("Age Distribution of Gun Deaths at Schools by Intent (Filtered)")
.render()



Insert cell
// Define age group helper
function getAgeGroup(age) {
if (age === null || isNaN(+age)) return null;
const numAge = +age;
const start = Math.floor(numAge / 5) * 5;
const end = start + 4;
return `${start}-${end}`;
}

Insert cell
grouped2 = (() => {
const result = {};
for (const d of suicideData) {
if (!d.age || !d.place || !d.intent) continue;
const ageGroup = getAgeGroup(d.age);
if (!ageGroup) continue; // <- skip nulls

const key = `${ageGroup}_${d.place}_${d.intent}`;
if (!result[key]) {
result[key] = { ageGroup, place: d.place, intent: d.intent, count: 0 };
}
result[key].count += 1;
}
return result;
})();

Insert cell
allGrouped = (() => {
const result = {};
for (const row of Object.values(grouped2)) {
const key = `${row.ageGroup}_${row.place}_All`;
if (!result[key]) {
result[key] = {
ageGroup: row.ageGroup,
place: row.place,
intent: "All",
count: 0
};
}
result[key].count += row.count;
}
return result;
})()
Insert cell
aggregatedData3 = [...Object.values(grouped2), ...Object.values(allGrouped)];

Insert cell
uniqueIntents = [...new Set(aggregatedData3.map(d => d.intent))].filter(d => d !== "All");
Insert cell
uniquePlaces = [...new Set(aggregatedData3.map(d => d.place))];
Insert cell
vl.markRect()
.data(aggregatedData3)
.params([
{
name: "intentFilter",
bind: {
input: "select",
options: ["All", ...uniqueIntents],
name: "Intent: "
},
value: "All"
},
{
name: "placeFilter",
bind: {
input: "select",
options: ["All", ...uniquePlaces],
name: "Location: "
},
value: "All"
}
])
.transform([
{ filter: "intentFilter === 'All' || datum.intent === intentFilter" },
{ filter: "placeFilter === 'All' || datum.place === placeFilter" }
])
.encode(
vl.x()
.fieldN("ageGroup")
.title("Age Group")
.sort([
"0-4", "5-9", "10-14", "15-19", "20-24", "25-29", "30-34", "35-39",
"40-44", "45-49", "50-54", "55-59", "60-64", "65-69", "70-74",
"75-79", "80-84", "85-89", "90-94", "95-99"
]),
vl.y().fieldN("place").title("Location").sort("ascending"),
vl.color()
.fieldQ("count")
.title("Number of Deaths")
.scale({ scheme: "reds", unknown: "#c6dbef" }),
vl.tooltip([
{ field: "ageGroup", title: "Age Group" },
{ field: "place", title: "Location" },
{ field: "intent", title: "Intent" },
{ field: "count", title: "Count" }
])
)
.width(900)
.height(500)
.title("Gun Deaths by Age Group and Location (Filterable by Intent and Location)")
.render();


Insert cell
raceData = suicideData
.filter(d => d.intent === "Suicide" || d.intent === "Homicide") // Keep only suicides & homicides
.reduce((acc, entry) => {
var race = entry["race"] || "Unknown"; // Handle missing values
if (!acc[race]) acc[race] = { Suicide: 0, Homicide: 0 };

acc[race][entry.intent] += 1;
return acc;
}, {});
Insert cell
formattedRaceData = Object.entries(raceData)
.flatMap(([race, counts]) => [
{ race, intent: "Suicide", count: counts.Suicide },
{ race, intent: "Homicide", count: counts.Homicide }
]);
Insert cell
raceChart = vl.layer(
// Base Strip Plot with Adjusted Points
vl.markCircle({ tooltip: true, size: 150, opacity: 0.8 })
.data(formattedRaceData)
.title("Gun Deaths by Race and Intent (Fixed Strip Plot)")
.encode(
vl.x().fieldN("race").title("Race").axis({ labelAngle: -30, labelFontSize: 14 }),
vl.y().fieldQ("count").title("Number of Deaths").axis({ grid: true }),
vl.color().fieldN("intent").scale({ scheme: "category10" }),
vl.xOffset().fieldN("intent"), // Spread Suicide & Homicide slightly apart
vl.tooltip(["race", "intent", "count"])
)
.width(900)
.height(500),

// Add Labels with Offset to Prevent Overlap
vl.markText({ fontSize: 14, opacity: 0.9, dy: -10 }) // Shift labels slightly upward
.data(formattedRaceData)
.encode(
vl.x().fieldN("race"),
vl.y().fieldQ("count"),
vl.text().fieldQ("count"),
vl.color().fieldN("intent"),
vl.xOffset().fieldN("intent"), // Move Suicide & Homicide apart
vl.yOffset().fieldN("intent").scale({ domain: ["Suicide", "Homicide"], range: [-10, 10] }) // Push text up/down
)
).render();


Insert cell
rawEducationData = suicideData
.filter(d => d.intent === "Suicide" || d.intent === "Homicide")
.reduce((acc, entry) => {
var edu = entry["education"] || "Unknown"; // Handle missing values
if (!acc[edu]) acc[edu] = { Suicide: 0, Homicide: 0 };

acc[edu][entry.intent] += 1;
return acc;
}, {});
Insert cell
formattedEducationData = Object.entries(rawEducationData)
.flatMap(([education, counts]) => [
{ education, intent: "Suicide", count: counts.Suicide },
{ education, intent: "Homicide", count: counts.Homicide }
]);

Insert cell
pointChart = vl.markCircle({ size: 80 }) // Circles for each data point
.data(formattedEducationData)
.encode(
vl.x().fieldN("education"),
vl.y().fieldQ("count"),
vl.color().fieldN("intent"),
vl.tooltip(["education", "intent", "count"])
);
Insert cell
lineChart = vl.markLine({ tooltip: true, interpolate: "monotone", strokeWidth: 3 })
.data(formattedEducationData)
.title("Gun Deaths by Education Level (Improved Parallel Coordinates)")
.encode(
vl.x().fieldN("education").title("Education Level").axis({ labelAngle: -30 }), // Rotate labels
vl.y().fieldQ("count").title("Number of Deaths").axis({ grid: true }), // Add gridlines
vl.color().fieldN("intent").scale({ scheme: "tableau10" }), // Better colors
vl.tooltip(["education", "intent", "count"])
)
.width(900)
.height(500);
Insert cell
EducationChartLayered = vl.layer(lineChart, pointChart).render();
Insert cell
vl.spec({
data: { values: suicideData },
params: [
{
name: "intentFilter",
bind: {
input: "select",
options: ["All", "Suicide", "Homicide"],
name: "Intent: "
},
value: "All"
},
{
name: "genderFilter",
bind: {
input: "select",
options: ["All", "M", "F"],
name: "Gender: "
},
value: "All"
},
{
name: "yearFilter",
bind: {
input: "select",
// Adjust the years below as needed (or generate dynamically if possible)
options: ["All", "2012", "2013", "2014"],
name: "Year: "
},
value: "All"
}
],
transform: [
{ filter: "datum.intent === 'Suicide' || datum.intent === 'Homicide'" },
{ filter: "intentFilter === 'All' || datum.intent === intentFilter" },
{ filter: "genderFilter === 'All' || datum.sex === genderFilter" },
{ filter: "yearFilter === 'All' || datum.year === yearFilter" },
{ calculate: "toDate(datum.year + '-' + datum.month + '-01')", as: "date" }
],
mark: "line",
encoding: {
x: {
field: "date",
type: "temporal",
timeUnit: "month",
title: "Month"
},
y: {
aggregate: "count",
type: "quantitative",
title: "Number of Deaths"
},
color: {
field: "intent",
type: "nominal",
title: "Intent"
},
tooltip: [
{ field: "year", type: "ordinal", title: "Year" },
{ field: "month", type: "ordinal", title: "Month" },
{ field: "intent", type: "nominal", title: "Intent" },
{ aggregate: "count", type: "quantitative", title: "Deaths" }
]
},
width: 900,
height: 500,
title: "Monthly Gun Deaths by Intent (Filtered by Year, Gender, & Intent)"
}).render();

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