Public
Edited
Jul 28
2 forks
Insert cell
Insert cell
Insert cell
data = FileAttachment("CTDC_global_synthetic_data_v2025_cleaned2.csv").csv()
Insert cell
genderOptions = [
"All Genders",
...Array.from(new Set(data.map((d) => d.gender))).sort()
]
Insert cell
viewof selectedGender = {
const wrapper = html`<details style="width: 285px; font: 14px sans-serif; margin-bottom: 12px;">
<summary style="cursor: pointer; padding: 8px 12px; border: 1px solid #333; border-radius: 8px; background: #fafafa;">
Gender
</summary>
<div style="display: flex; flex-direction: column; gap: 6px; padding: 10px 14px; border: 1px solid #333; border-radius: 8px; background: #fff; margin-top: 8px;"></div>
</details>`;

const container = wrapper.querySelector("div");
const summary = wrapper.querySelector("summary");

// Add checkbox inputs
const checkboxes = genderOptions.map((gender) => {
const label = html`<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" value="${gender}">
<span>${gender}</span>
</label>`;
container.appendChild(label);
return label.querySelector("input");
});

// Default select "All Genders"
checkboxes.forEach((cb) => {
if (cb.value === "All Genders") cb.checked = true;
});

function updateLogic() {
const selected = checkboxes
.filter((cb) => cb.checked)
.map((cb) => cb.value);
const allBox = checkboxes.find((cb) => cb.value === "All Genders");

// Auto-uncheck "All Genders" if others selected
if (selected.includes("All Genders") && selected.length > 1) {
allBox.checked = false;
}

// Auto-check "All Genders" if none selected
if (selected.length === 0) {
allBox.checked = true;
}

// Update summary label
const finalSelection = checkboxes
.filter((cb) => cb.checked)
.map((cb) => cb.value);
summary.textContent =
finalSelection.length === 1
? `Gender: ${finalSelection[0]}`
: `Gender: ${finalSelection.slice(0, 3).join(", ")}${
finalSelection.length > 3 ? ", ..." : ""
}`;

wrapper.dispatchEvent(new CustomEvent("input"));
}

container.addEventListener("change", updateLogic);

Object.defineProperty(wrapper, "value", {
get: () => checkboxes.filter((cb) => cb.checked).map((cb) => cb.value)
});

return wrapper;
}
Insert cell
sub_gender = selectedGender.includes("All Genders")
? data
: data.filter((d) => selectedGender.includes(d.gender))
Insert cell
AgeGroupOptions = [
"All Ages",
...Array.from(new Set(sub_gender.map((d) => d.ageBroad))).sort()
]
Insert cell
viewof selectedAgeGroups = {
const wrapper = html`<details style="width: 285px; font: 14px sans-serif; margin-bottom: 12px;">
<summary style="cursor: pointer; padding: 8px 12px; border: 1px solid #333; border-radius: 8px; background: #fafafa;">
Age Group
</summary>
<div style="display: flex; flex-direction: column; gap: 6px; padding: 10px 14px; border: 1px solid #333; border-radius: 8px; background: #fff; margin-top: 8px;"></div>
</details>`;

const container = wrapper.querySelector("div");
const summary = wrapper.querySelector("summary");

// Add checkboxes for each age group
const checkboxes = AgeGroupOptions.map((age) => {
const label = html`<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" value="${age}">
<span>${age}</span>
</label>`;
container.appendChild(label);
return label.querySelector("input");
});

// Default to "All Ages"
checkboxes.forEach((cb) => {
if (cb.value === "All Ages") cb.checked = true;
});

function updateLogic() {
const selected = checkboxes
.filter((cb) => cb.checked)
.map((cb) => cb.value);
const allBox = checkboxes.find((cb) => cb.value === "All Ages");

// Deselect "All Ages" if others selected
if (selected.includes("All Ages") && selected.length > 1) {
allBox.checked = false;
}

// Reselect "All Ages" if none selected
if (selected.length === 0) {
allBox.checked = true;
}

// Refresh selected list
const current = checkboxes.filter((cb) => cb.checked).map((cb) => cb.value);
summary.textContent =
current.length === 1
? `Age Group: ${current[0]}`
: `Age Group: ${current.slice(0, 3).join(", ")}${
current.length > 3 ? ", ..." : ""
}`;

wrapper.dispatchEvent(new CustomEvent("input"));
}

container.addEventListener("change", updateLogic);

Object.defineProperty(wrapper, "value", {
get: () => checkboxes.filter((cb) => cb.checked).map((cb) => cb.value)
});

return wrapper;
}
Insert cell
sub_gender_age = sub_gender.filter(
(d) =>
selectedAgeGroups.includes("All Ages") ||
selectedAgeGroups.includes(d.ageBroad)
)
Insert cell
CitizenshipOptions = [
"All Countries",
...Array.from(new Set(sub_gender_age.map((d) => d.citizenship))).sort()
]
Insert cell
viewof selectedcitizenship = {
const wrapper = html`<details style="width: 285px; font: 14px sans-serif; margin-bottom: 12px;">
<summary style="cursor: pointer; padding: 8px 12px; border: 1px solid #333; border-radius: 8px; background: #fafafa;">
Origin Country
</summary>
<div style="display: flex; flex-direction: column; gap: 6px; padding: 10px 14px; border: 1px solid #333; border-radius: 8px; background: #fff; margin-top: 8px;"></div>
</details>`;

const container = wrapper.querySelector("div");
const summary = wrapper.querySelector("summary");

// Add checkboxes for each country option
const checkboxes = CitizenshipOptions.map((country) => {
const label = html`<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" value="${country}">
<span>${country}</span>
</label>`;
container.appendChild(label);
return label.querySelector("input");
});

// Default select "All Countries"
checkboxes.forEach((cb) => {
if (cb.value === "All Countries") cb.checked = true;
});

function updateLogic() {
const selected = checkboxes
.filter((cb) => cb.checked)
.map((cb) => cb.value);
const allBox = checkboxes.find((cb) => cb.value === "All Countries");

// Deselect "All" if others selected
if (selected.includes("All Countries") && selected.length > 1) {
allBox.checked = false;
}

// Reselect "All" if none selected
if (selected.length === 0) {
allBox.checked = true;
}

// Update summary label
const current = checkboxes.filter((cb) => cb.checked).map((cb) => cb.value);
summary.textContent =
current.length === 1
? `Origin Country: ${current[0]}`
: `Origin Country: ${current.slice(0, 3).join(", ")}${
current.length > 3 ? ", ..." : ""
}`;

wrapper.dispatchEvent(new CustomEvent("input"));
}

container.addEventListener("change", updateLogic);

Object.defineProperty(wrapper, "value", {
get: () => checkboxes.filter((cb) => cb.checked).map((cb) => cb.value)
});

return wrapper;
}
Insert cell
sub_gender_age_citizenship = sub_gender_age.filter(
(d) =>
selectedcitizenship.includes("All Countries") ||
selectedcitizenship.includes(d.citizenship)
)
Insert cell
ExploitationOptions = [
"All Countries",
...Array.from(
new Set(sub_gender_age_citizenship.map((d) => d.CountryOfExploitation))
).sort()
]
Insert cell
viewof selectedexploitation = {
const wrapper = html`<details style="width: 285px; font: 14px sans-serif; margin-bottom: 12px;">
<summary style="cursor: pointer; padding: 8px 12px; border: 1px solid #333; border-radius: 8px; background: #fafafa;">
Destination Country
</summary>
<div style="display: flex; flex-direction: column; gap: 6px; padding: 10px 14px; border: 1px solid #333; border-radius: 8px; background: #fff; margin-top: 8px;"></div>
</details>`;

const container = wrapper.querySelector("div");
const summary = wrapper.querySelector("summary");

const checkboxes = ExploitationOptions.map((country) => {
const label = html`<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" value="${country}">
<span>${country}</span>
</label>`;
container.appendChild(label);
return label.querySelector("input");
});

// Default select "All Countries"
checkboxes.forEach((cb) => {
if (cb.value === "All Countries") cb.checked = true;
});

function updateLogic() {
const selected = checkboxes.filter((cb) => cb.checked).map((cb) => cb.value);
const allBox = checkboxes.find((cb) => cb.value === "All Countries");

if (selected.includes("All Countries") && selected.length > 1) {
allBox.checked = false;
}

if (selected.length === 0) {
allBox.checked = true;
}

const current = checkboxes.filter((cb) => cb.checked).map((cb) => cb.value);
summary.textContent =
current.length === 1
? `Destination Country: ${current[0]}`
: `Destination Country: ${current.slice(0, 3).join(", ")}${current.length > 3 ? ", ..." : ""}`;

wrapper.dispatchEvent(new CustomEvent("input"));
}

container.addEventListener("change", updateLogic);

Object.defineProperty(wrapper, "value", {
get: () => checkboxes.filter((cb) => cb.checked).map((cb) => cb.value)
});

return wrapper;
}

