{"id":"4f6a735dbc0faffe","slug":"bar-chart-race","trashed":false,"description":"","likes":3,"publish_level":"public","forks":1,"fork_of":{"id":"3ff9fa2c6593d814","slug":"bar-chart-race","title":"Bar Chart Race","owner":{"id":"863951e3ebe4c0ae","avatar_url":"https://avatars.observableusercontent.com/avatar/5af16e327a90b2873351dda8a596c0d2d3bf954f64523deefe80177c9764d0f7","login":"d3","name":"D3","bio":"Bring your data to life.","home_url":"https://d3js.org","type":"team","tier":"pro_2024"},"version":3012},"has_importers":false,"update_time":"2021-01-19T21:03:16.515Z","first_public_version":null,"paused_version":null,"publish_time":"2019-11-12T15:45:04.467Z","publish_version":3100,"latest_version":3100,"thumbnail":"90a6aaee9bbd1c7a9a952dce9f20d96965a0b86d1f3212571f09276caf193031","default_thumbnail":"90a6aaee9bbd1c7a9a952dce9f20d96965a0b86d1f3212571f09276caf193031","roles":[],"sharing":null,"owner":{"id":"f33497ce92520cec","avatar_url":"https://avatars.observableusercontent.com/avatar/5b8a2835a7cc19777e6773556b4d0a7212536edde99fdb799e83767188dde40c","login":"connorrothschild","name":"Connor Rothschild","bio":"","home_url":"http://www.connorrothschild.com","type":"team","tier":"starter_2024"},"creator":{"id":"e7db9d8898b24f63","avatar_url":"https://avatars.observableusercontent.com/avatar/5b8a2835a7cc19777e6773556b4d0a7212536edde99fdb799e83767188dde40c","login":"connorrothschild","name":"Connor Rothschild","bio":"","home_url":"http://www.connorrothschild.com","tier":"public"},"authors":[{"id":"e7db9d8898b24f63","avatar_url":"https://avatars.observableusercontent.com/avatar/5b8a2835a7cc19777e6773556b4d0a7212536edde99fdb799e83767188dde40c","name":"Connor Rothschild","login":"connorrothschild","bio":"","home_url":"http://www.connorrothschild.com","tier":"public","approved":true,"description":""}],"collections":[],"files":[{"id":"4b57146ce04606f01c835eab2d1386e021267c38370ea50d6f400a90b1fd21cc4bddcc4341d460c75b2bb36c953164084b2a18003be286100587b840c2e5fe3c","url":"https://static.observableusercontent.com/files/4b57146ce04606f01c835eab2d1386e021267c38370ea50d6f400a90b1fd21cc4bddcc4341d460c75b2bb36c953164084b2a18003be286100587b840c2e5fe3c","download_url":"https://static.observableusercontent.com/files/4b57146ce04606f01c835eab2d1386e021267c38370ea50d6f400a90b1fd21cc4bddcc4341d460c75b2bb36c953164084b2a18003be286100587b840c2e5fe3c?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27cable_weekly%25405.csv","name":"cable_weekly@5.csv","create_time":"2019-11-12T16:05:49.273Z","mime_type":"text/csv","status":"public","size":117954,"content_encoding":"gzip","private_bucket_id":null},{"id":"8ae68c422ff0e26917ba375dbbcf329e48a79fe61e6220b55100429b42278d17f8e229019132356858644edfdebd2a3eb6aafe19e3271acca2aad3975daf9f97","url":"https://static.observableusercontent.com/files/8ae68c422ff0e26917ba375dbbcf329e48a79fe61e6220b55100429b42278d17f8e229019132356858644edfdebd2a3eb6aafe19e3271acca2aad3975daf9f97","download_url":"https://static.observableusercontent.com/files/8ae68c422ff0e26917ba375dbbcf329e48a79fe61e6220b55100429b42278d17f8e229019132356858644edfdebd2a3eb6aafe19e3271acca2aad3975daf9f97?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27cable_weekly%25406.csv","name":"cable_weekly@6.csv","create_time":"2019-11-12T16:54:30.208Z","mime_type":"text/csv","status":"public","size":121975,"content_encoding":"gzip","private_bucket_id":null}],"comments":[],"commenting_lock":null,"suggestion_from":null,"suggestions_to":[],"version":3100,"title":"The Race for Media Attention","license":null,"copyright":"","nodes":[{"id":239,"value":"md`# The Race for Media Attention\n\nThis chart animates the number of media mentions Democratic candidates received from Fox News, CNN, and MSNBC throughout 2019. \\n\nIt is forked from Mike Bostock's [original bar chart race](https://observablehq.com/@d3/bar-chart-race-explained). \\n \nData: [FiveThirtyEight](https://github.com/fivethirtyeight/data/tree/master/media-mentions-2020)`","pinned":false,"mode":"js","data":null,"name":null},{"id":982,"value":"viewof replay = html`<button>Replay`","pinned":false,"mode":"js","data":null,"name":null},{"id":0,"value":"chart = {\n  replay;\n\n  const svg = d3.create(\"svg\")\n      .attr(\"viewBox\", [0, 0, width, height]);\n\n  const updateBars = bars(svg);\n  const updateAxis = axis(svg);\n  const updateLabels = labels(svg);\n  const updateTicker = ticker(svg);\n\n  yield svg.node();\n\n  for (const keyframe of keyframes) {\n    const transition = svg.transition()\n        .duration(duration)\n        .ease(d3.easeLinear);\n\n    // Extract the top bar’s value.\n    x.domain([0, keyframe[1][0].matched_clips]);\n\n    updateAxis(keyframe, transition);\n    updateBars(keyframe, transition);\n    updateLabels(keyframe, transition);\n    updateTicker(keyframe, transition);\n\n    invalidation.then(() => svg.interrupt());\n    await transition.end();\n  }\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":1530,"value":"function bars(svg) {\n  let bar = svg.append(\"g\")\n      .attr(\"fill-opacity\", 0.6)\n    .selectAll(\"rect\");\n\n  return ([date, data], transition) => bar = bar\n    .data(data.slice(0, n), d => d.name)\n    .join(\n      enter => enter.append(\"rect\")\n        .attr(\"fill\", color)\n        .attr(\"height\", y.bandwidth())\n        .attr(\"x\", x(0))\n        .attr(\"y\", d => y((prev.get(d) || d).rank))\n        .attr(\"width\", d => x((prev.get(d) || d).matched_clips) - x(0)),\n      update => update,\n      exit => exit.transition(transition).remove()\n        .attr(\"y\", d => y((next.get(d) || d).rank))\n        .attr(\"width\", d => x((next.get(d) || d).matched_clips) - x(0))\n    )\n    .call(bar => bar.transition(transition)\n      .attr(\"y\", d => y(d.rank))\n      .attr(\"width\", d => x(d.matched_clips) - x(0)));\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":1538,"value":"function labels(svg) {\n  let label = svg.append(\"g\")\n      .style(\"font\", \"bold 12px var(--sans-serif)\")\n      .style(\"font-variant-numeric\", \"tabular-nums\")\n      .attr(\"text-anchor\", \"end\")\n    .selectAll(\"text\");\n\n  return ([date, data], transition) => label = label\n    .data(data.slice(0, n), d => d.name)\n    .join(\n      enter => enter.append(\"text\")\n        .attr(\"transform\", d => `translate(${x((prev.get(d) || d).matched_clips)},${y((prev.get(d) || d).rank)})`)\n        .attr(\"y\", y.bandwidth() / 2)\n        .attr(\"x\", -3)\n        .attr(\"dy\", \"-0.25em\")\n        .text(d => d.name)\n        .call(text => text.append(\"tspan\")\n          .attr(\"fill-opacity\", 0.7)\n          .attr(\"font-weight\", \"normal\")\n          .attr(\"x\", -3)\n          .attr(\"dy\", \"1.15em\")),\n      update => update,\n      exit => exit.transition(transition).remove()\n        .attr(\"transform\", d => `translate(${x((next.get(d) || d).matched_clips)},${y((next.get(d) || d).rank)})`)\n        .call(g => g.select(\"tspan\").tween(\"text\", d => textTween(d.matched_clips, (next.get(d) || d).matched_clips)))\n    )\n    .call(bar => bar.transition(transition)\n      .attr(\"transform\", d => `translate(${x(d.matched_clips)},${y(d.rank)})`)\n      .call(g => g.select(\"tspan\").tween(\"text\", d => textTween((prev.get(d) || d).matched_clips, d.matched_clips))))\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1552,"value":"function axis(svg) {\n  const g = svg.append(\"g\")\n      .attr(\"transform\", `translate(0,${margin.top})`);\n\n  const axis = d3.axisTop(x)\n      .ticks(width / 160)\n      .tickSize(-barSize * (n + y.padding()));\n\n  return (_, transition) => {\n    g.transition(transition).call(axis);\n    g.selectAll(\".tick:not(:first-of-type) line\").attr(\"stroke\", \"white\");\n    g.select(\".domain\").remove();\n  };\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1555,"value":"function ticker(svg) {\n  const now = svg.append(\"text\")\n      .style(\"font\", `bold ${barSize}px var(--sans-serif)`)\n      .style(\"font-variant-numeric\", \"tabular-nums\")\n      .attr(\"text-anchor\", \"end\")\n      .attr(\"x\", width - 6)\n      .attr(\"y\", margin.top + barSize * (n - .6))\n      .attr(\"dy\", \"0.32em\")\n      .text(formatDate(keyframes[0][0]));\n\n  const ticker = svg.append(\"line\")\n      .attr(\"transform\", `translate(${width - margin.right - tickerSize},${margin.top + barSize * n})`)\n      .attr(\"x2\", tickerSize)\n      .attr(\"stroke\", \"#ddd\")\n    .clone()\n      .attr(\"stroke\", \"#000\");\n\n  let t0 = 0;\n  return ([date], transition) => {\n    const t1 = tickerProgress(date);\n    const i = d3.interpolateNumber(t0, t1 < t0 ? t1 + 1 : t1);\n    t0 = t1;\n    ticker.transition(transition).attrTween(\"x2\", () => t => i(t) % 1 * tickerSize);\n    transition.end().then(() => now.text(formatDate(date)));\n  };\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":408,"value":"duration = 100","pinned":true,"mode":"js","data":null,"name":null},{"id":411,"value":"n = 12","pinned":true,"mode":"js","data":null,"name":null},{"id":144,"value":"data = d3.csvParse(await FileAttachment(\"cable_weekly@6.csv\").text(), d3.autoType)","pinned":true,"mode":"js","data":null,"name":null},{"id":2135,"value":"names = new Set(data.map(d => d.name))","pinned":true,"mode":"js","data":null,"name":null},{"id":880,"value":"datevalues = Array.from(d3.rollup(data, ([d]) => d.matched_clips, d => d.date, d => d.name))\n  .map(([date, data]) => [new Date(date), data])\n  .sort(([a], [b]) => d3.ascending(a, b))","pinned":true,"mode":"js","data":null,"name":null},{"id":862,"value":"function rank(matched_clips) {\n  const data = Array.from(names, name => ({name, matched_clips: matched_clips(name) || 0}));\n  data.sort((a, b) => d3.descending(a.matched_clips, b.matched_clips));\n  for (let i = 0, n = data.length; i < n; ++i) data[i].rank = i;\n  return data;\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":757,"value":"keyframes = {\n  const keyframes = [];\n  let ka, a, kb, b;\n  for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {\n    for (let i = 0; i < k; ++i) {\n      const t = i / k;\n      keyframes.push([\n        new Date(ka * (1 - t) + kb * t),\n        rank(name => a.get(name) * (1 - t) + b.get(name) * t)\n      ]);\n    }\n  }\n  keyframes.push([new Date(kb), rank(name => b.get(name))]);\n  return keyframes;\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":830,"value":"k = 10","pinned":true,"mode":"js","data":null,"name":null},{"id":938,"value":"nameframes = d3.groups(keyframes.flatMap(([, data]) => data), d => d.name)","pinned":true,"mode":"js","data":null,"name":null},{"id":719,"value":"prev = new Map(nameframes.flatMap(([, data]) => d3.pairs(data, (a, b) => [b, a])))","pinned":true,"mode":"js","data":null,"name":null},{"id":734,"value":"next = new Map(nameframes.flatMap(([, data]) => d3.pairs(data)))","pinned":true,"mode":"js","data":null,"name":null},{"id":1122,"value":"function textTween(a, b) {\n  const i = d3.interpolateNumber(a, b);\n  return function(t) {\n    this.textContent = formatNumber(i(t));\n  };\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":1142,"value":"formatNumber = d3.format(\",d\")","pinned":true,"mode":"js","data":null,"name":null},{"id":2579,"value":"tickerProgress = date => {\n  const y0 = d3.utcMonth(date);\n  const y1 = d3.utcMonth.offset(y0);\n  return (date - y0) / (y1 - y0);\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":1287,"value":"formatDate = d3.utcFormat(\"%B\")","pinned":true,"mode":"js","data":null,"name":null},{"id":2049,"value":"tickerSize = 200 // The width of formatDate.","pinned":true,"mode":"js","data":null,"name":null},{"id":516,"value":"color = {\n  const scale = d3.scaleOrdinal(d3.schemeTableau10);\n  if (data.some(d => d.category !== undefined)) {\n    const categoryByName = new Map(data.map(d => [d.name, d.category]))\n    scale.domain(Array.from(categoryByName.values()));\n    return d => scale(categoryByName.get(d.name));\n  }\n  return d => scale(d.name);\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":638,"value":"x = d3.scaleLinear([0, 1], [margin.left, width - margin.right])","pinned":true,"mode":"js","data":null,"name":null},{"id":542,"value":"y = d3.scaleBand()\n    .domain(d3.range(m))\n    .range([margin.top, margin.top + barSize * (m + 0.1)])\n    .padding(0.1)","pinned":true,"mode":"js","data":null,"name":null},{"id":2463,"value":"m = names.size","pinned":true,"mode":"js","data":null,"name":null},{"id":1971,"value":"height = margin.top + barSize * n + margin.bottom","pinned":true,"mode":"js","data":null,"name":null},{"id":2,"value":"barSize = 48","pinned":true,"mode":"js","data":null,"name":null},{"id":501,"value":"margin = ({top: 16, right: 6, bottom: 6, left: 6})","pinned":true,"mode":"js","data":null,"name":null},{"id":5,"value":"d3 = require(\"d3@5\", \"d3-array@2\")","pinned":true,"mode":"js","data":null,"name":null},{"id":3093,"value":"import { analytics } from '@chrispahm/simple-privacy-friendly-web-analytics'","pinned":false,"mode":"js","data":null,"name":null},{"id":3094,"value":"analytics(html`<a href>`.href)","pinned":false,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}