Documentation

Drug Prevalence Dashboard

Table Wide Format

Code
wideTable = html`
<style>
.branch {overflow-x:scroll; direction: rtl; margin-left:180px;  }
td:first-child, td:nth-child(2), th:first-child, th:nth-child(2) {position:absolute; width:100px; top:auto; white-space:normal;} 
td:nth-child(1), th:first-child  {left: 110px; white-space:nowrap; word-wrap: break-word; }
td:nth-child(2), th:nth-child(2) {left: 210px; word-wrap: break-word;} 
td, th { vertical-align: middle;  padding: .5em; color: black; }
th { text-decoration: underline; white-space:nowrap; }  
tr:nth-child(3) {
  background-color: #ADD8E6;
} 
table {
  direction: ltr;
  border: none;
  border-collapse: collapse;
  text-align: center;
}


/* On screens that are 1720px or less, do this for the table and citation size */

@media screen and (max-width: 1680px) {
  .branch {overflow-x:scroll; direction: rtl; margin-left:170px;  }
td:first-child, td:nth-child(2), th:first-child, th:nth-child(2) {position:absolute; width:100px; top:auto; white-space:normal;} 
td:nth-child(1), th:first-child  {left: 90px; white-space:nowrap; word-wrap: break-word; }
td:nth-child(2), th:nth-child(2) {left: 190px; word-wrap: break-word;}
tr:nth-child(3) {
  background-color: #ADD8E6;
}
}

/* On screens that are 1720px or less, do this for the table */

@media screen and (max-width: 1375px) {
  .branch {overflow-x:scroll; direction: rtl; margin-left:160px;  }
td:first-child, td:nth-child(2), th:first-child, th:nth-child(2) {position:absolute; width:100px; top:auto; white-space:normal;} 
td:nth-child(1), th:first-child  {left: 65px; white-space:nowrap; word-wrap: break-word; }
td:nth-child(2), th:nth-child(2) {left: 170px; word-wrap: break-word;}
tr:nth-child(3) {
  background-color: #ADD8E6;
}
}


/* On screens that are 992px or less, do this for the table and citation size */

@media screen and (max-width: 992px) {
  .branch {overflow-x:scroll; direction: rtl; margin-left:155px;  }
td:first-child, td:nth-child(2), th:first-child, th:nth-child(2) {position:absolute; width:100px; top:auto; white-space:normal;} 
td:nth-child(1), th:first-child  {left: 45px; white-space:nowrap; word-wrap: break-word; }
td:nth-child(2), th:nth-child(2) {left: 145px; word-wrap: break-word;}
tr:nth-child(3) {
  background-color: #ADD8E6;
}
}


/* On screens that are 600px or less, do this for table and citation size */
@media screen and (max-width: 600px) {
  .branch {overflow-x:scroll; direction: rtl; margin-left:145px;  }
td:first-child, td:nth-child(2), th:first-child, th:nth-child(2) {position:absolute; width:100px; top:auto; white-space:normal;} 
td:nth-child(1), th:first-child  {left: 25px; white-space:nowrap; word-wrap: break-word; }
td:nth-child(2), th:nth-child(2) {left: 115px; word-wrap: break-word;}
tr:nth-child(3) {
  background-color: #ADD8E6;
}
}
</style>
<table aria-label="${radio[0].drug}: Trends in ${radio[0].time} Prevalence of Use in Grades 8, 10, and 12">
  <tr>
    <th scope="col">Report Interval</th>
    <th scope="col">Grade</th>
    <th scope="col">1975</th>
    <th scope="col">1976</th>
    <th scope="col">1977</th>
    <th scope="col">1978</th>
    <th scope="col">1979</th>
    <th scope="col">1980</th>
    <th scope="col">1981</th>
    <th scope="col">1982</th>
    <th scope="col">1983</th>
    <th scope="col">1984</th>
    <th scope="col">1985</th>
    <th scope="col">1986</th>
    <th scope="col">1987</th>
    <th scope="col">1988</th>
    <th scope="col">1989</th>
    <th scope="col">1990</th>
    <th scope="col">1991</th>
    <th scope="col">1992</th>
    <th scope="col">1993</th>
    <th scope="col">1994</th>
    <th scope="col">1995</th>
    <th scope="col">1996</th>
    <th scope="col">1997</th>
    <th scope="col">1998</th>
    <th scope="col">1999</th>
    <th scope="col">2000</th>
    <th scope="col">2001</th>
    <th scope="col">2002</th>
    <th scope="col">2003</th>
    <th scope="col">2004</th>
    <th scope="col">2005</th>
    <th scope="col">2006</th>
    <th scope="col">2007</th>
    <th scope="col">2008</th>
    <th scope="col">2009</th>
    <th scope="col">2010</th>
    <th scope="col">2011</th>
    <th scope="col">2012</th>
    <th scope="col">2013</th>
    <th scope="col">2014</th>
    <th scope="col">2015</th>
    <th scope="col">2016</th>
    <th scope="col">2017</th>
    <th scope="col">2018</th>
    <th scope="col">2019<sup>ff</sup></th>
    <th scope="col">2020<sup>tt</sup></th>
    <th scope="col">2021</th>
    <th scope="col">2022</th>
    <th scope="col">2023</th>
    <th scope="col">2024</th>
    <th scope="col">2023-2024 <br> Change</th>
  </tr>
  <tr>
    <td>${htmlTables[0]?.time}</td>
    <td>${htmlTables[0]?.grade}</td>
    <td>${htmlTables[0]?._1975}</td>
    <td>${htmlTables[0]?._1976}</td>
    <td>${htmlTables[0]?._1977}</td>
    <td>${htmlTables[0]?._1978}</td>
    <td>${htmlTables[0]?._1979}</td>
    <td>${htmlTables[0]?._1980}</td>
    <td>${htmlTables[0]?._1981}</td>
    <td>${htmlTables[0]?._1982}</td>
    <td>${htmlTables[0]?._1983}</td>
    <td>${htmlTables[0]?._1984}</td>
    <td>${htmlTables[0]?._1985}</td>
    <td>${htmlTables[0]?._1986}</td>
    <td>${htmlTables[0]?._1987}</td>
    <td>${htmlTables[0]?._1988}</td>
    <td>${htmlTables[0]?._1989}</td>
    <td>${htmlTables[0]?._1990}</td>
    <td>${htmlTables[0]?._1991}</td>
    <td>${htmlTables[0]?._1992}</td>
    <td>${htmlTables[0]?._1993}</td>
    <td>${htmlTables[0]?._1994}</td>
    <td>${htmlTables[0]?._1995}</td>
    <td>${htmlTables[0]?._1996}</td>
    <td>${htmlTables[0]?._1997}</td>
    <td>${htmlTables[0]?._1998}</td>
    <td>${htmlTables[0]?._1999}</td>
    <td>${htmlTables[0]?._2000}</td>
    <td>${htmlTables[0]?._2001}</td>
    <td>${htmlTables[0]?._2002}</td>
    <td>${htmlTables[0]?._2003}</td>
    <td>${htmlTables[0]?._2004}</td>
    <td>${htmlTables[0]?._2005}</td>
    <td>${htmlTables[0]?._2006}</td>
    <td>${htmlTables[0]?._2007}</td>
    <td>${htmlTables[0]?._2008}</td>
    <td>${htmlTables[0]?._2009}</td>
    <td>${htmlTables[0]?._2010}</td>
    <td>${htmlTables[0]?._2011}</td>
    <td>${htmlTables[0]?._2012}</td>
    <td>${htmlTables[0]?._2013}</td>
    <td>${htmlTables[0]?._2014}</td>
    <td>${htmlTables[0]?._2015}</td>
    <td>${htmlTables[0]?._2016}</td>
    <td>${htmlTables[0]?._2017}</td>
    <td>${htmlTables[0]?._2018}</td>
    <td>${htmlTables[0]?._2019}</td>
    <td>${htmlTables[0]?._2020}</td>
    <td>${htmlTables[0]?._2021}</td>
    <td>${htmlTables[0]?._2022}</td>
    <td>${htmlTables[0]?._2023}</td>
    <td>${htmlTables[0]?._2024}</td>
    <td>${htmlTables[0]?.changeEstimate}</td>
  </tr>
  <tr>
    <td>${htmlTables[1]?.time}</td>
    <td>${htmlTables[1]?.grade}</td>
    <td>${htmlTables[1]?._1975}</td>
    <td>${htmlTables[1]?._1976}</td>
    <td>${htmlTables[1]?._1977}</td>
    <td>${htmlTables[1]?._1978}</td>
    <td>${htmlTables[1]?._1979}</td>
    <td>${htmlTables[1]?._1980}</td>
    <td>${htmlTables[1]?._1981}</td>
    <td>${htmlTables[1]?._1982}</td>
    <td>${htmlTables[1]?._1983}</td>
    <td>${htmlTables[1]?._1984}</td>
    <td>${htmlTables[1]?._1985}</td>
    <td>${htmlTables[1]?._1986}</td>
    <td>${htmlTables[1]?._1987}</td>
    <td>${htmlTables[1]?._1988}</td>
    <td>${htmlTables[1]?._1989}</td>
    <td>${htmlTables[1]?._1990}</td>
    <td>${htmlTables[1]?._1991}</td>
    <td>${htmlTables[1]?._1992}</td>
    <td>${htmlTables[1]?._1993}</td>
    <td>${htmlTables[1]?._1994}</td>
    <td>${htmlTables[1]?._1995}</td>
    <td>${htmlTables[1]?._1996}</td>
    <td>${htmlTables[1]?._1997}</td>
    <td>${htmlTables[1]?._1998}</td>
    <td>${htmlTables[1]?._1999}</td>
    <td>${htmlTables[1]?._2000}</td>
    <td>${htmlTables[1]?._2001}</td>
    <td>${htmlTables[1]?._2002}</td>
    <td>${htmlTables[1]?._2003}</td>
    <td>${htmlTables[1]?._2004}</td>
    <td>${htmlTables[1]?._2005}</td>
    <td>${htmlTables[1]?._2006}</td>
    <td>${htmlTables[1]?._2007}</td>
    <td>${htmlTables[1]?._2008}</td>
    <td>${htmlTables[1]?._2009}</td>
    <td>${htmlTables[1]?._2010}</td>
    <td>${htmlTables[1]?._2011}</td>
    <td>${htmlTables[1]?._2012}</td>
    <td>${htmlTables[1]?._2013}</td>
    <td>${htmlTables[1]?._2014}</td>
    <td>${htmlTables[1]?._2015}</td>
    <td>${htmlTables[1]?._2016}</td>
    <td>${htmlTables[1]?._2017}</td>
    <td>${htmlTables[1]?._2018}</td>
    <td>${htmlTables[1]?._2019}</td>
    <td>${htmlTables[1]?._2020}</td>
    <td>${htmlTables[1]?._2021}</td>
    <td>${htmlTables[1]?._2022}</td>
    <td>${htmlTables[1]?._2023}</td>
    <td>${htmlTables[1]?._2024}</td>
    <td>${htmlTables[1]?.changeEstimate}</td>
  </tr>
  <tr>
    <td>${htmlTables[2]?.time}</td>
    <td>${htmlTables[2]?.grade}</td>
    <td>${htmlTables[2]?._1975}</td>
    <td>${htmlTables[2]?._1976}</td>
    <td>${htmlTables[2]?._1977}</td>
    <td>${htmlTables[2]?._1978}</td>
    <td>${htmlTables[2]?._1979}</td>
    <td>${htmlTables[2]?._1980}</td>
    <td>${htmlTables[2]?._1981}</td>
    <td>${htmlTables[2]?._1982}</td>
    <td>${htmlTables[2]?._1983}</td>
    <td>${htmlTables[2]?._1984}</td>
    <td>${htmlTables[2]?._1985}</td>
    <td>${htmlTables[2]?._1986}</td>
    <td>${htmlTables[2]?._1987}</td>
    <td>${htmlTables[2]?._1988}</td>
    <td>${htmlTables[2]?._1989}</td>
    <td>${htmlTables[2]?._1990}</td>
    <td>${htmlTables[2]?._1991}</td>
    <td>${htmlTables[2]?._1992}</td>
    <td>${htmlTables[2]?._1993}</td>
    <td>${htmlTables[2]?._1994}</td>
    <td>${htmlTables[2]?._1995}</td>
    <td>${htmlTables[2]?._1996}</td>
    <td>${htmlTables[2]?._1997}</td>
    <td>${htmlTables[2]?._1998}</td>
    <td>${htmlTables[2]?._1999}</td>
    <td>${htmlTables[2]?._2000}</td>
    <td>${htmlTables[2]?._2001}</td>
    <td>${htmlTables[2]?._2002}</td>
    <td>${htmlTables[2]?._2003}</td>
    <td>${htmlTables[2]?._2004}</td>
    <td>${htmlTables[2]?._2005}</td>
    <td>${htmlTables[2]?._2006}</td>
    <td>${htmlTables[2]?._2007}</td>
    <td>${htmlTables[2]?._2008}</td>
    <td>${htmlTables[2]?._2009}</td>
    <td>${htmlTables[2]?._2010}</td>
    <td>${htmlTables[2]?._2011}</td>
    <td>${htmlTables[2]?._2012}</td>
    <td>${htmlTables[2]?._2013}</td>
    <td>${htmlTables[2]?._2014}</td>
    <td>${htmlTables[2]?._2015}</td>
    <td>${htmlTables[2]?._2016}</td>
    <td>${htmlTables[2]?._2017}</td>
    <td>${htmlTables[2]?._2018}</td>
    <td>${htmlTables[2]?._2019}</td>
    <td>${htmlTables[2]?._2020}</td>
    <td>${htmlTables[2]?._2021}</td>
    <td>${htmlTables[2]?._2022}</td>
    <td>${htmlTables[2]?._2023}</td>
    <td>${htmlTables[2]?._2024}</td>
    <td>${htmlTables[2]?.changeEstimate}</td>
  </tr>
</table>
`