Insert cell
sub_gender_age_citizenship_exploitation = sub_gender_age_citizenship.filter(
(d) =>
selectedexploitation.includes("All Countries") ||
selectedexploitation.includes(d.CountryOfExploitation)
)
Insert cell
TrafficMonthOptions = [
"All Durations",
...Array.from(
new Set(
sub_gender_age_citizenship_exploitation.map((d) => d.traffickMonths)
)
).sort()
]
Insert cell
viewof selectedTraffickMonths = {
const wrapper = html`<details style="width: 285px; font: 14px sans-serif; margin-bottom: 12px;">
<summary style="cursor: pointer; padding: 8px 12px; border: 1px solid #333; border-radius: 8px; background: #fafafa;">
Trafficking Duration
</summary>
<div style="display: flex; flex-direction: column; gap: 6px; padding: 10px 14px; border: 1px solid #333; border-radius: 8px; background: #fff; margin-top: 8px;"></div>
</details>`;

const container = wrapper.querySelector("div");
const summary = wrapper.querySelector("summary");

const checkboxes = TrafficMonthOptions.map((month) => {
const label = html`<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" value="${month}">
<span>${month}</span>
</label>`;
container.appendChild(label);
return label.querySelector("input");
});

// Default select "All Durations"
checkboxes.forEach((cb) => {
if (cb.value === "All Durations") cb.checked = true;
});

function updateLogic() {
const selected = checkboxes
.filter((cb) => cb.checked)
.map((cb) => cb.value);
const allBox = checkboxes.find((cb) => cb.value === "All Durations");

if (selected.includes("All Durations") && selected.length > 1) {
allBox.checked = false;
}

if (selected.length === 0) {
allBox.checked = true;
}

const current = checkboxes.filter((cb) => cb.checked).map((cb) => cb.value);
summary.textContent =
current.length === 1
? `Trafficking Duration: ${current[0]}`
: `Trafficking Duration: ${current.slice(0, 3).join(", ")}${
current.length > 3 ? ", ..." : ""
}`;

wrapper.dispatchEvent(new CustomEvent("input"));
}

container.addEventListener("change", updateLogic);

Object.defineProperty(wrapper, "value", {
get: () => checkboxes.filter((cb) => cb.checked).map((cb) => cb.value)
});

return wrapper;
}
Insert cell
sub_gender_age_citizenship_exploitation_duration = sub_gender_age_citizenship_exploitation.filter(
(d) =>
selectedTraffickMonths.includes("All Durations") ||
selectedTraffickMonths.includes(d.traffickMonths)
)

