mutable subgroupsRequested = false
rawdata = subgroupsRequested
? FileAttachment("plotdata.csv").csv({
typed: false,
// Keep change as a string (values like "2.3*") and cast numerics manually
array: false
})
: []
changeData = FileAttachment("changedata.csv").csv({ typed: false })
prevalenceChangeData = FileAttachment("prevalence-change.csv").csv({ typed: false })
prevalenceRaw = FileAttachment("prevalence.csv").csv({ typed: false })
prevalenceData = prevalenceRaw
.sort((a, b) => a.drug && b.drug ? a.drug.localeCompare(b.drug) : 0)
.map(d => ({
...d,
year: parser(String(d.year)),
estimate: d.estimate == null || d.estimate === "" ? null : +d.estimate,
LBYear1: d.LBYear1 != null && d.LBYear1 !== "" ? parser(String(d.LBYear1)) : null,
LBYear2: d.LBYear2 != null && d.LBYear2 !== "" ? parser(String(d.LBYear2)) : null,
LBYear3: d.LBYear3 != null && d.LBYear3 !== "" ? parser(String(d.LBYear3)) : null
}))
alphabetical = rawdata.sort((a, b) =>
a.drug && b.drug ? a.drug.localeCompare(b.drug) : 0
)
parser = d3.timeParse("%Y")
formatter = d3.timeFormat("%Y")
data = alphabetical.map(d => ({
...d,
year: parser(String(d.year)),
estimate: d.estimate == null || d.estimate === "" ? null : +d.estimate,
LBYear1: d.LBYear1 != null && d.LBYear1 !== "" ? parser(String(d.LBYear1)) : null,
LBYear2: d.LBYear2 != null && d.LBYear2 !== "" ? parser(String(d.LBYear2)) : null,
LBYear3: d.LBYear3 != null && d.LBYear3 !== "" ? parser(String(d.LBYear3)) : null
}))Demographic Subgroups
Demographic Subgroup Differences among 8th, 10th, and 12th Grade Students
Data & Parsing
Group Configuration
// Single source of truth for all six groups.
// color/symbol arrays are index-aligned with subgroups.
groups = [
{
key: "Sex",
subgroups: ["Male", "Female"],
colors: ["#59bbeb", "#6ac4a1"],
symbols: ["circle", "square"]
},
{
key: "College Plans",
subgroups: ["No Plans To Attend 4-Year College", "Plans To Attend 4-Year College"],
colors: ["#59bbeb", "#6ac4a1"],
symbols: ["triangle", "diamond"]
},
{
key: "Region",
subgroups: ["Northeast", "Midwest", "South", "West"],
colors: ["#59bbeb", "#6ac4a1", "#cca438", "#FF834F"],
symbols: ["circle", "square", "triangle", "diamond"]
},
{
key: "Population Density",
subgroups: ["City", "Suburban", "Rural"],
colors: ["#59bbeb", "#6ac4a1", "#cca438"],
symbols: ["circle", "square", "triangle"]
},
{
key: "Parental Education",
subgroups: ["Neither Parent Has College Degree", "At Least One Parent Has College Degree"],
colors: ["#59bbeb", "#6ac4a1"],
symbols: ["circle", "triangle"]
},
{
key: "Race/Ethnicity",
subgroups: ["Hispanic", "Non-Hispanic Black", "Non-Hispanic White"],
colors: ["#59bbeb", "#6ac4a1", "#cca438"],
symbols: ["circle", "triangle", "square"]
}
]Controls
PersistInput = (field, input) => {
const getHashValue = () => {
let hashValue = new URLSearchParams(location.hash.slice(1)).get(field);
try {
hashValue = JSON.parse(hashValue);
} catch {}
return hashValue ? hashValue : input.value;
};
const setHashValue = (val) => {
const params = new URLSearchParams(location.hash.slice(1));
params.set(field, JSON.stringify(val));
html`<a href="#${params.toString()}">`.click();
};
const setInput = (val) => {
input.value = val;
input.dispatchEvent(new Event("input", { bubbles: true }));
};
const onInputChange = () => {
setHashValue(input.value);
};
input.addEventListener("input", onInputChange);
setInput(getHashValue() || input.value);
onInputChange();
return input;
}
// Sourced ONLY from the small prevalence file so it's computed once and never
// changes mid-session — a changing drugList would tear down and rebuild the
// search input (datalist: drugList) and break its reactive value. Assumes
// prevalence.csv contains every drug (true today; sanity-check it).
drugList = [...new Set(prevalenceData.map(d => d.drug).filter(Boolean))]
.sort((a, b) => a.localeCompare(b))
viewof select = PersistInput("drug",
Inputs.text({
width: 300,
value: "Any Illicit Drug",
submit: true,
placeholder: "Search Drug",
datalist: drugList
})
)
// Clear visible input text on load and after submit
{
const input = viewof select.querySelector("input")
input.value = ""
let lastValid = select
// Create error message element
const error = html`<div style="color:red; font-size:0.8rem; height:1rem;"></div>`
viewof select.appendChild(error)
viewof select.addEventListener("submit", (e) => {
const val = input.value.trim()
if (!drugList.some(d => d.toLocaleLowerCase() === val.toLocaleLowerCase())) {
e.preventDefault()
e.stopPropagation()
input.style.borderColor = "red"
error.textContent = "Please search a drug from the list."
viewof select.value = lastValid
viewof select.dispatchEvent(new Event("input"))
setTimeout(() => {
input.style.borderColor = ""
error.textContent = ""
input.value = ""
}, 2000)
return
}
lastValid = val
setTimeout(() => { input.value = "" }, 50)
})
}drugDataPrev = prevalenceData.filter(d =>
d.drug?.toLocaleLowerCase() === select?.toLocaleLowerCase()
)
viewof radioTime = {
const values = d3.group(drugDataPrev, d => d.time)
return Inputs.radio(values, {
key: values.has("12 Month") ? "12 Month" : values.keys().next().value,
label: "Reporting Interval"
})
}
viewof radioGrade = {
// Reorder into a fixed grade sequence — d3.group otherwise emits keys in
// data row order (which varies by source file), giving e.g. 10th/12th/8th.
const gradeOrder = ["8th Grade", "10th Grade", "12th Grade"]
const grouped = d3.group(radioTime, d => d.grade)
const values = new Map(
gradeOrder.filter(g => grouped.has(g)).map(g => [g, grouped.get(g)])
)
const defaultKey = values.has(savedGrade)
? savedGrade
: (values.has("12th Grade") ? "12th Grade" : values.keys().next().value)
const input = Inputs.radio(values, {
key: defaultKey,
label: "Grade"
})
input.addEventListener("input", () => { mutable savedGrade = input.value[0]?.grade })
return input
}
mutable activeGroup = "Overall"
mutable zoomed = false
mutable savedGrade = "12th Grade"
{
const observer = new MutationObserver(() => {
const active = document.querySelector(".bxby-subgroup-tabs .nav-link.active")
if (!active) return
mutable activeGroup = active.textContent.trim()
// Show/hide grade radio based on active tab
const gradeDiv = document.getElementById("grade-radio-container")
if (gradeDiv) gradeDiv.style.display = active.textContent.trim() === "Overall" ? "none" : "block"
})
observer.observe(document.body, { subtree: true, attributes: true, attributeFilter: ["class"] })
}currentData = data.filter(d =>
d.drug?.toLocaleLowerCase() === select?.toLocaleLowerCase() &&
d.time === radioTime?.[0]?.time &&
d.grade === radioGrade?.[0]?.grade
)Plot, Table & Download Generation
addTooltips = (chart, styles) => {
const stroke_styles = { stroke: "blue", "stroke-width": 3 };
const fill_styles = { fill: "blue", opacity: 0.5 };
// Workaround if it's in a figure
const type = d3.select(chart).node().tagName;
let wrapper =
type === "FIGURE" ? d3.select(chart).select("svg") : d3.select(chart);
// Workaround if there's a legend....
const svgs = d3.select(chart).selectAll("svg");
if (svgs.size() > 1) wrapper = d3.select([...svgs].pop());
wrapper.style("overflow", "visible"); // to avoid clipping at the edges
// Set pointer events to visibleStroke if the fill is none (e.g., if its a line)
wrapper.selectAll("path").each(function (data, index, nodes) {
// For line charts, set the pointer events to be visible stroke
if (
d3.select(this).attr("fill") === null ||
d3.select(this).attr("fill") === "none"
) {
d3.select(this).style("pointer-events", "visibleStroke");
if (styles === undefined) styles = stroke_styles;
}
});
if (styles === undefined) styles = fill_styles;
const tip = wrapper
.selectAll(".hover")
.data([1])
.join("g")
.attr("class", "hover")
.style("pointer-events", "none")
.style("text-anchor", "middle");
// Add a unique id to the chart for styling
const id = id_generator();
// Add the event listeners
d3.select(chart).classed(id, true); // using a class selector so that it doesn't overwrite the ID
wrapper.selectAll("title").each(function () {
// Get the text out of the title, set it as an attribute on the parent, and remove it
const title = d3.select(this); // title element that we want to remove
const parent = d3.select(this.parentNode); // visual mark on the screen
const t = title.text();
if (t) {
parent.attr("__title", t).classed("has-title", true);
title.remove();
}
// Mouse events
parent
.on("pointerenter pointermove", function (event) {
const text = d3.select(this).attr("__title");
const pointer = d3.pointer(event, wrapper.node());
if (text) tip.call(hover, pointer, text.split("\n"));
else tip.selectAll("*").remove();
// Raise it
d3.select(this).raise();
// Keep within the parent horizontally
const tipSize = tip.node().getBBox();
if (pointer[0] + tipSize.x < 0)
tip.attr(
"transform",
`translate(${tipSize.width / 2}, ${pointer[1] + 7})`
);
else if (pointer[0] + tipSize.width / 2 > wrapper.attr("width"))
tip.attr(
"transform",
`translate(${wrapper.attr("width") - tipSize.width / 2}, ${
pointer[1] + 7
})`
);
})
.on("pointerout", function (event) {
tip.selectAll("*").remove();
// Lower it!
d3.select(this).lower();
});
});
// Remove the tip if you tap on the wrapper (for mobile)
wrapper.on("touchstart", () => tip.selectAll("*").remove());
// Define the styles
chart.appendChild(html`<style>
.${id} .has-title { cursor: pointer; pointer-events: all; }
.${id} .has-title:hover { ${Object.entries(styles).map(([key, value]) => `${key}: ${value};`).join(" ")} }`);
return chart;
}
// To generate a unique ID for each chart so that the styles only apply to that chart
id_generator = () => {
var S4 = function () {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
return "a" + S4() + S4();
}
// Function to position the tooltip
hover = (tip, pos, text) => {
const side_padding = 10;
const vertical_padding = 5;
const vertical_offset = 15;
// Empty it out
tip.selectAll("*").remove();
// Append the text
tip
.style("text-anchor", "middle")
.style("pointer-events", "none")
.attr("transform", `translate(${pos[0]}, ${pos[1] + 7})`)
.selectAll("text")
.data(text)
.join("text")
.style("dominant-baseline", "ideographic")
.text((d) => d)
.attr("y", (d, i) => (i - (text.length - 1)) * 15 - vertical_offset)
.style("font-weight", (d, i) => (i === 0 ? "bold" : "normal"));
const bbox = tip.node().getBBox();
// Add a rectangle (as background)
tip
.append("rect")
.attr("y", bbox.y - vertical_padding)
.attr("x", bbox.x - side_padding)
.attr("width", bbox.width + side_padding * 2)
.attr("height", bbox.height + vertical_padding * 2)
.style("fill", "white")
.style("stroke", "#d3d3d3")
.lower();
}
// ── Line-break z-channel ──────────────────────────────────────────────────────
// Encodes which segment each point belongs to by checking whether the point
// falls after each of the (up to three) breakpoint years. A null LBYear means
// no break for that slot — every point shares the same constant boolean.
function zSegment(d) {
return [
d.subgroup,
d.LBYear1 === null
? d.year.getUTCFullYear() > 1974
: d.year.getUTCFullYear() > d.LBYear1.getUTCFullYear(),
d.LBYear2 === null
? d.year.getUTCFullYear() > 1974
: d.year.getUTCFullYear() > d.LBYear2.getUTCFullYear(),
d.LBYear3 === null
? d.year.getUTCFullYear() > 1974
: d.year.getUTCFullYear() > d.LBYear3.getUTCFullYear()
].join()
}
// ── Plot factory ──────────────────────────────────────────────────────────────
function makePlot(plotData, cfg) {
const present = new Set(plotData.map(d => d.subgroup))
const domain = cfg.subgroups.filter(s => present.has(s))
const colorScale = d3.scaleOrdinal(cfg.subgroups, cfg.colors)
const symbolScale = d3.scaleOrdinal(cfg.subgroups, cfg.symbols)
return addTooltips(
Plot.plot({
ariaLabel: `Line graph of drug use trends by ${cfg.key}`,
width: 900,
height: 700,
marginBottom: 50,
style: { overflow: "visible", fontSize: 12 },
symbol: { domain, range: domain.map(s => symbolScale(s)), legend: true, swatchSize: 23 },
color: { domain, range: domain.map(s => colorScale(s)) },
y: { label: null, axis: null, nice: true, domain: zoomed ? [0, d3.max(plotData, d => d.estimate) * 1.1] : [0, 100] },
x: {
type: "time",
label: null,
axis: null,
interval: "year",
domain: [
d3.utcYear.offset(d3.min(plotData, d => d.year), -1),
d3.utcYear.offset(d3.max(plotData, d => d.year), 0)
]
},
marks: [
Plot.ruleY([0]),
Plot.ruleX([d3.utcYear.offset(d3.min(plotData, d => d.year), -1), { strokeWidth: 1 }]),
Plot.axisY({
label: "Percentage (%)",
labelArrow: "none",
labelAnchor: "center",
labelOffset: 50,
tickSize: 0
}),
Plot.axisX({
label: "Years",
labelArrow: "none",
labelAnchor: "center",
anchor: "bottom",
tickSize: 0
}),
Plot.dot(plotData, {
x: "year", y: "estimate", r: 4,
fill: "subgroup", symbol: "subgroup",
title: d => `${d.subgroup}\n${formatter(d.year)}: ${d.estimate}%`
}),
Plot.line(plotData, {
x: "year", y: "estimate",
z: zSegment,
stroke: "subgroup"
})
]
}),
{ fill: "subgroup" }
)
}
// ── Wide-pivot table factory ──────────────────────────────────────────────────
function pivotWide(rows) {
const yearsSet = new Set(rows.map(d => formatter(d.year)))
const minYear = d3.min(rows, d => d.year.getUTCFullYear())
if (minYear < 2020) yearsSet.add("2020")
const years = [...yearsSet].sort()
const subgroups = [...new Set(rows.map(d => d.subgroup))]
const lookup = new Map(rows.map(d => [`${d.subgroup}|${formatter(d.year)}`, d.estimate]))
const columns = ["subgroup", ...years, "change"] // explicit order for Inputs.table
// Collect the set of break years for each subgroup so we can mark them with ‡
const breakYears = new Map()
for (const sg of subgroups) {
const sgRows = rows.filter(d => d.subgroup === sg)
const breaks = new Set()
for (const d of sgRows) {
if (d.LBYear1) breaks.add(formatter(d.LBYear1))
if (d.LBYear2) breaks.add(formatter(d.LBYear2))
if (d.LBYear3) breaks.add(formatter(d.LBYear3))
}
breakYears.set(sg, breaks)
}
// Look up change values from the dedicated change dataset
// keyed by drug|time|grade|subgroup — one value per combination
const drug = rows[0]?.drug
const time = rows[0]?.time
const grade = rows[0]?.grade
const changeLookup = new Map(
subgroups.map(sg => {
const match = changeData.find(d =>
d.drug === drug && d.time === time &&
d.grade === grade && d.subgroup === sg
)
return [sg, match?.change ?? ""]
})
)
const data = subgroups.map(sg => {
const row = { subgroup: sg }
const breaks = breakYears.get(sg)
for (const yr of years) {
if (yr === "2020") {
row[yr] = "§"
} else {
const val = lookup.get(`${sg}|${yr}`)
const dagger = breaks.has(yr) ? "‡" : ""
row[yr] = val != null ? `${val}${dagger}` : "—"
}
}
row["change"] = changeLookup.get(sg)
return row
})
return { data, columns }
}
// ── Download button factory ───────────────────────────────────────────────────
function makeDownload(rows, filename) {
const clean = rows.map(({ year, LBYear1, LBYear2, LBYear3, ...rest }) => ({
...rest,
year: formatter(year),
LBYear1: LBYear1 ? formatter(LBYear1) : "",
LBYear2: LBYear2 ? formatter(LBYear2) : "",
LBYear3: LBYear3 ? formatter(LBYear3) : ""
}))
const blob = new Blob([d3.csvFormat(clean)], { type: "text/csv" })
const size = (blob.size / 1024).toFixed(0)
return DOM.download(blob, filename, `Download ${filename} (~${size} KB)`)
}
// ── Single pass: build plots, tables, downloads for all six groups ────────────
// Declared and returned from ONE cell so OJS correctly tracks currentData,
// select, and radioTime as reactive dependencies.
groupOutputs = {
const plots = new Map()
const tables = new Map()
const downloads = new Map()
const dlName = `${select} - ${radioTime[0]?.time}`
for (const cfg of groups) {
const rows = currentData.filter(d => d.group_type === cfg.key)
plots.set(cfg.key, makePlot(rows, cfg))
const { data: tData, columns: tCols } = pivotWide(rows)
tables.set(cfg.key, Inputs.table(tData, { width: "100%", columns: tCols, header: { change: "24-25 Change"} }))
downloads.set(cfg.key, makeDownload(rows, `${dlName} - ${cfg.key}`))
}
return { plots, tables, downloads }
}Named Exports (consumed by index.qmd)
plotSex = groupOutputs.plots.get("Sex")
plotCollege = groupOutputs.plots.get("College Plans")
plotRegion = groupOutputs.plots.get("Region")
plotDensity = groupOutputs.plots.get("Population Density")
plotParentEd = groupOutputs.plots.get("Parental Education")
plotRace = groupOutputs.plots.get("Race/Ethnicity")
tableSex = groupOutputs.tables.get("Sex")
tableCollege = groupOutputs.tables.get("College Plans")
tableRegion = groupOutputs.tables.get("Region")
tableDensity = groupOutputs.tables.get("Population Density")
tableParentEd = groupOutputs.tables.get("Parental Education")
tableRace = groupOutputs.tables.get("Race/Ethnicity")
downloadSex = groupOutputs.downloads.get("Sex")
downloadCollege = groupOutputs.downloads.get("College Plans")
downloadRegion = groupOutputs.downloads.get("Region")
downloadDensity = groupOutputs.downloads.get("Population Density")
downloadParentEd = groupOutputs.downloads.get("Parental Education")
downloadRace = groupOutputs.downloads.get("Race/Ethnicity")Prevalence Plot, Table & Download
function zSegmentPrevalence(d) {
return [
d.grade,
d.LBYear1 === null
? d.year.getUTCFullYear() > 1974
: d.year.getUTCFullYear() > d.LBYear1.getUTCFullYear(),
d.LBYear2 === null
? d.year.getUTCFullYear() > 1974
: d.year.getUTCFullYear() > d.LBYear2.getUTCFullYear(),
d.LBYear3 === null
? d.year.getUTCFullYear() > 1974
: d.year.getUTCFullYear() > d.LBYear3.getUTCFullYear()
].join()
}
prevalenceOutputs = {
// Filter prevalence data to the current drug + time selections
const rows = prevalenceData.filter(d =>
d.drug?.toLocaleLowerCase() === select?.toLocaleLowerCase() &&
d.time === radioTime[0]?.time
)
const grades = ["8th Grade", "10th Grade", "12th Grade"]
const present = grades.filter(g => rows.some(d => d.grade === g))
const colorScale = d3.scaleOrdinal(grades, ["#59bbeb", "#6ac4a1", "#cca438"])
const symbolScale = d3.scaleOrdinal(grades, ["circle", "square", "triangle"])
// ── Plot ────────────────────────────────────────────────────────────────────
const plot = addTooltips(
Plot.plot({
ariaLabel: `Line graph of drug use prevalence by grade`,
width: 900,
height: 700,
marginBottom: 50,
style: { overflow: "visible", fontSize: 12 },
symbol: { domain: present, range: present.map(g => symbolScale(g)), legend: true, swatchSize: 23 },
color: { domain: present, range: present.map(g => colorScale(g)) },
y: { label: null, axis: null, nice:true, domain: zoomed ? [0, d3.max(rows, d => d.estimate) * 1.1] : [0, 100] },
x: {
type: "time",
interval: "year",
label: null,
axis: null,
domain: [
d3.utcYear.offset(d3.min(rows, d => d.year), -1),
d3.utcYear.offset(d3.max(rows, d => d.year), 0)
]
},
marks: [
Plot.ruleY([0]),
Plot.ruleX([d3.utcYear.offset(d3.min(rows, d => d.year), -1)], { strokeWidth: 1}),
Plot.axisY({
label: "Percentage (%)",
labelArrow: "none",
labelAnchor: "center",
labelOffset: 50,
tickSize: 0
}),
Plot.axisX({
label: "Years",
labelAnchor: "center",
labelArrow: "none",
anchor: "bottom",
tickSize: 0
}),
Plot.dot(rows, {
x: "year", y: "estimate", r: 4,
fill: "grade", symbol: "grade",
title: d => `${d.grade}
${formatter(d.year)}: ${d.estimate}%`
}),
Plot.line(rows, {
x: "year", y: "estimate",
z: zSegmentPrevalence,
stroke: "grade"
})
]
}),
{ fill: "grade" }
)
// ── Wide-pivot table ────────────────────────────────────────────────────────
const yearsSet = new Set(rows.map(d => formatter(d.year)))
const minYear = d3.min(rows, d => d.year.getUTCFullYear())
if (minYear < 2020) yearsSet.add("2020")
const years = [...yearsSet].sort()
const lookup = new Map(rows.map(d => [`${d.grade}|${formatter(d.year)}`, d.estimate]))
// Break years per grade
const breakYears = new Map()
for (const g of present) {
const gRows = rows.filter(d => d.grade === g)
const breaks = new Set()
for (const d of gRows) {
if (d.LBYear1) breaks.add(formatter(d.LBYear1))
if (d.LBYear2) breaks.add(formatter(d.LBYear2))
if (d.LBYear3) breaks.add(formatter(d.LBYear3))
}
breakYears.set(g, breaks)
}
const tData = present.map(g => {
const row = { grade: g }
const breaks = breakYears.get(g)
for (const yr of years) {
if (yr === "2020") {
row[yr] = "§"
} else {
const val = lookup.get(`${g}|${yr}`)
const dagger = breaks.has(yr) ? "‡" : ""
row[yr] = val != null ? `${val}${dagger}` : "—"
}
}
return row
})
// Look up change value per grade from prevalence-change.csv
const prevChangeLookup = new Map(
present.map(g => {
const match = prevalenceChangeData.find(d =>
d.drug === rows[0]?.drug && d.time === rows[0]?.time && d.grade === g
)
return [g, match?.change ?? ""]
})
)
tData.forEach(row => { row["change"] = prevChangeLookup.get(row.grade) })
const tCols = ["grade", ...years, "change"]
const table = Inputs.table(tData, { width: "100%", columns: tCols, header: { change: "24-25 Change" } })
// ── Download ────────────────────────────────────────────────────────────────
const clean = rows.map(({ year, LBYear1, LBYear2, LBYear3, ...rest }) => ({
...rest,
year: formatter(year),
LBYear1: LBYear1 ? formatter(LBYear1) : "",
LBYear2: LBYear2 ? formatter(LBYear2) : "",
LBYear3: LBYear3 ? formatter(LBYear3) : ""
}))
const blob = new Blob([d3.csvFormat(clean)], { type: "text/csv" })
const size = (blob.size / 1024).toFixed(0)
const dlName = `${select} - ${radioTime[0]?.time} - Prevalence`
const download = DOM.download(blob, dlName, `Download ${dlName} (~${size} KB)`)
return { plot, table, download, present }
}
prevalencePlot = prevalenceOutputs.plot
prevalenceTable = prevalenceOutputs.table
prevalenceDownload = prevalenceOutputs.download
prevalenceGrades = prevalenceOutputs.presentCitation
citation = () => html`<div style="max-width: 750px;"><p class="text-center" style="font-size: small;">Suggested citation: Miech, R. A., Patrick, M. E., O’Malley, P. M., Jager, J. O. and Jang, J. B. (2026). Monitoring the Future national survey results on drug use, 1975–2025: Overview and detailed results for secondary school students. Monitoring the Future Monograph Series. Ann Arbor, MI: Institute for Social Research, University of Michigan. Available at <a style="color: #20399D;" href="https://monitoringthefuture.org/results/annual-reports/">https://monitoringthefuture.org/results/annual-reports/</a></p></div>`Footnotes
Commentary
function makeAccordion(table) {
// Each accordion needs its own ID namespace — all seven tabs render this
// function, so hardcoded IDs would collide (invalid HTML, and Bootstrap's
// data-bs-parent scoping breaks).
const uid = `acc-${Math.random().toString(36).slice(2, 9)}`
const tableId = `${uid}-table`
const commId = `${uid}-comm`
const el = html`
<div style="width: 90%" class="accordion accordion-flush" id="${uid}">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#${tableId}" aria-controls="${tableId}">
Table with Detailed Estimates
</button>
</h2>
<div id="${tableId}" class="accordion-collapse collapse" data-bs-parent="#${uid}">
<div class="accordion-body">
<div>${table}</div>
<p style="font-size:small; margin-bottom:0px;">Note:
Level of significance: *=p<.05, **=p<.01, ***=p<.001.<br>
‡ indicates that the question changed the following year.<br>
§ indicates insufficient data for that year.
</p>
${googledoclink[0]?.["Footnote Numbers"] ? html`<p style="font-size:small; margin-bottom:0px;">Footnote Numbers: ${googledoclink[0]["Footnote Numbers"]}</p>` : ""}
<p style="font-size:small; margin-top:4px;"><a href="footnotes.html" target="_blank">Footnotes</a></p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#${commId}" aria-controls="${commId}">
Commentary
</button>
</h2>
<div id="${commId}" class="accordion-collapse collapse" data-bs-parent="#${uid}">
<div style="display:flex; justify-content:center; width:100%" class="accordion-body">${publishdoc()}</div>
</div>
</div>
</div>`
// When the "Detailed Estimates" accordion opens, scroll each overflowing
// element to its right edge so the most recent years are visible by default.
// We wait for shown.bs.collapse because scrollWidth/clientWidth are unreliable
// while the accordion is still collapsed, and wrap in requestAnimationFrame
// to ensure browser layout has settled before we read/write scroll values.
const tablesCollapse = el.querySelector(`#${tableId}`)
if (tablesCollapse) {
tablesCollapse.addEventListener("shown.bs.collapse", () => {
requestAnimationFrame(() => {
el.querySelectorAll("*").forEach(node => {
if (node.scrollWidth > node.clientWidth + 1) {
node.scrollLeft = node.scrollWidth
}
})
})
})
}
return el
}Commentary Data
drugCommentary = FileAttachment("drug_commentary_with_footnotes.csv").csv()
googledoclink = drugCommentary.filter(
({ DrugName }) => DrugName.toLocaleLowerCase() === select.toLocaleLowerCase()
)
editdoc = html`<iframe title="Commentary for selected drug" width="100%" height="800px" src=${googledoclink[0].EditLink}></iframe>`
publishdoc = () => html`<iframe title="Commentary for selected drug" style="width:900px; height:1300px;"src=${googledoclink[0].PublishLink}></iframe>`