In ObservableJS you can embed HTML, the html with the two escape characters allows you to write anything that you could put within an HTML. Here I use CSS in the beginning to style the table and to make it responsive to screen size changes. Then I list out all of the estimates by connecting them to the htmlTables dataset. When we search for new drugs via the search input or change the reporting interval via the radio buttons it updates the dataset. The estimates change dynamically because of the ${} which inject the newly updated estimates via javascript. I named the code cell wideTable to use later on.

Accordion Buttons

accordionButtons = 
html`
<div style="width: 90%" class="accordion accordion-flush" id="accordionFlushExample">
  <div class="accordion-item">
    <h2 class="accordion-header" id="flush-headingOne">
      <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapseOne" aria-expanded="false" aria-controls="flush-collapseOne">
        Table with Detailed Estimates
      </button>
    </h2>
    <div id="flush-collapseOne" class="accordion-collapse collapse" aria-labelledby="flush-headingOne" data-bs-parent="#accordionFlushExample">
      <div class="accordion-body">
              <div class="branch"><div>${wideTable}</div></div>
                <div>
                <p style="font-size: small; margin-bottom: 0px;">Note:
                      Level of significance: *=p<.05, **=p<.01, ***=p<.001. <br>
                      "." indicates data is not available. <br>
                      "&Dagger;" indicates that the question changed the following year. <br>
                      "&sect;" indicates insufficient data for that year. <br>
                      <a href="https://monitoringthefuture.org/data/bx-by/drug-prevalence/Footnotes.pdf">Footnotes (PDF)</a></p></div>
                      
          </div>
    </div>
  </div>
  <div class="accordion-item">
    <h2 class="accordion-header" id="flush-headingTwo">
      <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapseTwo" aria-expanded="false" aria-controls="flush-collapseTwo">
        Commentary
      </button>
    </h2>
    <div id="flush-collapseTwo" class="accordion-collapse collapse" aria-labelledby="flush-headingTwo" data-bs-parent="#accordionFlushExample">
      <div style="display: flex; justify-content: center; width:100%" class="accordion-body">${publishdoc}</div>
    </div>
  </div>
</div>`