Insert cell
viewof selectedMeansControls = {
const options = [
{ label: "All Means", key: "all" },
{ label: "Debt-Bondage / Earnings", key: "meansDebtBondageEarnings" },
{ label: "Threats", key: "meansThreats" },
{ label: "Psych./Phys./Sex. Abuse", key: "meansAbusePsyPhySex" },
{ label: "False Promises", key: "meansFalsePromises" },
{ label: "Drugs / Alcohol", key: "meansDrugsAlcohol" },
{ label: "Deny Basic Needs", key: "meansDenyBasicNeeds" },
{ label: "Excessive Work Hours", key: "meansExcessiveWorkHours" },
{ label: "Withhold Documents", key: "meansWithholdDocs" }
];

const wrapper = html`<details style="width: 285px; font: 14px sans-serif; margin-bottom: 12px;">
<summary style="cursor: pointer; padding: 8px 12px; border: 1px solid #333; border-radius: 8px; background: #fafafa;">
All Means of Control
</summary>
<div style="display: flex; flex-direction: column; gap: 6px; padding: 10px 14px; border: 1px solid #333; border-radius: 8px; background: #fff; margin-top: 8px;"></div>
</details>`;

const container = wrapper.querySelector("div");

const checkboxes = options.map((opt) => {
const node = html`<label style="display:flex;align-items:center;gap:8px;">
<input type="checkbox" value="${opt.key}">
<span>${opt.label}</span>
</label>`;
container.appendChild(node);
return node.querySelector("input");
});

// Check "all" by default
checkboxes.find((cb) => cb.value === "all").checked = true;

function updateSummary() {
const selectedLabels = options
.filter((opt) =>
checkboxes.find((cb) => cb.value === opt.key && cb.checked)
)
.map((opt) => opt.label);

wrapper.querySelector("summary").textContent =
selectedLabels.length === 0 || selectedLabels.includes("All Means")
? "All Means of Control"
: `Means: ${selectedLabels.join(", ")}`;
}

container.addEventListener("change", () => {
const allBox = checkboxes.find((cb) => cb.value === "all");
const otherBoxes = checkboxes.filter((cb) => cb.value !== "all");

// Auto-uncheck "All Means" if other options selected
if (allBox.checked && otherBoxes.some((cb) => cb.checked)) {
allBox.checked = false;
}

// Auto-check "All Means" if nothing else is selected
if (!allBox.checked && otherBoxes.every((cb) => !cb.checked)) {
allBox.checked = true;
}

updateSummary();
wrapper.dispatchEvent(new CustomEvent("input"));
});

Object.defineProperty(wrapper, "value", {
get: () => checkboxes.filter((cb) => cb.checked).map((cb) => cb.value)
});

updateSummary();
return wrapper;
}
Insert cell
selectedMeansControls
Insert cell
sub_gender_age_citizenship_exploitation_duration_meanscontrol = sub_gender_age_citizenship_exploitation_duration.filter(
(d) =>
selectedMeansControls.includes("all") ||
selectedMeansControls.some((key) => d[key] == 1)
)
Insert cell
viewof selectedExploitationTypes = {
const options = [
{ label: "All Types", key: "all" },
{ label: "Forced Labour", key: "isForcedLabour" },
{ label: "Sexual Exploitation", key: "isSexualExploit" },
{ label: "Other Exploitation", key: "isOtherExploit" }
];

const wrapper = html`<details style="width:285px;font:14px sans-serif;margin-bottom:12px;">
<summary style="cursor:pointer;padding:8px 12px;border:1px solid #333;border-radius:8px;background:#fafafa;">
All Types
</summary>
<div style="display:flex;flex-direction:column;gap:6px;padding:10px 14px;border:1px solid #333;border-radius:8px;background:#fff;margin-top:8px;"></div>
</details>`;

const summary = wrapper.querySelector("summary");
const container = wrapper.querySelector("div");

const checkboxes = options.map((opt) => {
const node = html`<label style="display:flex;align-items:center;gap:8px;">
<input type="checkbox" value="${opt.key}">
<span>${opt.label}</span>
</label>`;
container.appendChild(node);
return node.querySelector("input");
});

const updateSummaryText = () => {
const selected = checkboxes
.filter((cb) => cb.checked)
.map((cb) => cb.value);
const selectedLabels = options
.filter((opt) => selected.includes(opt.key) && opt.key !== "all")
.map((opt) => opt.label);

summary.textContent =
selected.includes("all") || selectedLabels.length === 0
? "All Types"
: selectedLabels.join(", ");
};

// Default select "all"
checkboxes.find((cb) => cb.value === "all").checked = true;
updateSummaryText();

checkboxes.forEach((cb) =>
cb.addEventListener("change", () => {
const allBox = checkboxes.find((c) => c.value === "all");

if (cb.value === "all" && cb.checked) {
checkboxes.forEach((c) => {
if (c.value !== "all") c.checked = false;
});
} else if (cb.value !== "all" && cb.checked) {
allBox.checked = false;
} else {
const anyChecked = checkboxes.some(
(c) => c.value !== "all" && c.checked
);
if (!anyChecked) allBox.checked = true;
}

updateSummaryText();
wrapper.dispatchEvent(new CustomEvent("input"));
})
);

Object.defineProperty(wrapper, "value", {
get: () => checkboxes.filter((cb) => cb.checked).map((cb) => cb.value)
});

return wrapper;
}
Insert cell
selectedExploitationTypes
Insert cell
sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype = sub_gender_age_citizenship_exploitation_duration_meanscontrol.filter(
(d) =>
selectedExploitationTypes.includes("all") ||
selectedExploitationTypes.some((key) => d[key] == 1)
)
Insert cell
viewof selectedLaborSectors = {
const options = [
{ label: "All Sectors", key: "all" },
{ label: "Agriculture", key: "typeOfLabourAgriculture" },
{ label: "Construction", key: "typeOfLabourConstruction" },
{ label: "Domestic Work", key: "typeOfLabourDomesticWork" },
{ label: "Hospitality", key: "typeOfLabourHospitality" }
];

const wrapper = html`<details style="width:285px;font:14px sans-serif;margin-bottom:12px;">
<summary style="cursor:pointer;padding:8px 12px;border:1px solid #333;border-radius:8px;background:#fafafa;">
All Sectors
</summary>
<div style="display:flex;flex-direction:column;gap:6px;padding:10px 14px;border:1px solid #333;border-radius:8px;background:#fff;margin-top:8px;"></div>
</details>`;

const summary = wrapper.querySelector("summary");
const container = wrapper.querySelector("div");

const checkboxes = options.map((opt) => {
const node = html`<label style="display:flex;align-items:center;gap:8px;">
<input type="checkbox" value="${opt.key}">
<span>${opt.label}</span>
</label>`;
container.appendChild(node);
return node.querySelector("input");
});

const updateSummaryText = () => {
const selected = checkboxes
.filter((cb) => cb.checked)
.map((cb) => cb.value);
const selectedLabels = options
.filter((opt) => selected.includes(opt.key) && opt.key !== "all")
.map((opt) => opt.label);

summary.textContent =
selected.includes("all") || selectedLabels.length === 0
? "All Sectors"
: selectedLabels.join(", ");
};

// Default to "All Sectors"
checkboxes.find((cb) => cb.value === "all").checked = true;
updateSummaryText();

checkboxes.forEach((cb) =>
cb.addEventListener("change", () => {
const allBox = checkboxes.find((c) => c.value === "all");

if (cb.value === "all" && cb.checked) {
checkboxes.forEach((c) => {
if (c.value !== "all") c.checked = false;
});
} else if (cb.value !== "all" && cb.checked) {
allBox.checked = false;
} else {
const anyChecked = checkboxes.some(
(c) => c.value !== "all" && c.checked
);
if (!anyChecked) allBox.checked = true;
}

updateSummaryText();
wrapper.dispatchEvent(new CustomEvent("input"));
})
);

Object.defineProperty(wrapper, "value", {
get: () => checkboxes.filter((cb) => cb.checked).map((cb) => cb.value)
});

return wrapper;
}
Insert cell
selectedLaborSectors
Insert cell
sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor = sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype.filter(
(d) =>
selectedLaborSectors.includes("all") ||
selectedLaborSectors.some((key) => d[key] == 1)
)
Insert cell
viewof selectedSexualExploitTypes = {
const options = [
{ label: "All Sexual Exploitation Types", key: "all" },
{ label: "Prostitution", key: "typeOfSexProstitution" },
{ label: "Pornography", key: "typeOfSexPornography" }
];

const wrapper = html`<details style="width:285px;font:14px sans-serif;margin-bottom:12px;">
<summary style="cursor:pointer;padding:8px 12px;border:1px solid #333;border-radius:8px;background:#fafafa;">
All Sexual Exploitation Types
</summary>
<div style="display:flex;flex-direction:column;gap:6px;padding:10px 14px;border:1px solid #333;border-radius:8px;background:#fff;margin-top:8px;"></div>
</details>`;

const summary = wrapper.querySelector("summary");
const container = wrapper.querySelector("div");

const checkboxes = options.map((opt) => {
const node = html`<label style="display:flex;align-items:center;gap:8px;">
<input type="checkbox" value="${opt.key}">
<span>${opt.label}</span>
</label>`;
container.appendChild(node);
return node.querySelector("input");
});

const updateSummaryText = () => {
const selected = checkboxes
.filter((cb) => cb.checked)
.map((cb) => cb.value);
const selectedLabels = options
.filter((opt) => selected.includes(opt.key) && opt.key !== "all")
.map((opt) => opt.label);

summary.textContent =
selected.includes("all") || selectedLabels.length === 0
? "All Sexual Exploitation Types"
: selectedLabels.join(", ");
};

// Default to "All"
checkboxes.find((cb) => cb.value === "all").checked = true;
updateSummaryText();

checkboxes.forEach((cb) =>
cb.addEventListener("change", () => {
const allBox = checkboxes.find((c) => c.value === "all");

if (cb.value === "all" && cb.checked) {
checkboxes.forEach((c) => {
if (c.value !== "all") c.checked = false;
});
} else if (cb.value !== "all" && cb.checked) {
allBox.checked = false;
} else {
const anyChecked = checkboxes.some(
(c) => c.value !== "all" && c.checked
);
if (!anyChecked) allBox.checked = true;
}

updateSummaryText();
wrapper.dispatchEvent(new CustomEvent("input"));
})
);

Object.defineProperty(wrapper, "value", {
get: () => checkboxes.filter((cb) => cb.checked).map((cb) => cb.value)
});

return wrapper;
}
Insert cell
selectedSexualExploitTypes
Insert cell
sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor_sexual =
sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor.filter(
(d) =>
selectedSexualExploitTypes.includes("all") ||
selectedSexualExploitTypes.some((key) => d[key] == 1)
);
Insert cell
viewof selectedRecruiterRelations = {
const options = [
{ label: "All Relationships", key: "all" },
{ label: "Intimate Partner", key: "recruiterRelationIntimatePartner" },
{ label: "Friend", key: "recruiterRelationFriend" },
{ label: "Family", key: "recruiterRelationFamily" },
{ label: "Other", key: "recruiterRelationOther" }
];

const wrapper = html`<details style="width:285px;font:14px sans-serif;margin-bottom:12px;">
<summary style="cursor:pointer;padding:8px 12px;border:1px solid #333;border-radius:8px;background:#fafafa;">
All Relationships
</summary>
<div style="display:flex;flex-direction:column;gap:6px;padding:10px 14px;border:1px solid #333;border-radius:8px;background:#fff;margin-top:8px;"></div>
</details>`;

const summary = wrapper.querySelector("summary");
const container = wrapper.querySelector("div");

const checkboxes = options.map((opt) => {
const node = html`<label style="display:flex;align-items:center;gap:8px;">
<input type="checkbox" value="${opt.key}">
<span>${opt.label}</span>
</label>`;
container.appendChild(node);
return node.querySelector("input");
});

const updateSummaryText = () => {
const selected = checkboxes
.filter((cb) => cb.checked)
.map((cb) => cb.value);
const selectedLabels = options
.filter((opt) => selected.includes(opt.key) && opt.key !== "all")
.map((opt) => opt.label);

summary.textContent =
selected.includes("all") || selectedLabels.length === 0
? "All Relationships"
: selectedLabels.join(", ");
};

checkboxes.find((cb) => cb.value === "all").checked = true;
updateSummaryText();

checkboxes.forEach((cb) =>
cb.addEventListener("change", () => {
const allBox = checkboxes.find((c) => c.value === "all");

if (cb.value === "all" && cb.checked) {
checkboxes.forEach((c) => {
if (c.value !== "all") c.checked = false;
});
} else if (cb.value !== "all" && cb.checked) {
allBox.checked = false;
} else {
const anyChecked = checkboxes.some(
(c) => c.value !== "all" && c.checked
);
if (!anyChecked) allBox.checked = true;
}

updateSummaryText();
wrapper.dispatchEvent(new CustomEvent("input"));
})
);

Object.defineProperty(wrapper, "value", {
get: () => checkboxes.filter((cb) => cb.checked).map((cb) => cb.value)
});

return wrapper;
}
Insert cell
selectedRecruiterRelations
Insert cell
sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor_sexual_recruiter = sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor_sexual.filter(
(d) =>
selectedRecruiterRelations.includes("all") ||
selectedRecruiterRelations.some((key) => d[key] == 1)
)
Insert cell
YearOptions = [
"All Years",
...Array.from(
new Set(
sub_gender_age_citizenship_exploitation_duration.map((d) => d.yearOfRegistration)
)
)
.filter((d) => d !== "Unknown") // Remove 'unknown' first
.sort((a, b) => +a - +b) // Sort numeric years
.concat("Unknown") // Then append 'unknown' at the end
]
Insert cell
viewof selectedYears = {
const wrapper = html`<details style="width: 285px; font: 14px sans-serif; margin-bottom: 12px;">
<summary style="cursor: pointer; padding: 8px 12px; border: 1px solid #333; border-radius: 8px; background: #fafafa;">
Year of Registration
</summary>
<div style="display: flex; flex-direction: column; gap: 6px; padding: 10px 14px; border: 1px solid #333; border-radius: 8px; background: #fff; margin-top: 8px;"></div>
</details>`;

const container = wrapper.querySelector("div");
const summary = wrapper.querySelector("summary");

// Add checkbox inputs
const checkboxes = YearOptions.map((year) => {
const label = html`<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" value="${year}">
<span>${year}</span>
</label>`;
container.appendChild(label);
return label.querySelector("input");
});

// Default select "All Years"
checkboxes.forEach((cb) => {
if (cb.value === "All Years") cb.checked = true;
});

function updateLogic() {
const selected = checkboxes
.filter((cb) => cb.checked)
.map((cb) => cb.value);
const allBox = checkboxes.find((cb) => cb.value === "All Years");

// Auto-uncheck "All Years" if others selected
if (selected.includes("All Years") && selected.length > 1) {
allBox.checked = false;
}

// Auto-check "All Years" if none selected
if (selected.length === 0) {
allBox.checked = true;
}

// Update summary label
const finalSelection = checkboxes
.filter((cb) => cb.checked)
.map((cb) => cb.value);
summary.textContent =
finalSelection.length === 1
? `Year: ${finalSelection[0]}`
: `Years: ${finalSelection.slice(0, 3).join(", ")}${
finalSelection.length > 3 ? ", ..." : ""
}`;

wrapper.dispatchEvent(new CustomEvent("input"));
}

container.addEventListener("change", updateLogic);

Object.defineProperty(wrapper, "value", {
get: () => checkboxes.filter((cb) => cb.checked).map((cb) => cb.value)
});

return wrapper;
}
Insert cell
sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor_sexual_recruiter_year = sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor_sexual_recruiter.filter(
(d) =>
selectedYears.includes("All Years") ||
selectedYears.includes(d.yearOfRegistration)
)
Insert cell
// Aggregate total victims by citizenship
victimsByCitizenship = Array.from(
d3.rollup(
sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor_sexual_recruiter_year,
(v) => v.length, // count of records per group
(d) => d.citizenship
),
([citizenship, count]) => ({ citizenship, count })
)
Insert cell
viewof totalVictimsBox = {
const box = html`<div style="
background-color: #f5f5f5;
border: 1px solid #333;
border-radius: 8px;
padding: 12px 16px;
font: 14px sans-serif;
font-weight: bold;
color: #333;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
display: inline-block;
">Total Victims: ${sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor_sexual_recruiter_year.length}</div>`;

return box;
}
Insert cell
barChart = Plot.plot({
y: { label: "Number of Victims", grid: true },
x: { label: "Citizenship", tickRotate: -45 },
marks: [
Plot.barY(victimsByCitizenship, {
x: "citizenship",
y: "count",
fill: "steelblue"
})
],
height: 400,
width: 700,
marginLeft: 80,
marginBottom: 100
})
Insert cell
dashboard = html`<div style="
display: flex;
max-width: 100%;
box-sizing: border-box;
overflow: hidden;
font-family: sans-serif;
">

<!-- Sidebar -->
<div style="
flex: 0 0 318px;
padding-right: 10px;
display: flex;
flex-direction: column;
gap: 16px;
box-sizing: border-box;
">

<!-- Demographics Filter Box -->
<div style="background-color: #e0e0e0; border: 1px solid #333; border-radius: 8px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<h3 style="margin-top: 0; margin-bottom: 12px; font: 16px sans-serif; font-weight: bold; color: #333;">Demographics</h3>
<p style="margin: 0 0 6px 0; font-size: 13px; color: #555;">Gender of Victims</p>
${viewof selectedGender}
<p style="margin: 12px 0 6px 0; font-size: 13px; color: #555;">Age Ranges of Victims</p>
${viewof selectedAgeGroups}
<p style="margin: 12px 0 6px 0; font-size: 13px; color: #555;">Origin Country of Victims</p>
${viewof selectedcitizenship}
<p style="margin: 12px 0 6px 0; font-size: 13px; color: #555;">Destination Country of Victims</p>
${viewof selectedexploitation}
<p style="margin: 12px 0 6px 0; font-size: 13px; color: #555;">Year of Registration</p>
${viewof selectedYears}
</div>

<!-- Trafficking Conditions -->
<div style="background-color: #e0e0e0; border: 1px solid #333; border-radius: 8px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<h3 style="margin: 0; font: 16px sans-serif; font-weight: bold; color: #333;">Trafficking Conditions</h3>
<p style="margin: 12px 0 6px 0; font-size: 13px; color: #555;">Trafficking Duration in Months</p>
${viewof selectedTraffickMonths}
<p style="margin: 12px 0 6px 0; font-size: 13px; color: #555;">Means of Controlling Victims</p>
${viewof selectedMeansControls}
</div>

<!-- Exploitation Type -->
<div style="background-color: #e0e0e0; border: 1px solid #333; border-radius: 8px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<h3 style="margin: 0; font: 16px sans-serif; font-weight: bold; color: #333;">Exploitation Type</h3>
<p style="margin-top: 12px; font-size: 13px; color: #555;"></p>
${viewof selectedExploitationTypes}
<p style="margin: 12px 0 6px 0; font-size: 13px; color: #555;">Labor Sector</p>
${viewof selectedLaborSectors}
<p style="margin: 12px 0 6px 0; font-size: 13px; color: #555;">Sexual Expoitation Type</p>
${viewof selectedSexualExploitTypes}
</div>

<!-- Recruiter Relationship -->
<div style="background-color: #e0e0e0; border: 1px solid #333; border-radius: 8px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<h3 style="margin: 0; font: 16px sans-serif; font-weight: bold; color: #333;">Recruiter Relationship</h3>
<p style="margin-top: 12px; font-size: 13px; color: #555;">Victims' Relationship Type With Recruiter</p>
${viewof selectedRecruiterRelations}
</div>
</div>

<!-- Main content -->
<div style="
flex: 1;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
box-sizing: border-box;
min-width: 0;
">

<!-- Section Title -->
<h3 style="margin: 0 0 6px; font-size: 16px; font-weight: bold; color: #333;">
Victim Demographics Overview
</h3>
<p style="margin: 0 0 16px; font-size: 12px; line-height: 1.4; color: #555;">
The following charts provide key demographic information about trafficking victims, offering important context to better understand the flow patterns.
</p>

<!-- Three Column Chart Grid -->
<div style="
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 16px;
">
<!-- Chart 1 -->
<div style="flex: 1 1 0; min-width: 240px; max-width: 33%;">
<h4 style="font-size: 13px; margin: 0 0 4px; color: #333;">Origin vs Destination</h4>
<p style="font-size: 11px; margin: 0 0 8px; color: #555;">Trafficking flows into and out of countries. The U.S. and Ukraine were top destinations (2002–2023).</p>
<div style="height: auto; overflow-x: auto;">
${viewof traffickInOutBarChart}
</div>
</div>

<!-- Chart 2 -->
<div style="flex: 1 1 0; min-width: 240px; max-width: 33%;">
<h4 style="font-size: 13px; margin: 0 0 4px; color: #333;">Gender Distribution</h4>
<p style="font-size: 11px; margin: 0 0 8px; color: #555;">Nearly half are women. Unknown gender cases highlight data gaps.</p>
<div style="height: auto; overflow-x: auto;">
${viewof genderBarChart}
</div>
</div>

<!-- Chart 3 -->
<div style="flex: 1 1 0; min-width: 240px; max-width: 33%;">
<h4 style="font-size: 13px; margin: 0 0 4px; color: #333;">Age Distribution</h4>
<p style="font-size: 11px; margin: 0 0 8px; color: #555;">Most are adults, but children and adolescents are also affected.</p>
<div style="height: auto; overflow-x: auto;">
${viewof ageGroupBarChart}
</div>
</div>
</div>

<!-- Map Description Block -->
<div style="
width: 100%;
margin: 30px 0 20px;
font-family: sans-serif;
color: #444;
background: #fff;
padding: 20px 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border-radius: 10px;
box-sizing: border-box;
">
<h2 style="font-size: 20px; margin-top: 0; margin-bottom: 10px;">Explore the Global Flow Map</h2>
<p style="font-size: 14px; line-height: 1.6; margin: 0;">
This interactive map shows trafficking routes between countries of origin and exploitation. Use the filters on the left to narrow by demographics or trafficking conditions. Hover on the lines to view origin, destination, and victim counts. Pan or zoom the map to explore more.
</p>
</div>

<!-- Summary Boxes -->
<div style="display: flex; flex-wrap: wrap; gap: 12px; margin: 24px 0;">
${viewof totalVictimsBox}
${legendBox}
</div>

<!-- Map -->
<div style="width: 100%;">
${viewof map}
</div>
</div>
</div>

<!-- Footnote -->
<div style="max-width: 1000px; margin: 16px auto 40px; font-size: 14px; color: #555; line-height: 1.5; padding: 0 10px;">
<em>Note:</em> Some data has unknown year or gender. This may cause underrepresentation and data quality variation by region.
</div>`
Insert cell
import { Plot } from "@observablehq/plot"
Insert cell
// Summary table for final flow map
summaryTable = Array.from(
d3.rollup(
sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor_sexual_recruiter_year,
(v) => v.length, // count victims
(d) => d.citizenship,
(d) => d.CountryOfExploitation
),
([citizenship, destinations]) => {
return Array.from(destinations, ([exploitationCountry, count]) => ({
citizenship,
exploitationCountry,
count
}));
}
).flat()
Insert cell
Inputs.table(summaryTable)
Insert cell
// add country centorid data
centroids = await FileAttachment("table.csv").csv()
Insert cell
countryCentroids = Object.fromEntries(
centroids.map((d) => [
d.ISO_A3,
{
name: d.NAME_EN,
coords: [+d.LABEL_X, +d.LABEL_Y]
}
])
)
Insert cell
flowData = summaryTable
.map((d) => {
const source = countryCentroids[d.citizenship];
const target = countryCentroids[d.exploitationCountry];

if (!source || !target) return null;

return {
...d,
sourceCoords: source.coords,
targetCoords: target.coords,
sourceName: source.name,
targetName: target.name
};
})
.filter(Boolean)
Insert cell
world = await FileAttachment("countries-110m.json").json()
Insert cell
countries = topojson.feature(world, world.objects.countries)
Insert cell
d3geo = await import("d3-geo@3")
Insert cell
flowGeo = flowData.map((d) => ({
type: "Feature",
geometry: {
type: "LineString",
coordinates: [d.sourceCoords, d.targetCoords]
},
properties: {
count: d.count,
sourceName: d.sourceName,
targetName: d.targetName,
title: `Citizenship: ${d.sourceName}
Exploitation: ${d.targetName}
Count: ${d.count}`
}
}))
Insert cell
domesticGeo = flowData
.filter((d) => d.citizenship === d.exploitationCountry) // ← compare codes
.map((d) => ({
type: "Feature",
geometry: {
type: "Point",
coordinates: d.sourceCoords
},
properties: {
count: d.count,
sourceName: d.sourceName, // e.g. “United States of America”
targetName: d.targetName,
title: `Citizenship: ${d.sourceName}
Exploitation: ${d.targetName}
Count: ${d.count}`
}
}))
Insert cell
import { Inputs } from "@observablehq/inputs"
Insert cell
Insert cell
import { mapboxD3 } from "@john-guerra/mapbox-d3"
Insert cell
import { width } from "@observablehq/stdlib"
Insert cell
viewof map = mapboxD3({
features: flowGeo,
width, // Observable's dynamic width (usually full container width)
height: 600, // Adjust as needed
mapboxOptions: {
center: [60, 4.711],
zoom: 1,
style: "mapbox://styles/mapbox/light-v10",
scrollZoom: true
}
})
Insert cell
colorScale = {
flowGeo; // ensure reactivity

return d3.scaleSequential(d3.interpolateReds).domain([
Math.log10(1),
Math.log10(d3.max(flowGeo, (d) => d.properties.count || 1))
]);
}

