sigData = FileAttachment("chapter2_sigtests.csv").csv({ typed: true })
sigsDrug = sigData.filter(
({ drug }) => drug === select
)
significance = sigsDrug.filter(({ time }) => time === radio[0].time)
significanceDocumentation
Adult and Young Adult Substance Use Dashboard
Significance Tests
Significance Legend in Graph
fmtChange = d => (d == null || isNaN(d) ? "." : d.toFixed(1))
fmtSig = d => (d == null ? "N/A" : d)
// Baseline-aware significance marker. A trend window only exists if its
// baseline year is present; parseNum() returns null for null / "" / "."
// (and never NaN). When the baseline (OneYear1 / FiveYear1 / TenYear1) is
// missing, show "N/A" regardless of what sig_* holds. Otherwise show the
// marker, or "N/A" if it too is empty. This guards the legend/table even if
// a stray "n.s." ever slips back into the source data.
sigMark = (row, baseKey, sigKey) => {
if (row == null) return "N/A";
if (parseNum(row[baseKey]) == null) return "N/A";
const sv = row[sigKey];
return (sv == null || sv === "" || sv === ".") ? "N/A" : sv;
}
sigLegend1 = html`<style>
caption {
font-size: 9px;
}
table {
margin-bottom: 0.5rem;
}
.small td {
font-size: 9px;
color: #000066;
}
</style>
<table class="small" style="width: 250px; ">
<caption style="color: #000066;"> <b><u>2024 Trends</b></u> </caption>
<tr>
<td>Ages 19-30</td>
<td>1-Year Change</td>
<td>${fmtChange(significance[0]?.oneyrchange)}</td>
<td>${sigMark(significance[0], "OneYear1", "sig_oneyr")}</td>
</tr>
<tr>
<td> </td>
<td>5-Year Change</td>
<td>${fmtChange(significance[0]?.fiveyrchange)}</td>
<td>${sigMark(significance[0], "FiveYear1", "sig_fiveyr")}</td>
</tr>
<tr style="background-color: white;">
<td></td>
<td>10-Year Change</td>
<td>${fmtChange(significance[0]?.tenyrchange)}</td>
<td>${sigMark(significance[0], "TenYear1", "sig_tenyr")}</td>
</tr>
</table>`
sigLegend2 = html`<style>
caption {
font-size: 9px;
}
table {
margin-bottom: 0.5rem;
}
.small td {
font-size: 9px;
color: #000066;
}
</style>
<table class="small" style="width: 250px; ">
<caption style="color: #000066;"> <b><u>2024 Trends</b></u> </caption>
<tr>
<td>Ages 19-30</td>
<td>1-Year Change</td>
<td>${fmtChange(significance[0]?.oneyrchange)}</td>
<td>${sigMark(significance[0], "OneYear1", "sig_oneyr")}</td>
</tr>
<tr>
<td> </td>
<td>5-Year Change</td>
<td>${fmtChange(significance[0]?.fiveyrchange)}</td>
<td>${sigMark(significance[0], "FiveYear1", "sig_fiveyr")}</td>
</tr>
<tr style="background-color: white;">
<td></td>
<td>10-Year Change</td>
<td>${fmtChange(significance[0]?.tenyrchange)}</td>
<td>${sigMark(significance[0], "TenYear1", "sig_tenyr")}</td>
</tr>
<tr>
<td>Ages 35-50</td>
<td>1-Year Change</td>
<td>${fmtChange(significance[1]?.oneyrchange)}</td>
<td>${sigMark(significance[1], "OneYear1", "sig_oneyr")}</td>
</tr>
<tr>
<td> </td>
<td>5-Year Change</td>
<td>${fmtChange(significance[1]?.fiveyrchange)}</td>
<td>${sigMark(significance[1], "FiveYear1", "sig_fiveyr")}</td>
</tr>
<tr>
<td></td>
<td>10-Year Change</td>
<td>${fmtChange(significance[1]?.tenyrchange)}</td>
<td>${sigMark(significance[1], "TenYear1", "sig_tenyr")}</td>
</tr>
<tr>
<td>Ages 55-65</td>
<td>1-Year Change</td>
<td>${fmtChange(significance[2]?.oneyrchange)}</td>
<td>${sigMark(significance[2], "OneYear1", "sig_oneyr")}</td>
</tr>
<tr>
<td> </td>
<td>5-Year Change</td>
<td>${fmtChange(significance[2]?.fiveyrchange)}</td>
<td>${sigMark(significance[2], "FiveYear1", "sig_fiveyr")}</td>
</tr>
<tr>
<td></td>
<td>10-Year Change</td>
<td>${fmtChange(significance[2]?.tenyrchange)}</td>
<td>${sigMark(significance[2], "TenYear1", "sig_tenyr")}</td>
</tr>
</table>`
sigLegend = significance[1]?.oneyrchange === undefined ? sigLegend1 : sigLegend2
legendPlace = {if (radio[0]?.drug == "Alcohol" && radio[0]?.time == "12 Month"){
return "bottom: 60px; right: 50px;";
} else if (radio[0]?.drug == "Alcohol" && radio[0]?.time == "Lifetime") {
return "bottom: 60px; right: 50px;";
} else if (radio[0]?.drug == "Alcohol" && radio[0]?.time == "30 Day") {
return "bottom: 60px; right: 50px;";
} else {
return "top: 50px; right: 50px;";
}
}
legendPlaceTable Long Format
longTable = Inputs.table(tablesTime, {
columns: [
"year",
"age_18",
"age_19_20",
"age_21_22",
"age_23_24",
"age_25_26",
"age_27_28",
"age_29_30",
"age_35",
"age_40",
"age_45",
"age_50",
"age_55",
"age_60",
"age_65"
],
header: {
year: "Year",
age_18: "Age 18",
age_19_20: "Ages 19-20",
age_21_22: "Ages 21-22",
age_23_24: "Ages 23-24",
age_25_26: "Ages 25-26",
age_27_28: "Ages 27-28",
age_29_30: "Ages 29-30",
age_35: "Age 35",
age_40: "Age 40",
age_45: "Age 45",
age_50: "Age 50",
age_55: "Age 55",
age_60: "Age 60",
age_65: "Age 65"
},
format: {
year: d => html`<b>${d}</b>`,
age_18: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_19_20: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_21_22: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_23_24: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_25_26: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_27_28: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_29_30: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_35: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_40: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_45: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_50: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_55: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_60: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1),
age_65: d=> (d == null || isNaN(d)) ? "." : d.toFixed(1)
},
rows: 75,
width: 1200
})Citation
citation = html`<div style="max-width: 750px;"><p style="font-size: small;">Suggested citation: Patrick, M. E., Miech, R. A., O'Malley, P. M., Jager, J. O., & Jang, J. B. (2026). Monitoring the Future Longitudinal Panel Study annual report: National data on substance use among adults ages 19 to 65, 1976–2025. Monitoring the Future Monograph Series. Ann Arbor, MI: Institute for Social Research, University of Michigan. <a href="https://monitoringthefuture.org/wp-content/uploads/2026/07/mtfpanel2026.pdf" target="_blank" rel="noopener noreferrer">MTF Panel Study Annual Report</a></p></div>`Footnotes
footnotesData = FileAttachment("Footnotes.csv").csv({ typed: true })
footnotesFiltered = footnotesData.filter(
({ drug }) => drug.toLocaleLowerCase() === select.toLocaleLowerCase()
)
function checkFootnote(d) {
if (d.length == 0 ) {
return html`<div></div>`;
} else if (d.length == 1) {
return html`<div style="max-width: 750px;"><p style="font-size: small;"><sup>${footnotesFiltered[0]?.superscript}</sup>${footnotesFiltered[0]?.footnote}</p></div>`;
} else {
return html`<div style="max-width: 750px;"><p style="font-size: small;"><sup>${footnotesFiltered[0]?.superscript}</sup>${footnotesFiltered[0]?.footnote}<br><sup>${footnotesFiltered[1]?.superscript}</sup>${footnotesFiltered[1]?.footnote}</p></div>`;
}
}
footnotes = checkFootnote(footnotesFiltered)
html`${footnotes}`Table Data
tables = FileAttachment("final_table_data.csv").csv({ typed: true })
// Missing values arrive as "" or "." (and sometimes null). Anything that
// doesn't coerce to a finite number becomes null, never NaN -- NaN survives
// `!= null` checks and silently poisons extents, min/max, and toFixed().
parseNum = d => {
if (d == null || d === "") return null;
const n = +d;
return Number.isFinite(n) ? n : null;
}
tablesFormatted = {
const tables2 = tables.map(
({ time, drug, year, age_18, age_19_20, age_21_22, age_23_24, age_25_26, age_27_28, age_29_30, age_35, age_40, age_45, age_50, age_55, age_60, age_65
}) => ({
time: time,
drug: drug,
year: String(year),
age_18: parseNum(age_18),
age_19_20: parseNum(age_19_20),
age_21_22: parseNum(age_21_22),
age_23_24: parseNum(age_23_24),
age_25_26: parseNum(age_25_26),
age_27_28: parseNum(age_27_28),
age_29_30: parseNum(age_29_30),
age_35: parseNum(age_35),
age_40: parseNum(age_40),
age_45: parseNum(age_45),
age_50: parseNum(age_50),
age_55: parseNum(age_55),
age_60: parseNum(age_60),
age_65: parseNum(age_65)
})
);
return tables2.filter(
({ drug }) => drug?.toLocaleLowerCase() === select?.toLocaleLowerCase()
);
}
tablesTime = tablesFormatted.filter(({ time }) => time === radio[0].time)
tablesTimeSUD Stacked Bar Data and Controls
sudRaw = FileAttachment("sud_stacked_data.csv").csv({ typed: true })
sudSubstances = [...new Set(sudRaw.map(d => d.substance))]
viewof sudSubstance = Inputs.select(sudSubstances, {
value: "Cannabis Use Disorder",
label: "Select SUD Measure:",
width: 220
})
viewof sudAgeGroup = Inputs.radio(["Age 35", "Ages 40-50", "Ages 55-65"], {
value: "Ages 40-50",
label: "Age Group:"
})
// Rank pins the stack order (0 = bottom), matching the Excel column charts:
// Disordered on the bottom, Nondisordered in the middle, Abstainer on top.
sudRank = ({ Disordered: 0, Nondisordered: 1, Abstainer: 2 })
sudData = sudRaw
.filter(d => d.substance === sudSubstance && d.age_group === sudAgeGroup)
.map(d => ({
substance: d.substance,
age_group: d.age_group,
year: String(d.year),
category: d.category,
percentage: parseNum(d.percentage),
rank: sudRank[d.category]
}))
sudDataSUD Stacked Bar Plot
sudColorDomain = ["Abstainer", "Nondisordered", "Disordered"]
sudColorRange = ["#59bbeb", "#d1a438", "#6ac464"]
sudPlot = addTooltips(
Plot.plot({
ariaLabel: `100% stacked bar chart of ${sudSubstance} composition among ${sudAgeGroup}, by year`,
width: 700,
height: 450,
marginBottom: 45,
marginLeft: 55,
style: { overflow: "visible", fontSize: 12 },
x: { label: "Year", type: "band", tickFormat: d => d },
y: { label: "Percentage (%)", labelAnchor: "center", domain: [0, 100], grid: true },
color: {
domain: sudColorDomain,
range: sudColorRange,
legend: true,
swatchSize: 18
},
marks: [
// order: "rank" -> Disordered (0) on the bottom, Abstainer (2) on top.
Plot.barY(sudData, Plot.stackY({ order: "rank" }, {
x: "year",
y: "percentage",
fill: "category",
inset: 0.5,
title: d => `${d.category}\n${d.year}: ${d.percentage == null ? "No data" : d.percentage.toFixed(1) + "%"}`
})),
Plot.ruleY([0])
]
}),
{ fill: "category" }
)SUD Combined Table
sudEstimateYears = [...new Set(sudData.map(d => d.year))].sort()
sudSig = FileAttachment("sud_sigtests.csv").csv({ typed: true })
sudSigRows = sudSig.filter(
d => d.substance === sudSubstance && d.age_group === sudAgeGroup
)
// Empty change -> "."; empty sig -> "N/A"; keeps "n.s." and "p<.xx" verbatim.
sudFmtChange = d => {
const n = parseNum(d);
return n == null ? "." : n.toFixed(1);
}
sudFmtSig = d => (d == null || d === "") ? "N/A" : d
sudTrendCols = ["1-Yr Change", "1-Yr Sig.", "5-Yr Change", "5-Yr Sig.", "10-Yr Change", "10-Yr Sig."]
// One row per group (Abstainer / Nondisordered / Disordered); columns are the
// per-year percentages followed by the trend / p-value labels.
sudCombinedRows = ["Abstainer", "Nondisordered", "Disordered"].map(cat => {
const row = { Group: cat };
sudEstimateYears.forEach(y => {
const m = sudData.find(d => d.year === y && d.category === cat);
row[y] = m ? m.percentage : null;
});
const sig = sudSigRows.find(r => r.category === cat) || {};
row["1-Yr Change"] = sig.oneyr_change;
row["1-Yr Sig."] = sig.oneyr_sig;
row["5-Yr Change"] = sig.fiveyr_change;
row["5-Yr Sig."] = sig.fiveyr_sig;
row["10-Yr Change"] = sig.tenyr_change;
row["10-Yr Sig."] = sig.tenyr_sig;
return row;
})
sudCombinedColumns = ["Group", ...sudEstimateYears, ...sudTrendCols]
sudCombinedFormat = ({
Group: d => html`<b>${d}</b>`,
"1-Yr Change": sudFmtChange,
"1-Yr Sig.": sudFmtSig,
"5-Yr Change": sudFmtChange,
"5-Yr Sig.": sudFmtSig,
"10-Yr Change": sudFmtChange,
"10-Yr Sig.": sudFmtSig,
...Object.fromEntries(
sudEstimateYears.map(y => [y, d => (d == null || isNaN(d)) ? "." : d.toFixed(1)])
)
})
// Per-column widths; their sum drives the accordion width so the dropdown
// button lines up with the table underneath it.
sudColWidths = ({
Group: 120,
...Object.fromEntries(sudEstimateYears.map(y => [y, 55])),
...Object.fromEntries(sudTrendCols.map(c => [c, 82]))
})
sudTableWidth = Object.values(sudColWidths).reduce((a, b) => a + b, 0)
sudCombinedTable = Inputs.table(sudCombinedRows, {
columns: sudCombinedColumns,
format: sudCombinedFormat,
width: sudColWidths,
rows: 5
})SUD Table Accordion
// Single dropdown holding the combined estimates + significance table. Its
// width tracks the table (sudTableWidth) so the button matches the table.
sudTablesAccordion = html`
<div style="width: ${sudTableWidth + 24}px; max-width: 100%;" class="accordion accordion-flush" id="accordionSud">
<div class="accordion-item">
<h2 class="accordion-header" id="sud-headingCombined">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sud-collapseCombined" aria-expanded="false" aria-controls="sud-collapseCombined">
${sudSubstance}: ${sudAgeGroup} Estimates and Significance
</button>
</h2>
<div id="sud-collapseCombined" class="accordion-collapse collapse" aria-labelledby="sud-headingCombined" data-bs-parent="#accordionSud">
<div class="accordion-body">
<div class="center"><div>${sudCombinedTable}</div></div>
</div>
</div>
</div>
</div>`Plot Data
parser = d3.timeParse("%Y")
format = d3.format(".4")
raw = FileAttachment("final_graph2_data.csv").csv({ typed: true })
rawAlphabetical = raw.sort((a, b) => {
if (a.drug && b.drug) {
return a.drug.localeCompare(b.drug);
} else {
return 0; // Preserve the order if 'name' is missing
}
});
rawAlphabeticalprevalence = rawAlphabetical.filter(thing => thing.drug != null)
data = {
const subset = prevalence.map(
({ year, drug, estimate, age, time, flag }) => ({
year: parser(year),
year2: format(year),
estimate: parseNum(estimate),
drug: drug,
age: age,
time: time,
flag: flag
})
);
const filtered = subset.filter(thing =>
thing.drug?.toLocaleLowerCase() === select?.toLocaleLowerCase()
);
return filtered;
}
dataradioDropdown Input
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;
}
viewof select = PersistInput("drug",
Inputs.select(
prevalence.map((d) => d.drug),
{
value: "Alcohol",
width: 175,
unique: true,
label: "Select Drug:"
}
))Horizontal Estimates and Significance Table
pivotAges = ["Age 19-30", "Age 35-50", "Age 55-65"]
pivotYears = [...new Set(radio.map(d => d.year.getFullYear()))].sort((a, b) => a - b)
pivotRows = pivotAges
.map((age, i) => {
const row = { age };
pivotYears.forEach(y => {
const match = radio.find(d => d.age === age && d.year.getFullYear() === y);
row["_" + y] = match ? match.estimate : null;
});
const sig = significance[i];
row.oneyr_change = sig?.oneyrchange ?? null;
row.oneyr_sig = sigMark(sig, "OneYear1", "sig_oneyr");
row.fiveyr_change = sig?.fiveyrchange ?? null;
row.fiveyr_sig = sigMark(sig, "FiveYear1", "sig_fiveyr");
row.tenyr_change = sig?.tenyrchange ?? null;
row.tenyr_sig = sigMark(sig, "TenYear1", "sig_tenyr");
return row;
})
.filter(row => pivotYears.some(y => row["_" + y] != null))
pivotColumns = [
"age",
...pivotYears.map(y => "_" + y),
"oneyr_change",
"oneyr_sig",
"fiveyr_change",
"fiveyr_sig",
"tenyr_change",
"tenyr_sig"
]
pivotHeader = ({
age: "Age Group",
oneyr_change: "1-Yr Change",
oneyr_sig: "1-Yr Sig.",
fiveyr_change: "5-Yr Change",
fiveyr_sig: "5-Yr Sig.",
tenyr_change: "10-Yr Change",
tenyr_sig: "10-Yr Sig.",
...Object.fromEntries(pivotYears.map(y => ["_" + y, String(y)]))
})
pivotFormat = ({
age: d => d,
oneyr_change: fmtChange,
oneyr_sig: fmtSig,
fiveyr_change: fmtChange,
fiveyr_sig: fmtSig,
tenyr_change: fmtChange,
tenyr_sig: fmtSig,
...Object.fromEntries(
pivotYears.map(y => ["_" + y, d => (d == null || isNaN(d)) ? "." : d.toFixed(1)])
)
})
// Finds whichever descendant actually has the horizontal overflow and pins it right
scrollTableRight = (root) => {
if (!root) return false;
const candidates = [root, ...root.querySelectorAll("*")];
const scroller = candidates.find(el => el.scrollWidth > el.clientWidth + 1);
if (!scroller) return false;
scroller.scrollLeft = scroller.scrollWidth - scroller.clientWidth;
return true;
}
horizontalTable = Inputs.table(pivotRows, {
columns: pivotColumns,
header: pivotHeader,
format: pivotFormat,
rows: 10,
width: 1200
})
accordionButtonsHorizontal = {
const el = html`
<div style="width: 1224px; max-width: 100%;" class="accordion accordion-flush" id="accordionFlushHorizontal">
<div class="accordion-item">
<h2 class="accordion-header" id="flush-headingHorizontal">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapseHorizontal" aria-expanded="false" aria-controls="flush-collapseHorizontal">
${radio[0].drug}: ${radio[0].time} Estimates and Significance by Age Group
</button>
</h2>
<div id="flush-collapseHorizontal" class="accordion-collapse collapse" aria-labelledby="flush-headingHorizontal" data-bs-parent="#accordionFlushHorizontal">
<div class="accordion-body">
<div class="center"><div>${horizontalTable}</div></div>
</div>
</div>
</div>`;
const panel = el.querySelector("#flush-collapseHorizontal");
// The table has zero width while the accordion is collapsed, so pin the
// scrollbar to the right each time the panel actually becomes visible.
const pinRight = () => {
if (!scrollTableRight(horizontalTable)) {
// Layout may not be settled yet; try again on the next frame.
requestAnimationFrame(() => scrollTableRight(horizontalTable));
}
};
// Fires as the panel starts opening, and again once the transition finishes.
panel.addEventListener("show.bs.collapse", () => requestAnimationFrame(pinRight));
panel.addEventListener("shown.bs.collapse", pinRight);
// Fallback if Bootstrap's JS events aren't available: watch for the panel
// gaining the "show" class.
new MutationObserver(() => {
if (panel.classList.contains("show")) requestAnimationFrame(pinRight);
}).observe(panel, { attributes: true, attributeFilter: ["class"] });
return el;
}Plot Title
ageQuestion = significance.length > 1
? "Respondents Aged 19 through 65"
: "Respondents Aged 19 through 30"Rendered Plot for Combined Age Groups
import {addTooltips} from "@mkfreeman/plot-tooltip"
formatter = d3.timeFormat("%Y")
color = d3.scaleOrdinal(
["Age 19-30", "Age 35-50", "Age 55-65"],
["#59bbeb", "#6ac4a1", "#cca438"]
)
symbol = d3.scaleOrdinal(
["Age 19-30", "Age 35-50", "Age 55-65"],
["circle", "square", "triangle"]
)
dateFilter1 = radio.filter(d => d.year > new Date("1988-01-01"));
dateFilter2 = radio.filter(d => d.year > new Date("2008-01-01"));
dateFilter3 = radio.filter(d => d.year > new Date("2023-01-01"));
legendFilter1 = dateFilter1.filter(d => d.age === "Age 19-30");
legendFilter2 = dateFilter2.filter(d => d.age === "Age 35-50");
legendFilter3 = dateFilter3.filter(d => d.age === "Age 55-65");
legendData = [...legendFilter1, ...legendFilter2, ...legendFilter3].filter(d => Number.isFinite(d.estimate));
legendDataxDomain = {
const [first, last] = d3.extent(legendData, d => d.year);
if (first == null) return [new Date(1987, 0, 1), new Date(2024, 0, 1)];
// d3.timeYear (local) matches d3.timeParse("%Y"), which parses in local time.
return [d3.timeYear.offset(first, -1), last];
}
// Tick step in years: every year for short spans, thinning to round intervals
// (2/5/10/20) as the span grows, so labels never collide at width 900.
xTickStep = {
const span = d3.timeYear.count(xDomain[0], xDomain[1]);
const maxLabels = 14;
return [1, 2, 5, 10, 20, 25, 50].find(s => span / s <= maxLabels) ?? 50;
}
plot = addTooltips(
Plot.plot({
ariaLabel: "Line graph depicting trends in drug use by age group (ages 19-30, ages 35-50, ages 55-65) over time",
width: 900,
height: 700,
marginBottom: 50,
marginLeft: 50,
style: {
overflow: "visible",
fontSize: 12
},
symbol: {
domain: new Set(legendData.map((d) => d.age)),
range: [...new Set(legendData.map((d) => d.age))].map(symbol),
legend: true,
swatchSize: 23
},
color: {
domain: new Set(legendData.map((d) => d.age)),
range: [...new Set(legendData.map((d) => d.age))].map(color),
},
y: {
label: "Percentage (%)",
labelAnchor: "center",
domain: yscale
},
x: {
type: "time",
domain: xDomain,
interval: "year",
ticks: d3.timeYear.every(xTickStep),
tickFormat: "%Y",
label: "Years",
anchor: "bottom",
labelAnchor: "center"
},
marks: [
Plot.ruleY([0]),
Plot.dot(legendData, {
x: "year",
y: "estimate",
r: 4,
fill: "age",
symbol: "age",
title: (d) => `${d.age} \n ${formatter(d.year)}: ${d.estimate == null ? "No data" : d.estimate.toFixed(1) + "%"}`
}),
Plot.line(legendData, {
x: "year",
y: "estimate",
z: (d) => // This creates the line breaks
[
d.age,
d.flag
].join(),
stroke: "age"})
]
}),
{ fill: "age" }
)All Ages Heatmap
filter1 = radio.filter(d => d.age != "Ages 19-30")
filter2 = filter1.filter(d => d.age != "Ages 35-50" && Number.isFinite(d.estimate))
plot2 = addTooltips(Plot.plot({
marginTop: 50,
marginLeft: 100,
width: 1500,
height: 500,
grid: true,
x: {axis: "top", label: "Year", type: "band"},
y: {label: "Age", domain: ["Ages 19-20", "Ages 21-22", "Ages 23-24", "Ages 25-26", "Ages 27-28", "Ages 29-30", "Age 35", "Age 40", "Age 45", "Age 50", "Age 55", "Age 60"]},
color: {type: "linear", scheme: "BuRd", legend: true, label: "Drug Use Estimate"},
marks: [
Plot.cell(filter2, {x: "year2", y: "age", fill: "estimate", inset: 0.5, title: (d) => `${d.age} \n ${formatter(d.year)}: ${d.estimate}%`}),
Plot.text(filter2, {x: "year2", y: "age", text: "estimate", stroke: "white", strokeOpacity: 0.7, fill: "black", title: (d) => `${d.age} \n ${formatter(d.year)}: ${d.estimate}%`})
]
}),
{fill: "estimate"}
)Combined Sig Plot with Legend
comboPlot = html`
<div style="position: relative;">
${plot}
<div style="position: absolute; ${legendPlace} border: solid 1px black;">
${sigLegend}
</div>
</div>`