Here I am again using the html tag to embed some accordianButtons. Because Quarto uses Bootstrap for styling, I just need to add the class and id tags for the buttons to inherit the button styles. In addition, I use ${wideTable} to place the reactive html table code and ${editdoc} to place the linked google doc within the accordian buttons so that we will see it when we click on the button. I also link to a dataset that has all of the footnote letter superscripts for the prevalence figures ${googledoclink[0].Superscript} and updates according to the drug.

Citation

citation = html`<div style="max-width: 750px;"><p style="font-size: small;">Miech, R. A., Johnston, L. D., Patrick, M. E., & O’Malley, P. M. (2025). Monitoring the Future national survey results on drug use, 1975–2024: 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 href="https://monitoringthefuture.org/results/annual-reports/">https://monitoringthefuture.org/results/annual-reports/</a></p></div>`

Here is the citation section that links to the monographs on the Monitoring the Future website.

Footnotes

footnotes = html`<p style="font-size: small;"><span style="color:#ff57f9; font-weight:500;">2023-2024 Change Level of Significance: *=p<.05, **=p<.01, ***=p<.001.</span> <br> <a href="https://monitoringthefuture.org/data/bx-by/drug-prevalence/Footnotes.pdf">Footnotes (PDF)</a></p>`

This is the footnote uses the same ${googledoclink[0].Superscript} to update the letter according to which drug is searched.