Insert cell
styledFlowMap = {
const svg = d3.select(".d3Mapbox");

// Remove any previous tooltips
d3.select("#tooltip").remove();

// Create a tooltip div
const tooltip = d3
.select("body")
.append("div")
.attr("id", "tooltip")
.style("position", "absolute")
.style("pointer-events", "none")
.style("padding", "8px 12px")
.style("background", "rgba(255, 255, 255, 0.95)")
.style("border", "1px solid #aaa")
.style("border-radius", "4px")
.style("font", "13px sans-serif")
.style("display", "none")
.style("z-index", 9999);

// Style flow lines
const paths = svg
.selectAll("path.feature")
.data(
flowGeo,
(d) => `${d.properties.sourceName}-${d.properties.targetName}`
);

paths.join(
(enter) =>
enter
.append("path")
.attr("class", "feature")
.attr("d", map.path)
.style("stroke", (d) => colorScale(Math.log10(d.properties.count || 1)))
.style("stroke-opacity", 0.8)
.style("stroke-width", 1.5)
.style("fill", "none"),
(update) =>
update
.attr("d", map.path)
.style("stroke", (d) =>
colorScale(Math.log10(d.properties.count || 1))
),
(exit) => exit.remove()
);

// Remove old hitboxes
svg.selectAll("path.hitbox").remove();

// Add invisible hitboxes for better hover
svg
.selectAll("path.hitbox")
.data(flowGeo)
.enter()
.append("path")
.attr("class", "hitbox")
.attr("d", map.path)
.style("fill", "none")
.style("stroke", "transparent")
.style("stroke-width", 12)
.style("pointer-events", "stroke")
.on("mouseover", (event, d) => {
tooltip.style("display", "block").html(`
<strong>Citizenship:</strong> ${d.properties.sourceName}<br/>
<strong>Exploitation:</strong> ${d.properties.targetName}<br/>
<strong>Count:</strong> ${d.properties.count}
`);
})
.on("mousemove", (event) => {
tooltip
.style("left", `${event.pageX + 12}px`)
.style("top", `${event.pageY - 28}px`);
})
.on("mouseout", () => {
tooltip.style("display", "none");
});

const project = (coords) =>
map.path.centroid({ type: "Point", coordinates: coords });

const circles = svg
.selectAll("circle.domestic")
.data(domesticGeo, (d) => d.properties.sourceName)
.join(
(enter) =>
enter
.append("circle")
.attr("class", "domestic")
.attr("r", 5)
.style("pointer-events", "all") // <‑‑ ensure events
.attr("fill", (d) => colorScale(Math.log10(d.properties.count || 1)))
.attr("stroke", "#222")
.attr("stroke-width", 0.7)
.attr("cx", (d) => project(d.geometry.coordinates)[0])
.attr("cy", (d) => project(d.geometry.coordinates)[1])
.on("mouseover", (event, d) => {
tooltip.style("display", "block")
.html(`<strong>Citizenship:</strong> ${d.properties.sourceName}<br/>
<strong>Exploitation:</strong> ${d.properties.targetName}<br/>
<strong>Count:</strong> ${d.properties.count}`);
})
.on("mousemove", (event) => {
tooltip
.style("left", `${event.pageX + 12}px`)
.style("top", `${event.pageY - 28}px`);
})
.on("mouseout", () => tooltip.style("display", "none")),
(update) =>
update
.attr("fill", (d) => colorScale(Math.log10(d.properties.count || 1)))
.attr("cx", (d) => project(d.geometry.coordinates)[0])
.attr("cy", (d) => project(d.geometry.coordinates)[1]),
(exit) => exit.remove()
);

// keep circles locked to centroids on pan / zoom
map.map.on("move", () => {
circles
.attr("cx", (d) => project(d.geometry.coordinates)[0])
.attr("cy", (d) => project(d.geometry.coordinates)[1]);
});

return true;
}
Insert cell
legendBox = {
colorScale;

const legendDomain = colorScale.domain();
const minValue = Math.round(Math.pow(10, legendDomain[0]));
const maxValue = Math.round(Math.pow(10, legendDomain[1]));

return html`<div style="
display: flex;
align-items: center;
gap: 10px;
background: white;
border: 1px solid #333;
border-radius: 8px;
padding: 10px 16px;
font: 13px sans-serif;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
width: max-content;
">
<span style="font-weight: bold; font-size: 13px; white-space: nowrap;">Victim Count:</span>

<!-- Gradient bar -->
<div style="display: flex; align-items: center; gap: 6px;">
<span>${minValue}</span>
<div style="
width: 160px;
height: 12px;
background: linear-gradient(to right,
${Array.from({ length: 20 }, (_, i) => {
const t = i / 19;
const val =
legendDomain[0] + t * (legendDomain[1] - legendDomain[0]);
return colorScale(val);
}).join(",")}
);
border: 1px solid #ccc;
"></div>
<span>${maxValue}</span>
</div>
</div>`;
}
Insert cell
Insert cell
viewof traffickInOutBarChart = {
const traffickedInCounts = new Map();
const traffickedOutCounts = new Map();

summaryTable.forEach(({ citizenship, exploitationCountry, count }) => {
traffickedOutCounts.set(
citizenship,
(traffickedOutCounts.get(citizenship) || 0) + count
);
traffickedInCounts.set(
exploitationCountry,
(traffickedInCounts.get(exploitationCountry) || 0) + count
);
});

const countries = Array.from(
new Set([...traffickedInCounts.keys(), ...traffickedOutCounts.keys()])
);

// Sort descending by trafficked-in counts
countries.sort(
(a, b) =>
(traffickedInCounts.get(b) || 0) - (traffickedInCounts.get(a) || 0)
);

const data = countries.map((country) => ({
country,
in: traffickedInCounts.get(country) || 0,
out: traffickedOutCounts.get(country) || 0
}));

// Dimensions — reduce bottom margin, wider width for spacing bars
const margin = { top: 30, right: 20, bottom: 90, left: 50 };
const singleCountryWidth = 30;
const width = Math.max(360, singleCountryWidth * countries.length);
const height = 240 - margin.top - margin.bottom;

const x0 = d3
.scaleBand()
.domain(countries)
.range([0, width])
.paddingInner(0.3);

const x1 = d3
.scaleBand()
.domain(["in", "out"])
.range([0, x0.bandwidth()])
.padding(0.1);

const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => Math.max(d.in, d.out)) * 1.1])
.nice()
.range([height, 0]);

