{"id":"a8f26d24229f7b55","slug":"predominant-cause-of-opioid-overdose-deaths-in-the-u-s","trashed":false,"description":"","likes":12,"publish_level":"live","forks":0,"fork_of":null,"has_importers":true,"update_time":"2020-02-11T20:19:42.689Z","first_public_version":2082,"paused_version":null,"publish_time":"2023-03-23T00:02:24.195Z","publish_version":2082,"latest_version":2082,"thumbnail":"9bb8a27d720c8d247760f620b6526849220faa511e61931170d42c08ec0f4875","default_thumbnail":"7c280532f2adea8a087812805873f7055b5a99fa4de07155b7ce7d99892ed794","roles":[],"sharing":null,"owner":{"id":"eb234b21118ba2f1","avatar_url":"https://avatars.observableusercontent.com/avatar/d3a33f280ffec5c8283a9aae244b575dced553a6f821571324a77d92f0ac5839","login":"aboutaaron","name":"Aaron Williams","bio":"data equity + viz + analysis ✊🏿 senior viz engineer @netflix ✊🏿 former investigative data reporter @washingtonpost ⚡advisory board @OpenNews","home_url":"https://www.acwx.net","type":"team","tier":"starter_2024"},"creator":{"id":"c8854dae9d7f94e1","avatar_url":"https://avatars.observableusercontent.com/avatar/d3a33f280ffec5c8283a9aae244b575dced553a6f821571324a77d92f0ac5839","login":"aboutaaron","name":"Aaron Williams","bio":"data equity + viz + analysis ✊🏿 senior viz engineer @netflix ✊🏿 former investigative data reporter @washingtonpost ⚡advisory board @OpenNews","home_url":"https://www.acwx.net","tier":"pro"},"authors":[{"id":"c8854dae9d7f94e1","avatar_url":"https://avatars.observableusercontent.com/avatar/d3a33f280ffec5c8283a9aae244b575dced553a6f821571324a77d92f0ac5839","name":"Aaron Williams","login":"aboutaaron","bio":"data equity + viz + analysis ✊🏿 senior viz engineer @netflix ✊🏿 former investigative data reporter @washingtonpost ⚡advisory board @OpenNews","home_url":"https://www.acwx.net","tier":"pro","approved":true,"description":""}],"collections":[],"files":[{"id":"56fe2ec7a48cb7a86b1d4e4a667a3da1853dfbad9fd36ea85d9bbf3d15756f0a2141c9d3f3ba985ba6dba1e42742a449a99d89e523d2c4c8cb0550ff822c7886","url":"https://static.observableusercontent.com/files/56fe2ec7a48cb7a86b1d4e4a667a3da1853dfbad9fd36ea85d9bbf3d15756f0a2141c9d3f3ba985ba6dba1e42742a449a99d89e523d2c4c8cb0550ff822c7886","download_url":"https://static.observableusercontent.com/files/56fe2ec7a48cb7a86b1d4e4a667a3da1853dfbad9fd36ea85d9bbf3d15756f0a2141c9d3f3ba985ba6dba1e42742a449a99d89e523d2c4c8cb0550ff822c7886?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27counties.json","name":"counties.json","create_time":"2020-01-08T18:24:22.751Z","mime_type":"application/json","status":"public","size":917073,"content_encoding":"gzip","private_bucket_id":null},{"id":"57ac43015b11a5531914f88db811e847e5e3393dbc021886208098bf55efa69310005e3b3eed3fd4151a7175a7843c58ccaddeab65d5684b6eb832d3fc1b81d7","url":"https://static.observableusercontent.com/files/57ac43015b11a5531914f88db811e847e5e3393dbc021886208098bf55efa69310005e3b3eed3fd4151a7175a7843c58ccaddeab65d5684b6eb832d3fc1b81d7","download_url":"https://static.observableusercontent.com/files/57ac43015b11a5531914f88db811e847e5e3393dbc021886208098bf55efa69310005e3b3eed3fd4151a7175a7843c58ccaddeab65d5684b6eb832d3fc1b81d7?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27annotations.json","name":"annotations.json","create_time":"2020-01-08T19:07:01.799Z","mime_type":"application/json","status":"public","size":825,"content_encoding":"gzip","private_bucket_id":null},{"id":"6cd5b5f8580ae9ca28564c3b98abd4c073ab6bf98a7be972ab76b6e236a03cc2cfcc4890b85663208eac57f4e25729ff37d5b03342ef4d5e3c968b83240ea7f2","url":"https://static.observableusercontent.com/files/6cd5b5f8580ae9ca28564c3b98abd4c073ab6bf98a7be972ab76b6e236a03cc2cfcc4890b85663208eac57f4e25729ff37d5b03342ef4d5e3c968b83240ea7f2","download_url":"https://static.observableusercontent.com/files/6cd5b5f8580ae9ca28564c3b98abd4c073ab6bf98a7be972ab76b6e236a03cc2cfcc4890b85663208eac57f4e25729ff37d5b03342ef4d5e3c968b83240ea7f2?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27countyLookup.json","name":"countyLookup.json","create_time":"2020-01-08T19:09:22.719Z","mime_type":"application/json","status":"public","size":296,"content_encoding":"gzip","private_bucket_id":null}],"comments":[],"commenting_lock":null,"suggestion_from":null,"suggestions_to":[],"version":2082,"title":"Predominant cause of opioid overdose deaths in the U.S.","license":null,"copyright":"","nodes":[{"id":0,"value":"md`# Predominant cause of opioid overdose deaths in the U.S.\n\nExclusive data – obtained by The Washington Post from the Centers for Disease Control and Prevention – shows how individual counties moved through the three waves of the opioid crisis: <span style=\"font-family: Helvetica; font-weight: bold; border-bottom: 3px solid #1a94a9;\">prescription pills</span>, <span style=\"font-family: Helvetica; font-weight: bold; border-bottom: 3px solid #9c143f;\">heroin</span> and <span style=\"font-family: Helvetica; font-weight: bold; border-bottom: 3px solid #fbbe4a;\">fentanyl</span>. This graphic illustrates how the opioid responsible for the majority of overdose deaths shifted from 2011 to 2017 in more than 1,300 U.S. counties.\n\n<small>Note: For some years, there was <span style=\"font-family: Helvetica; font-weight: bold; border-bottom: 3px solid #d5d5d5;\">no dominant opioid</span> driving deaths.</small>`","pinned":false,"mode":"js","data":null,"name":null},{"id":7,"value":"chart = {\n  const svg = d3.select(DOM.svg(width, width));\n\n  svg\n    .append('rect')\n    .attr('width', width)\n    .attr('height', width)\n    .attr('fill', '#1A1A1A');\n\n  const g = svg\n    .append('g')\n    .attr('transform', `translate(${margin.left}, ${margin.top})`);\n\n  const axis = g.append('g').attr('id', 'axis');\n\n  axis.append('g').call(yAxis);\n  axis.append('g').call(topAxis);\n\n  const county = g\n    .selectAll('g.county')\n    .data(counties)\n    .join('g');\n\n  county.each((d, i, nodes) => {\n    const colorId = DOM.uid(\"color\");\n    const group = d3.select(nodes[i]);\n\n    /* \n      Each county needs a unique linearGradient to create the effect.\n      The opacity scale logic is defined in the `makeScale` function.\n      \n      We're adjusting the opacity based on several variables including:\n      \n      1. Are we showing every county? If so, use the full color spectrum \n         but lower the opacity to emphasize dominate years.\n         \n      2. Are we just showing a single county? If so, raise the opacity and \n         stroke width of that path but reduce stroke width of all other paths, \n         make the opacity super low, and change the color to gray.\n    */\n\n    const opacityScale = makeScale(countiesSelect, d.key);\n\n    group\n      .append(\"linearGradient\")\n      .attr(\"id\", colorId.id)\n      .attr(\"gradientUnits\", \"userSpaceOnUse\")\n      .attr(\"gradientTransform\", ({ key }) => {\n        // I'm using translate here to move the gradient up on the coordinate system\n        // because sometimes the transition from one color to another\n        // doesn't correlate with the axes defined by d3\n        // Read more here: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient\n        return key === \"54003\" ? `translate(0, -90)` : `translate(0, -150)`;\n      })\n      .attr(\"x1\", 0)\n      .attr(\"x2\", 0)\n      .attr(\"y1\", 0)\n      .attr(\"y2\", \"100%\")\n      .selectAll(\"stop\")\n      .data(\n        d.values.filter(d =>\n          isEmpty(countiesSelect) || countiesSelect === d.countyfips\n            ? true\n            : [2011, 2014, 2017].includes(d.year)\n        )\n      )\n      .join(\"stop\")\n      .attr(\"offset\", d => y(d.year) / height)\n      .attr(\"stop-color\", d =>\n        isEmpty(countiesSelect) || countiesSelect === d.countyfips\n          ? getColor(d.group)\n          : '#cecece'\n      )\n      .attr(\"stop-opacity\", d => round(opacityScale[d.group](d[d.group])));\n\n    group\n      .append('path')\n      .attr('fill', 'none')\n      .attr('stroke', colorId)\n      .attr('stroke-width', d =>\n        isEmpty(countiesSelect) ? 1.5 : countiesSelect === d.key ? 3 : 0.5\n      )\n      .attr(\"stroke-linejoin\", \"round\")\n      .attr(\"stroke-linecap\", \"round\")\n      .attr(\"d\", line(d.values.filter(line.defined())));\n  });\n\n  const labels = svg.append('g').attr('class', 'labels');\n\n  labels\n    .selectAll('text.county')\n    .data(counties.filter(d => Object.keys(countyLookup).includes(d.key)))\n    .join('text')\n    .attr(\n      `transform`,\n      _ => `translate(${height - margin.left * 2.5}, ${y.step() / 1.10})`\n    )\n    .attr(\"opacity\", d =>\n      isEmpty(countiesSelect) || countiesSelect !== d.key ? 0 : 0.9\n    )\n    .text(d => {\n      const label = countyLookup[d.key];\n      if (label) {\n        return d.key === \"11001\"\n          ? label.county.replace(\"Of\", \"of\")\n          : `${label.county}, ${getAPState(d.key)}`;\n      }\n    })\n    .style('font-size', \"1em\")\n    .style('fill', \"#d5d5d5\")\n    .style('font-family', '\"FranklinITCProLight\",Helvetica,Arial,sans-serif')\n    .style('text-transform', 'uppercase')\n    .style('font-weight', 'bold');\n\n  return svg.node();\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1331,"value":"viewof countiesSelect = radio({\n  title: 'Filter by',\n  options: [\n    { label: 'Show all', value: '' },\n    ...Object.keys(countyLookup).map(f => ({\n      label:\n        f === \"11001\"\n          ? countyLookup[f].county.replace(\"Of\", \"of\")\n          : `${countyLookup[f].county}, ${getAPState(f)}`,\n      value: f\n    }))\n  ],\n  value: '',\n  description: 'Note: this will take a second to re-render...'\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":310,"value":"md`\n\n\n---\nThis graphic was created for the _Washington Post_ story, [How the opioid epidemic evolved](https://www.washingtonpost.com/graphics/2019/investigations/opioid-pills-overdose-analysis/), published on Dec. 23, 2019.\n\n---\n### Appendix`","pinned":false,"mode":"js","data":null,"name":null},{"id":640,"value":"height = width - margin.top - margin.bottom","pinned":false,"mode":"js","data":null,"name":null},{"id":225,"value":"margin = ({ left: 50, right: 50, top: 50, bottom: 50 })","pinned":false,"mode":"js","data":null,"name":null},{"id":645,"value":"x = d3\n  .scaleBand()\n  .domain(labels)\n  .range([0, height])","pinned":false,"mode":"js","data":null,"name":null},{"id":694,"value":"y = d3\n  .scalePoint()\n  .domain(d3.range(2011, 2018).reverse())\n  .range([height, 0])","pinned":false,"mode":"js","data":null,"name":null},{"id":696,"value":"yAxis = g => {\n  g.attr(\"class\", \"axis\")\n    .attr(\"id\", \"left\")\n    .call(\n      d3\n        .axisLeft(y)\n        .tickSizeOuter(0)\n        .tickSize(-width)\n        .tickFormat(d => ([2011, 2014, 2017].includes(d) ? d : \"\"))\n    );\n\n  g.select(\".domain\").remove();\n  g.selectAll(\".tick line\")\n    .attr(\"stroke\", \"#cecece\")\n    .attr(\"stroke-opacity\", 0.25);\n\n  g.selectAll('text')\n    .style('font-size', \"1.25em\")\n    .style('fill', \"#d5d5d5\")\n    .style('font-family', '\"FranklinITCProLight\",Helvetica,Arial,sans-serif')\n    .style('text-transform', 'uppercase');\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1493,"value":"topAxis = g => {\n  const dict = { a: \"pills\", b: \"heroin\", c: \"fentanyl\" };\n  g.attr(\"class\", \"axis\")\n    .attr(\"id\", \"top\")\n    .call(d3.axisTop(x).tickFormat(d => dict[d]));\n\n  g.select(\".domain\").remove();\n  g.selectAll(\".tick text\").attr(\"y\", -18);\n  g.selectAll(\".tick line\").remove();\n  g.selectAll('text')\n    .style('font-size', \"1.25em\")\n    .style('fill', \"#d5d5d5\")\n    .style('font-family', '\"FranklinITCProLight\",Helvetica,Arial,sans-serif')\n    .style('text-transform', 'uppercase');\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":778,"value":"line = d3\n  .line()\n  .defined(d => labels.includes(d.group))\n  .x(d => x(d.group) + x.bandwidth() / 2)\n  .y(d => y(d.year))\n  .curve(d3.curveMonotoneX)","pinned":false,"mode":"js","data":null,"name":null},{"id":1141,"value":"getColor = group =>\n  group === 'a'\n    ? '#1a94a9'\n    : group === 'b'\n    ? '#9c143f'\n    : group === 'c'\n    ? '#fbbe4a'\n    : group === 'd'\n    ? '#8b8b8b'\n    : 'transparent'","pinned":false,"mode":"js","data":null,"name":null},{"id":622,"value":"labels = ['a', 'b', 'c']","pinned":false,"mode":"js","data":null,"name":null},{"id":1509,"value":"counties = FileAttachment(\"counties.json\").json()","pinned":false,"mode":"js","data":null,"name":null},{"id":1602,"value":"countyLookup = FileAttachment('countyLookup.json').json()","pinned":false,"mode":"js","data":null,"name":null},{"id":1690,"value":"isEmpty = value => (value === \"\" ? true : false)","pinned":false,"mode":"js","data":null,"name":null},{"id":1434,"value":"round = (d, precision = 4) =>\n  Number(Math.round(d + 'e' + precision) + 'e-' + precision)","pinned":false,"mode":"js","data":null,"name":null},{"id":1463,"value":"createStopOpacityScale = (opacity = [0, 0.25]) => {\n  const groups = d3\n    .nest()\n    .key(d => d.group)\n    .entries(_.flatten(counties.map(d => d.values)))\n    .map(({ key, values }) => [key, d3.max(values.map(d => d[key]))]);\n\n  const scales = groups.reduce((obj, [drug, maxValue]) => {\n    // produce an object of d3.scaleLinear based on max death rate\n    // of each drug in dataset\n\n    if (drug === \"null\") {\n      obj[drug] = () => 0;\n    } else if (drug === \"d\") {\n      obj[drug] = () => 0.25;\n    } else {\n      // linear scale for each drug\n      obj[drug] = d3\n        .scaleLinear()\n        .domain([0, maxValue])\n        .range([opacity[0], opacity[1]]);\n    }\n    return obj;\n  }, {});\n\n  return scales;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1694,"value":"makeScale = (countyfips, key) =>\n  isEmpty(countyfips)\n    ? createStopOpacityScale() // show all\n    : countyfips === key\n    ? createStopOpacityScale([0.75, 1]) // selected\n    : createStopOpacityScale([0, 0.02]) // muted","pinned":false,"mode":"js","data":null,"name":null},{"id":1762,"value":"getAPState = key => us.lookup(key.substring(0, 2)).ap_abbr","pinned":false,"mode":"js","data":null,"name":null},{"id":1862,"value":"md`---`","pinned":false,"mode":"js","data":null,"name":null},{"id":1859,"value":"makeAnnotations = _ =>\n  d3\n    .annotation()\n    .type(d3.annotationCalloutCircle)\n    .disable([\"connector\"])\n    .accessors({\n      x: d => x(d.group),\n      y: d => y(d.year)\n    })\n    .accessorsInverse({\n      group: d => x.invert(d.x),\n      year: d => y.invert(d.y)\n    })\n    .annotations(annotations)","pinned":false,"mode":"js","data":null,"name":null},{"id":1855,"value":"annotations = {\n  // create annotations for graphic\n  const selected = annotationsText.map(d => {\n    const anno = Object.assign({}, d, { year: +d.year });\n    const [obj = { values: [] }] = counties.filter(\n      v => v.key === anno.countyfips\n    );\n    const { values } = obj;\n    const [filtered = {}] = values.filter(v => v.year === anno.year);\n    const merged = { ...filtered, ...anno };\n    return merged;\n  });\n  const formatted = selected.map(formatAnnotation);\n  return formatted.filter(({ data: { countyfips } }) =>\n    isEmpty(countiesSelect)\n      ? countyfips === 'default'\n      : countiesSelect === countyfips\n  );\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1851,"value":"formatAnnotation = d => {\n  const createNote = d => {\n    const { text } = d;\n    let obj = { label: text };\n\n    return obj;\n  };\n\n  return {\n    // create annotation object\n    id: `anno-${d.countyfips}`,\n    note: createNote(d),\n    data: d,\n    className: `pg-annotation anno-${d.countyfips}`,\n    dx: 20,\n    dy: 5\n  };\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1848,"value":"annotationsText = FileAttachment('annotations.json').json()","pinned":false,"mode":"js","data":null,"name":null},{"id":1930,"value":"drawAnnotations = (annotator, target) => {\n  // This assigns the annotations after the SVG is rendered to the DOM\n  chart;\n  const svg = d3.select(target);\n  const group = svg.selectAll('.annotation-group');\n  if (group.nodes().length > 0) {\n    group.remove();\n  }\n\n  svg\n    .append('g')\n    .attr('class', 'annotation-group')\n    .call(annotator())\n    .call(adjustAnnotations);\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1909,"value":"adjustAnnotations = g => {\n  const anno = g.selectAll(\".pg-annotation\");\n  anno.attr(\n    \"transform\",\n    d => `translate(${d._x + y.step() + margin.left}, ${d._y + margin.top})`\n  );\n\n  g.selectAll(\".note-line\").remove();\n  anno\n    .selectAll(\".subject\")\n    .attr(\"stroke\", \"white\")\n    .attr('stroke-width', 1)\n    .attr(\"stroke-opacity\", 1);\n\n  anno\n    .selectAll('text')\n    .style('font-size', \"16px\")\n    .style('fill', \"#ffffff\")\n    .style('font-family', '\"FranklinITCProLight\",Helvetica,Arial,sans-serif');\n\n  g.select(\".anno-default\").style(\"opacity\", 1);\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":2051,"value":"md`---\n\n[d3-annotation](https://github.com/susielu/d3-annotation) needs the svg to exist before adding annotations, so, we need to call a function to render after Observable has created the chart in the DOM. \n\n[See this GitHub issue](https://github.com/susielu/d3-legend/issues/81#issuecomment-373117278) for more information.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1955,"value":"drawAnnotations(makeAnnotations, chart)","pinned":true,"mode":"js","data":null,"name":null},{"id":208,"value":"md`---\n\n#### Libraries`","pinned":false,"mode":"js","data":null,"name":null},{"id":80,"value":"_ = require('lodash')","pinned":false,"mode":"js","data":null,"name":null},{"id":1759,"value":"us = require(\"us\")","pinned":false,"mode":"js","data":null,"name":null},{"id":9,"value":"d3 = require('d3-selection', 'd3-scale', 'd3-axis', 'd3-array', 'd3-shape', 'd3-collection', 'd3-svg-annotation@2.5.1/d3-annotation.js')","pinned":false,"mode":"js","data":null,"name":null},{"id":136,"value":"import { radio } from \"@jashkenas/inputs\"","pinned":false,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}