Commentary Data

drugCommentary = FileAttachment("drug-commentary.csv").csv()

googledoclink = drugCommentary.filter(
  ({ DrugName }) => DrugName.toLocaleLowerCase() === select.toLocaleLowerCase()
)

editdoc = html`<iframe width="100%" height="800px" src=${googledoclink[0].EditLink}></iframe>`
publishdoc = html`<iframe style="width:900px; height:1300px;"src=${googledoclink[0].PublishLink}></iframe>`

This is where the commentary dataset lives. It is set up to filter by DrugName via the search input. This houses the links to both the editable and the published google docs organized by drug name. In addition it has each footnote letter organized by drug name. I named each version of google doc so that I can easily switch between them.

Table Data

tables = FileAttachment("tabledata.csv").csv({ typed: true })
tables2 = FileAttachment("tabledata.csv").csv()

tablesDrug = tables.filter(
  ({ drug }) => drug.toLocaleLowerCase() === select.toLocaleLowerCase()
)
tablesDrug2 = tables2.filter(
  ({ drug }) => drug.toLocaleLowerCase() === select.toLocaleLowerCase()
)

tablesDrugTime = tablesDrug.filter(({ time }) => time === radio[0].time)
tablesDrugTime2 = tablesDrug2.filter(({ time }) => time === radio[0].time)