const color = d3
.scaleOrdinal()
.domain(["in", "out"])
.range(["#003F7D", "#eda946"]);

const container = document.createElement("div");
container.style.overflowX = "auto";
container.style.border = "1px solid #ccc";
container.style.padding = "10px";
container.style.maxWidth = "100%";

const svg = d3
.create("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("font-family", "sans-serif");

container.appendChild(svg.node());

const g = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);

// X Axis
g.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x0))
.selectAll("text")
.attr("transform", "rotate(-65)")
.style("text-anchor", "end")
.style("font-size", "11px");

// Y Axis
g.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(y).ticks(null, "s"))
.call((g) => g.select(".domain").remove());

// Y axis label, horizontally rotated left outside the axis ticks
svg
.append("text")
.attr("text-anchor", "middle")
.attr(
"transform",
`translate(${margin.left / 3},${margin.top + height / 2}) rotate(-90)`
)
.style("font-weight", "bold")
.style("font-size", "13px")
.text("Number of Victims");

// Tooltip div
let tooltip = d3.select("body").select(".bar-tooltip");
if (tooltip.empty()) {
tooltip = d3
.select("body")
.append("div")
.attr("class", "bar-tooltip")
.style("position", "absolute")
.style("pointer-events", "none")
.style("padding", "6px 10px")
.style("background", "rgba(255,255,255,0.95)")
.style("border", "1px solid #666")
.style("border-radius", "4px")
.style("font", "13px sans-serif")
.style("color", "#111")
.style("box-shadow", "1px 1px 4px rgba(0,0,0,0.15)")
.style("display", "none")
.style("z-index", "1000");
}