tablesDrug2
tablesDrugTime
tablesDrugTime2
htmlTables = tablesDrugTime2

htmlTables

This is the data in wide format for the HTML tables. It is filtered by both Drug name and by Time reporting interval. I had to do it twice because one needs to be read by the graph for the asterisks and one needs to be displayed in the HTML tables. For some reason (still not 100% sure why) in order for the negative numbers to show in the HTML tables it needs to be untyped.

Plot Data

prevalence = FileAttachment("plotdata.csv").csv({ typed: true })

prevalence
parser = d3.timeParse("%Y")

data = { // This creates new variables
  const subset = prevalence.map(
    ({ drug, time, grade, year, estimate, LBYear1, LBYear2 }) => ({
      drug: drug,
      time: time,
      grade: grade,
      year: parser(year),
      estimate: +estimate.toFixed(1),
      LBYear1: parser(LBYear1),
      LBYear2: parser(LBYear2)
    })
  );

  return subset.filter(
    ({ drug }) => drug.toLocaleLowerCase() === select.toLocaleLowerCase()
  );
}

data

This is the data for the Plot. I attach the .csv file and use .csv({ typed: true }) to try to have ObservableJS guess the data types of each of the variables. I do some data management to set it up to be read by Observable Plot the data visualization library. I name a time parser function parser = d3.timeParse("%Y") that I use later on to change the year variable from a number to a date year: parser(year) in addition I created two more variables to denote when there needs to be a line break LBYear1 and LBYear1. I coerce the estimate variable into a number and round it to the tenths place estimate: +estimate.toFixed(1). Then after I rewrite my variables I set the data to filter based on the drug name by the search input.

Radio Buttons

viewof radio = {
  const values = d3.group(data, (d) => d.time);
  return Inputs.radio(values, {
    key: values.has("12 Month")
      ? "12 Month"
      : values.keys().next().value
  });
}

radio

This is another input called radio it uses the plot data to extract the different time periods as options. This sets Last 12 Months as the default reporting interval. If there is no Last 12 Months value, then it selects the next value within that drug’s set of reporting intervals. The viewof allows me to use the filtered data by reporting interval which I will refer to later on.

Zoom-in Button

function buttonToggle({ 
  onText = "Zoom in",
  offText = "Zoom out",
  value = 0,
  click = (value, clicks) => clicks
} = {}) {
  let text = onText,
    ml = html`<button type="button" class="btn btn-quarto" >${text}</button>`,
    clicks = 0,
    v = value;

  ml.value = v;
  ml.onclick = () => {
    v = click(v, ++clicks);

    ml.value = v;
    ml.innerHTML = clicks % 2 === 0 ? onText : offText;
  };
  return ml;
}

max = Math.max(...radio.map((o) => o.estimate))

yscale = {
  if (zoomInButton % 2 == 0) {
    return [0, 100]; // If this statement is true, return this
  } else {
    return [0, max]; // If the second statement is true, return this
  }
}

viewof zoomInButton = buttonToggle()

This is a function to create the Zoom in button. It basically updates the Y Axis of the graph from the default of 100 as the max to whatever the max is of the current drug and reporting interval. The buttonToggle creates a value (either even or odd) when clicked that changes the name of the button and is used within the yscale if statement that returns either the full scale of 100 or that drug’s max estimate. To get the max estimate max = Math.max(...radio.map((o) => o.estimate)) It takes the radio dataset that is filtered by both drug name and time reporting interval and finds the max value via the Math.max() function bult-in to JavaScript. Then I use the viewof operator to be able to name the input and import it.

Download Data Button

downloadButton = (data, filename) => {
  
  let downloadData;
  downloadData = new Blob([d3.csvFormat(data)], { type: "text/csv" });
  const size = (downloadData.size / 1024).toFixed(0);
  const button = DOM.download(
    downloadData,
    filename,
    `Download ${filename} Dataset (~${size} KB)`
  );
  return button;
}

name = `${radio[0].drug} - ${radio[0].time}`

radioWithoutLineBreaks = radio.map(obj => {
  const {year, estimate, drug, grade, time} = obj;
  return {year, estimate, drug, grade, time};
});

downloadData = downloadButton(radioWithoutLineBreaks, name)

downloadButton is a function that takes an array of data as the first argument and the name you want to call the file as the second argument. It displays the filename and the size of the file in kilobytes.

Plot Title

title = html`<h3> ${radio[0].drug.toLocaleUpperCase()}: Trends in <u>${radio[0].time}</u> Prevalence of Use in ${gradeQuestion} </h3>`

gradeQuestion = gradeTitle.length > 1
  ? "8th, 10th, and 12th Grade"
  : "12th Grade"

radioSet = new Set(radio.map((d) => d.grade))

gradeTitle = [...radioSet]

This creates the title of the plot by reactively updating via the radio data set. First is the drug name ${radio[0].drug} then the reporting interval ${radio[0].time} then whether it is all grades or just 12th grade ${gradeQuestion}. That variable is just a ternary operator that figures out if there is more than one grade variable.

Rendered Plot

import {addTooltips} from "@mkfreeman/plot-tooltip"

formatter = d3.timeFormat("%Y")

color = d3.scaleOrdinal(
  ["8th Grade", "10th Grade", "12th Grade"],
  ["#59bbeb", "#6ac4a1", "#cca438"]
)

symbol = d3.scaleOrdinal(
  ["8th Grade", "10th Grade", "12th Grade"],
  ["circle", "square", "triangle"]
)

gradeOrder = ["8th Grade", "10th Grade", "12th Grade"];