const countryGroups = g
.selectAll("g.country")
.data(data)
.join("g")
.attr("class", "country")
.attr("transform", (d) => `translate(${x0(d.country)},0)`);

countryGroups
.selectAll("rect")
.data((d) =>
["in", "out"].map((key) => ({ key, value: d[key], country: d.country }))
)
.join("rect")
.attr("x", (d) => x1(d.key))
.attr("y", (d) => y(d.value))
.attr("width", x1.bandwidth())
.attr("height", (d) => height - y(d.value))
.attr("fill", (d) => color(d.key))
.on("mouseover", (event, d) => {
tooltip
.style("display", "block")
.html(
`<strong>${d.country}</strong><br>${
d.key === "in" ? "Trafficked In" : "Trafficked Out"
}: <strong>${d.value}</strong>`
);
})
.on("mousemove", (event) => {
tooltip
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY - 28 + "px");
})
.on("mouseout", () => {
tooltip.style("display", "none");
});

// Legend
const legend = svg
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top / 2})`);

const legendItems = legend
.selectAll("g.legend-item")
.data(["Trafficked In", "Trafficked Out"])
.join("g")
.attr("class", "legend-item")
.attr("transform", (d, i) => `translate(${i * 140}, 0)`);

legendItems
.append("rect")
.attr("x", 0)
.attr("y", -12)
.attr("width", 18)
.attr("height", 18)
.attr("fill", (d, i) => color(i === 0 ? "in" : "out"));

legendItems
.append("text")
.attr("x", 24)
.attr("y", 0)
.attr("dy", "-0.25em")
.style("font-size", "13px")
.text((d) => d);

return container;
}
Insert cell
viewof genderBarChart = {
const margin = { top: 30, right: 20, bottom: 70, left: 50 };
const width = 360;
const height = 240;

const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.style("font-family", "sans-serif"); // Global font

// Tooltip
const tooltip = d3
.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("pointer-events", "none")
.style("background", "rgba(255, 255, 255, 0.95)")
.style("padding", "6px 10px")
.style("border", "1px solid #aaa")
.style("border-radius", "4px")
.style("font", "13px sans-serif")
.style("display", "none")
.style("z-index", 9999);

// Aggregate and sort gender data
const data = d3
.rollups(
sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor_sexual_recruiter_year,
(v) => v.length,
(d) => d.gender
)
.map(([gender, count]) => ({ gender, count }))
.sort((a, b) => d3.descending(a.count, b.count));

const x = d3
.scaleBand()
.domain(data.map((d) => d.gender))
.range([margin.left, width - margin.right])
.padding(0.3);

const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.count)])
.nice()
.range([height - margin.bottom, margin.top]);

const color = d3
.scaleOrdinal()
.domain(data.map((d) => d.gender))
.range(["#fd7979", "#68a8f1", "#649455", "#8496d3"]);

// X-axis
svg
.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "rotate(-15)")
.style("text-anchor", "end")
.style("font-size", "11px");

// X-axis label
svg
.append("text")
.attr("x", (width + margin.left - margin.right) / 2)
.attr("y", height - 8)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.style("font-size", "11px")
.text("Gender");

// Y-axis
svg
.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y));

// Y-axis label
svg
.append("text")
.attr("text-anchor", "middle")
.attr("transform", `rotate(-90)`)
.attr("x", -height / 2)
.attr("y", 10)
.style("font-size", "11px")
.style("font-weight", "bold")
.text("Number of Victims");

// Bars with tooltip events
svg
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", (d) => x(d.gender))
.attr("y", (d) => y(d.count))
.attr("width", x.bandwidth())
.attr("height", (d) => y(0) - y(d.count))
.attr("fill", (d) => color(d.gender))
.on("mouseover", (event, d) => {
tooltip
.style("display", "block")
.html(`<strong>${d.gender}</strong><br>Count: ${d.count}`);
})
.on("mousemove", (event) => {
tooltip
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY - 28}px`);
})
.on("mouseout", () => {
tooltip.style("display", "none");
});

return svg.node();
}
Insert cell
viewof ageGroupBarChart = {
// Data assumed available as 'sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor_sexual_recruiter_year'
const dataRaw =
sub_gender_age_citizenship_exploitation_duration_meanscontrol_exploitationtype_labor_sexual_recruiter_year;

// Count by ageBroad
const countsByAge = d3.rollups(
dataRaw,
(v) => v.length,
(d) => d.ageBroad
);

// Sort descending by count
countsByAge.sort((a, b) => d3.descending(a[1], b[1]));

const data = countsByAge.map(([ageBroad, count]) => ({ ageBroad, count }));

// Dimensions
const margin = { top: 30, right: 20, bottom: 70, left: 50 };
const width = 360;
const height = 240;

// Create SVG
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.style("font-family", "sans-serif");

// X scale - categorical, age groups
const x = d3
.scaleBand()
.domain(data.map((d) => d.ageBroad))
.range([margin.left, width - margin.right])
.padding(0.3);

// Y scale - linear, counts
const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.count)])
.nice()
.range([height - margin.bottom, margin.top]);

// Color scale
const color = d3
.scaleOrdinal()
.domain(data.map((d) => d.ageBroad))
.range(d3.schemeTableau10);

// X Axis
svg
.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end")
.style("font-size", "11px");