radio.sort((a, b) => gradeOrder.indexOf(a.grade) - gradeOrder.indexOf(b.grade));
plot = addTooltips(
  Plot.plot({
    ariaLabel: "Drug Prevalence Line Charts",
    ariaDescription:
      "Drug Prevalence Chart showing estimates of use over time (1975 to 2024) in 8th, 10th, and 12th grade filtered by drug name and reporting interval. Currently showing ${radio[0].drug}",
    width: 900,
    height: 570,
    marginBottom: 50,
    style: {
      overflow: "visible",
      fontSize: 12
    },
    symbol: {
      domain: new Set(radio.map((d) => d.grade)),
      range: [...new Set(radio.map((d) => d.grade))].map(symbol),
      legend: true,
      swatchSize: 23
    },
    color: {
      domain: new Set(radio.map((d) => d.grade)),
      range: [...new Set(radio.map((d) => d.grade))].map(color)
    },
    y: {
      label: "Percentage (%)",
      labelAnchor: "center",
      domain: yscale
    },
    x: {
      type: "time",
      domain: [new Date("1974-01-01"), new Date("2024-01-01")],
      label: "Years",
      labelAnchor: "center"
    },
    marks: [
      Plot.ruleY([0]),
      Plot.dot(radio, {
        r: 5,
        x: "year",
        y: "estimate",
        symbol: "grade",
        fill: "grade",
        title: (d) => `${d.grade} \n ${formatter(d.year)}: ${d.estimate}%`
      }),
      Plot.line(radio, {
        x: "year",
        y: "estimate",
        z: (d) => // This creates the line breaks
          [
            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()
          ].join(),
        stroke: "grade"
      }),
      Plot.text(
        radio.filter((d) => d.grade === "8th Grade"),
        Plot.selectLast({
          x: "year",
          y: "estimate",
          text: (d) =>
            `${tablesDrugTime[0].plotSig === null ? " " : tablesDrugTime[0].plotSig}`, // This is the label that displays in the tooltip
          textAnchor: "start",
          fill: "#ff57f9",
          fontSize: 20,
          fontWeight: 800,
          dx: 10
        })
      ),
      Plot.text(
        radio.filter((d) => d.grade === "10th Grade"),
        Plot.selectLast({
          x: "year",
          y: "estimate",
          text: (d) =>
            `${tablesDrugTime[1].plotSig === null ? " " : tablesDrugTime[1].plotSig}`,
          textAnchor: "start",
          fill: "#ff57f9",
          fontSize: 20,
          fontWeight: 800,
          dx: 10
        })
      ),
      Plot.text(
        radio.filter((d) => d.grade === "12th Grade"),
        Plot.selectLast({
          x: "year",
          y: "estimate",
          text: (d) =>
            `${tablesDrugTime[2]?.plotSig == null ? " " : tablesDrugTime[2].plotSig}`,
          textAnchor: "start",
          fill: "#ff57f9",
          fontSize: 20,
          fontWeight: 800,
          dx: 10
        })
      )
    ]
  }),
  { fill: "grade" }
)

Here I import a tooltip via this notebook Observable has created new tooltips since I wrote this code. So I may go back and change it but for now I am still using the imported addTooltip() function. I create the color and symbol scales for the grades and name a function to format the year. Then I create three functions that check if the results of the significance test within the htmlTable dataset have astersisk. If they do then I display them in the graph. If they don’t nothing is displayed. For more on how the graph is made check out Observable Plot.

Diverging Bar Chart

sigdata = FileAttachment("sigbarchart.csv").csv({typed: true})
  .then(data => data.map((d) => ({...d, value: (d[2023] - d[2024]) / d[2024]})))

sigDrug = sigdata.filter(
  ({ drug }) => drug.toLocaleLowerCase() === select.toLocaleLowerCase()
)

sigDrugTime = sigDrug.filter(({ time }) => time === radio[0].time)

sigDrugTime
sigmax = Math.max(...sigdata.map((o) => o._1_Year_Chg))
sigmin = Math.min(...sigdata.map((o) => o._1_Year_Chg))

sigbarChart = Plot.plot({
  label: null,
  marginLeft: 100,
  marginRight: 100,
  marginTop: 50,
  style: {
    overflow: "visible",
    fontSize: 12
  },
  x: {
    axis: "top",
    label: "← decrease · 1-Year Change in Drug Use, 2023–2024 · increase →",
    labelAnchor: "center",
    domain: [sigmin, sigmax]
  },
  y: {
    domain: ["8th Grade", "10th Grade", "12th Grade"]
  },
  color: {
    scheme: "PiYG",
    type: "ordinal"
  },
  marks: [
    Plot.barX(sigDrugTime, {
      x: "_1_Year_Chg",
      y: "grade",
      fill: (d) => d._1_Year_Chg > 0
    }),
    Plot.gridX({ stroke: "white", strokeOpacity: 0.5 }),
    d3
      .groups(sigDrugTime, (d) => d._1_Year_Chg > 0)
      .map(([growth, grades]) => [
        Plot.axisY({
          x: 0,
          ticks: grades.map((d) => d.grade),
          tickSize: 0,
          anchor: growth ? "left" : "right"
        }),
        Plot.textX(grades, {
          x: "_1_Year_Chg",
          y: "grade",
          text: "_1_Year_Chg",
          textAnchor: growth ? "start" : "end",
          dx: growth ? 4 : -4,
        })
      ]),
    Plot.textX(tablesDrugTime, {
      x: "_1_Year_Chg",
      y: "grade",
      text: "plotSig"
    }),
    Plot.ruleX([0])
  ]
})