// X axis label
svg
.append("text")
.attr("x", (width + margin.left - margin.right) / 2)
.attr("y", height - 8)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.style("font-size", "11px")
.text("Age Group");

// Y Axis
svg
.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y).ticks(6))
.selectAll("text")
.style("font-size", "11px");

// Y axis label
svg
.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -(height / 2))
.attr("y", 10)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.style("font-size", "11px")
.text("Number of Victims");

// Tooltip div
let tooltip = d3.select("body").select(".ageGroup-tooltip");
if (tooltip.empty()) {
tooltip = d3
.select("body")
.append("div")
.attr("class", "ageGroup-tooltip")
.style("position", "absolute")
.style("background", "#fff")
.style("border", "1px solid #333")
.style("padding", "6px 8px")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font", "13px sans-serif")
.style("display", "none");
}

// Bars
svg
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d) => x(d.ageBroad))
.attr("y", (d) => y(d.count))
.attr("width", x.bandwidth())
.attr("height", (d) => y(0) - y(d.count))
.attr("fill", (d) => color(d.ageBroad))
.on("mouseover", (event, d) => {
tooltip
.style("display", "block")
.html(
`<strong>Age Group:</strong> ${d.ageBroad}<br/><strong>Victims:</strong> ${d.count}`
);
})
.on("mousemove", (event) => {
tooltip
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY - 28 + "px");
})
.on("mouseout", () => tooltip.style("display", "none"));

return svg.node();
}
Insert cell
traffickingDashboard = html`<div style="max-width: 1000px; margin: 40px 0; font-family: sans-serif; background: #fff; padding: 30px; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">

<div style="margin-bottom: 40px;">
<h1 style="font-size: 32px; margin-bottom: 8px;">Victim Demographics Overview</h1>
<p style="font-size: 16px; line-height: 1.5; color: #444; max-width: 700px;">
The following charts provide key demographic information about trafficking victims, offering important context to better understand the flow patterns.
</p>
</div>

<h1 style="font-size: 26px; margin-bottom: 10px;">Origin vs Destination of Trafficking Victims</h1>
<p style="font-size: 16px; line-height: 1.6; margin-bottom: 30px;">
Below, you will see the number of victims trafficked <strong>into</strong> and <strong>out of</strong> each country based on reported cases in the dataset. It highlights both destination and origin points for trafficking victims. Between 2002 and 2023, the <strong>United States of America</strong> and <strong>Ukraine</strong> had trafficked the highest number of victims into their countries.
</p>

<div style="margin-top: 20px; margin-bottom: 50px;">
${viewof traffickInOutBarChart}
</div>

<h2 style="font-size: 26px; margin-bottom: 10px;">Demographics: Gender Distribution of Victims</h2>
<p style="font-size: 16px; line-height: 1.6; margin-bottom: 30px;">
The data shows that nearly <strong>half of all trafficking victims are women</strong>, followed by men who make up <strong>just over a quarter</strong> of reported cases. A substantial portion of cases list gender as <strong>unknown</strong>, highlighting gaps in data collection. Less than 1% of victims are identified as <strong>transgender, nonconforming, or other identities</strong>.
</p>

<div style="margin-top: 20px; margin-bottom: 50px;">
${viewof genderBarChart}
</div>

<h2 style="font-size: 26px; margin-bottom: 10px;">Demographics: Age Distribution of Victims</h2>
<p style="font-size: 16px; line-height: 1.6; margin-bottom: 30px;">
Most trafficking victims are <strong>adults</strong>, though a significant portion are <strong>children and adolescents</strong>. These patterns reflect how trafficking affects individuals across a <strong>wide range of age groups</strong>, often depending on the <strong>type of exploitation</strong> and region.
</p>

<div style="margin-top: 20px;">
${viewof ageGroupBarChart}
</div>
</div>`
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