{"id":"3d59627b07ff910d","slug":"bar-chart-race","first_public_version":799,"paused_version":null,"likes":12,"publish_level":"live","forks":4,"fork_of":{"id":"9efb7ba3d196a72e","slug":"bar-chart-race","title":"Bar chart race","owner":{"id":"c72427f1036f62b9","avatar_url":"https://avatars.observableusercontent.com/avatar/de57be7c9bd04e77db4b60770c9a9e2f51a19f47a966b9ce8f1eb6cdfbd82529","login":"johnburnmurdoch","name":"","bio":"","home_url":"","type":"team","tier":"starter_2024"},"version":482},"has_importers":false,"thumbnail":"09022e0121c87300e12aeec3e6583545326c61855664e9f808ffe5c337926fd9","default_thumbnail":"09022e0121c87300e12aeec3e6583545326c61855664e9f808ffe5c337926fd9","update_time":"2020-07-31T15:45:22.775Z","publish_time":"2023-09-12T09:34:32.711Z","publish_version":799,"latest_version":799,"roles":[],"sharing":null,"owner":{"id":"1a55a189adb52dd7","avatar_url":"https://avatars.observableusercontent.com/avatar/39220b21cbf7eff8318ad71b8f1f1b73aaaa40953e51c89e2bf097d8e901318c","login":"juba","name":"Julien Barnier","bio":"Trying to do things.","home_url":"https://data.nozav.org","type":"team","tier":"starter_2024"},"creator":{"id":"ef385df54c3fe400","avatar_url":"https://avatars.observableusercontent.com/avatar/39220b21cbf7eff8318ad71b8f1f1b73aaaa40953e51c89e2bf097d8e901318c","login":"juba","name":"Julien Barnier","bio":"Trying to do things.","home_url":"https://data.nozav.org","tier":"public"},"authors":[{"id":"ef385df54c3fe400","avatar_url":"https://avatars.observableusercontent.com/avatar/39220b21cbf7eff8318ad71b8f1f1b73aaaa40953e51c89e2bf097d8e901318c","name":"Julien Barnier","login":"juba","bio":"Trying to do things.","home_url":"https://data.nozav.org","tier":"public","approved":true,"description":""}],"files":[{"id":"c7046107405f1bbcea248050b86637f7bcd716e9ad326e98af56210f2011d95898d001c7c1ebe3bf185ab8e1434a4cfd3f447a675d3e4b9a5dfc8c81c96763dd","url":"https://static.observableusercontent.com/files/c7046107405f1bbcea248050b86637f7bcd716e9ad326e98af56210f2011d95898d001c7c1ebe3bf185ab8e1434a4cfd3f447a675d3e4b9a5dfc8c81c96763dd","download_url":"https://static.observableusercontent.com/files/c7046107405f1bbcea248050b86637f7bcd716e9ad326e98af56210f2011d95898d001c7c1ebe3bf185ab8e1434a4cfd3f447a675d3e4b9a5dfc8c81c96763dd?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27covid_deaths_by_country%25401.csv","name":"covid_deaths_by_country@1.csv","create_time":"2020-07-31T15:00:41.483Z","mime_type":"text/csv","status":"public","size":836505,"content_encoding":"gzip","private_bucket_id":null}],"comments":[],"commenting_lock":null,"suggestions_to":[],"suggestion_from":null,"collections":[{"id":"7ce57e5117a4f35d","type":"public","slug":"robservable","title":"robservable","description":"","update_time":"2020-07-02T09:30:42.639Z","pinned":false,"ordered":false,"custom_thumbnail":null,"default_thumbnail":"3c0eb3256ff2525b2a55991f8e0f23877a4775d6eab62297c43f308d85f96072","thumbnail":"3c0eb3256ff2525b2a55991f8e0f23877a4775d6eab62297c43f308d85f96072","listing_count":5,"parent_collection_count":0,"owner":{"id":"1a55a189adb52dd7","avatar_url":"https://avatars.observableusercontent.com/avatar/39220b21cbf7eff8318ad71b8f1f1b73aaaa40953e51c89e2bf097d8e901318c","login":"juba","name":"Julien Barnier","bio":"Trying to do things.","home_url":"https://data.nozav.org","type":"team","tier":"starter_2024"}}],"version":798,"title":"Bar chart race","license":null,"copyright":"","nodes":[{"id":239,"value":"md`# Reusable bar chart race`","pinned":false,"mode":"js","data":null,"name":null},{"id":774,"value":"md`This notebook has been adapted from johnburnmurdoch's [Bar chart race](https://observablehq.com/@johnburnmurdoch/bar-chart-race) notebook.\n\nData is from [Johns Hopkins University](https://github.com/CSSEGISandData/COVID-19)\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":692,"value":"viewof date = Scrubber(dates, {\n  autoplay: false,\n  loop: false,\n  delay: tickDuration\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":0,"value":"chart = {\n  const svg = d3.select(DOM.svg(width, chart_height));\n\n  let barPadding = (chart_height - (margin.bottom + margin.top)) / (top_n * 5);\n\n  // Title\n  svg\n    .append('text')\n    .attr(\"class\", \"title\")\n    .attr(\"y\", 24)\n    .html(title);\n\n  // Subtitle\n  svg\n    .append('text')\n    .attr(\"class\", \"subTitle\")\n    .attr(\"y\", 55)\n    .html(subtitle);\n\n  // Source\n  svg\n    .append('text')\n    .attr(\"class\", \"caption\")\n    .attr(\"x\", width)\n    .attr(\"y\", chart_height - 5)\n    .style(\"text-anchor\", \"end\")\n    .html(source);\n\n  let color = d3\n    .scaleOrdinal(d3[color_scheme])\n    .domain([...new Set(data.map(d => d.id))]);\n\n  data.forEach(d => {\n    (d.value = +d.value),\n      (d.value = isNaN(d.value) ? 0 : d.value),\n      (d.date_label = d.date_label === undefined ? d.date : d.date_label),\n      (d.colour = color(d.id));\n  });\n\n  let x = d3.scaleLinear().range([margin.left, width - margin.right - 65]);\n\n  let y = d3\n    .scaleLinear()\n    .domain([top_n, 0])\n    .range([chart_height - margin.bottom, margin.top]);\n\n  let xAxis = d3\n    .axisTop()\n    .scale(x)\n    .ticks(width > 500 ? 5 : 2)\n    .tickSize(-(chart_height - margin.top - margin.bottom))\n    .tickFormat(d => d3.format(',')(d));\n\n  svg\n    .append('g')\n    .attr(\"class\", \"axis xAxis\")\n    .attr(\"transform\", `translate(0, ${margin.top})`)\n    .call(xAxis)\n    .selectAll('.tick line')\n    .classed('origin', d => d == 0);\n\n  let yearText = svg\n    .append('text')\n    .attr(\"class\", \"yearText\")\n    .attr(\"x\", width - margin.right)\n    .attr(\"y\", chart_height - 25)\n    .style(\"text-anchor\", \"end\")\n    .call(halo, 10);\n\n  // Ticker\n  function update(date) {\n    let date_slice = data\n      .filter(d => d.date == date && !isNaN(d.value))\n      .sort((a, b) => b.value - a.value)\n      .slice(0, top_n);\n\n    date_slice.forEach((d, i) => (d.rank = i));\n\n    x.domain([0, d3.max(date_slice, d => d.value)]);\n\n    svg\n      .select('.xAxis')\n      .transition()\n      .duration(tickDuration)\n      .ease(d3.easeLinear)\n      .call(xAxis);\n\n    let bars = svg\n      .selectAll('.bar')\n      .data(date_slice, d => d.id)\n      .join(\n        enter =>\n          enter\n            .append('rect')\n            .attr(\"class\", d => `bar ${d.id.replace(/\\s/g, '_')}`)\n            .attr(\"x\", x(0) + 1)\n            .attr(\"width\", d => x(d.value) - x(0) - 1)\n            .attr(\"y\", d => y(top_n + 1) + 5)\n            .attr(\"height\", y(1) - y(0) - barPadding)\n            .style(\"fill\", d => d.colour),\n        update =>\n          update\n            .transition()\n            .duration(tickDuration)\n            .ease(d3.easeLinear)\n            .attr(\"y\", d => y(d.rank) + 5)\n            .attr(\"width\", d => x(d.value) - x(0) - 1),\n        exit =>\n          exit\n            .transition()\n            .duration(tickDuration)\n            .ease(d3.easeLinear)\n            .attr(\"width\", d => x(d.value) - x(0) - 1)\n            .attr(\"y\", d => y(top_n + 1) + 5)\n            .remove()\n      );\n\n    let labels = svg\n      .selectAll('.label')\n      .data(date_slice, d => d.id)\n      .join(\n        enter =>\n          enter\n            .append('text')\n            .attr(\"class\", \"label\")\n            .attr(\"x\", d => x(d.value) - 8)\n            .attr(\"y\", d => y(top_n + 1) + 5 + (y(1) - y(0)) / 2)\n            .style(\"text-anchor\", \"end\")\n            .html(d => d.id),\n        update =>\n          update\n            .transition()\n            .duration(tickDuration)\n            .ease(d3.easeLinear)\n            .attr(\"x\", d => x(d.value) - 8)\n            .attr(\"y\", d => y(d.rank) + 5 + (y(1) - y(0)) / 2 + 1),\n        exit =>\n          exit\n            .transition()\n            .duration(tickDuration)\n            .ease(d3.easeLinear)\n            .attr(\"x\", d => x(d.value) - 8)\n            .attr(\"y\", d => y(top_n + 1) + 5)\n            .remove()\n      );\n\n    let valueLabels = svg\n      .selectAll('.valueLabel')\n      .data(date_slice, d => d.id)\n      .join(\n        enter =>\n          enter\n            .append('text')\n            .attr(\"class\", 'valueLabel')\n            .attr(\"x\", d => x(d.value) + 5)\n            .attr(\"y\", d => y(top_n + 1) + 5)\n            .text(d => d.value),\n        update =>\n          update\n            .transition()\n            .duration(tickDuration)\n            .ease(d3.easeLinear)\n            .attr(\"x\", d => x(d.value) + 5)\n            .attr(\"y\", d => y(d.rank) + 5 + (y(1) - y(0)) / 2 + 1)\n            .tween(\"text\", function(d) {\n              let i = d3.interpolateRound(\n                +this.textContent.replace(/,/g, ''),\n                d.value\n              );\n              return function(t) {\n                this.textContent = d3.format(',')(i(t));\n              };\n            }),\n        exit =>\n          exit\n            .transition()\n            .duration(tickDuration)\n            .ease(d3.easeLinear)\n            .attr(\"x\", d => x(d.value) + 5)\n            .attr(\"y\", d => y(top_n + 1) + 5)\n            .remove()\n      );\n\n    yearText.html(date_slice.map(d => d.date_label)[0]);\n  }\n\n  update(dates[0]);\n\n  return Object.assign(svg.node(), { update });\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":671,"value":"draw = {\n  chart.update(date);\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":785,"value":"md`## Usage\n\n[robservable](https://juba.github.io/robservable/) users can call this notebook from R with something like the following. Use the <code>input</code> named list to customize the [settings](#sec_settings) :\n\n\\`\\`\\`r\n## Load sample data from Johns Hopkins' Github\nd <- read_csv(\"https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_deaths_global.csv\")\n\n## Data must be in a data frame with id, date and value columns. Data must be ordered by date.\nd <- d %>%\n  select(-\\`Province/State\\`, -Lat, -Long) %>%\n  rename(id = \\`Country/Region\\`) %>%\n  group_by(id) %>%\n  summarise(across(everything(), sum)) %>%\n  pivot_longer(-id, names_to = \"date\") %>%\n  mutate(date = as.character(lubridate::mdy(date)))\n\nrobservable(\n  \"https://observablehq.com/@juba/bar-chart-race\",\n  include = c(\"viewof date\", \"chart\", \"draw\", \"styles\"),\n  hide = \"draw\",\n  input = list(\n    data = d,\n    title = \"COVID-19 deaths\",\n    subtitle = \"Cumulative number of COVID-19 deaths by country\",\n    source = \"Source : Johns Hopkins University\"\n  )\n)\n\\`\\`\\`\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":494,"value":"sec_settings = md`## Settings`","pinned":false,"mode":"js","data":null,"name":null},{"id":794,"value":"md`Data must be an array with <code>id</code>, <code>date</code> and <code>value</code> columns, ordered by <code>id</code> and <code>date</code>. An optional <code>date_label</code> column can be used if you want to display another value than <code>date</code> in the chart (for example if you iterate over monthly data but only want to display the year value).`","pinned":false,"mode":"js","data":null,"name":null},{"id":144,"value":"data = d3.csvParse(await FileAttachment('covid_deaths_by_country@1.csv').text())","pinned":false,"mode":"js","data":null,"name":null},{"id":498,"value":"title = 'COVID-19 deaths'","pinned":false,"mode":"js","data":null,"name":null},{"id":502,"value":"subtitle = 'Cumulative number of COVID-19 deaths by country'","pinned":false,"mode":"js","data":null,"name":null},{"id":511,"value":"source = \"Source: Johns Hopkins University\"","pinned":false,"mode":"js","data":null,"name":null},{"id":408,"value":"tickDuration = 500","pinned":false,"mode":"js","data":null,"name":null},{"id":411,"value":"top_n = 12;","pinned":false,"mode":"js","data":null,"name":null},{"id":647,"value":"color_scheme = \"schemeSet3\"","pinned":false,"mode":"js","data":null,"name":null},{"id":2,"value":"chart_height = 600","pinned":false,"mode":"js","data":null,"name":null},{"id":488,"value":"margin = ({\n  top: 80,\n  right: 0,\n  bottom: 5,\n  left: 0\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":491,"value":"md`## Notebook code`","pinned":false,"mode":"js","data":null,"name":null},{"id":297,"value":"styles = html`<style>\ntext{\n  font-size: 16px;\n  font-family: Roboto Condensed, Open Sans, sans-serif;\n}\ntext.title{\n  font-size: 24px;\n  font-weight: 500;\n}\ntext.subTitle{\n  font-weight: 500;\n  fill: #777777;\n}\ntext.caption{\n  font-weight: 400;\n  font-size: 14px;\n  fill: #777777;\n}\ntext.label{\n  font-weight: 600;\n}\ntext.yearText{\n  font-size: 50px;\n  font-weight: 700;\n  opacity: 0.25;\n}\n.tick text {\n  font-size: 12px;\n  fill: #777777;\n}\n.xAxis .tick:nth-child(2) text {\n  text-anchor: start;\n}\n.tick line {\n  shape-rendering: CrispEdges;\n  stroke: #dddddd;\n}\n.tick line.origin{\n  stroke: #aaaaaa;\n}\npath.domain{\n  display: none;\n}\n</style>`","pinned":false,"mode":"js","data":null,"name":null},{"id":547,"value":"dates = [...new Set(data.map(d => d.date))]","pinned":true,"mode":"js","data":null,"name":null},{"id":356,"value":"// This is a lightly modified version of Mike Bostock’s text halo function from @d3/connected-scatterplot\nhalo = function(text, strokeWidth) {\n  text\n    .select(function() {\n      return this.parentNode.insertBefore(this.cloneNode(true), this);\n    })\n    .style(\"fill\", '#ffffff')\n    .style(\"stroke\", '#ffffff')\n    .style(\"stroke-width\", strokeWidth)\n    .style(\"stroke-linejoin\", 'round')\n    .style(\"opacity\", 1);\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":689,"value":"import { Scrubber } from \"@mbostock/scrubber\"","pinned":false,"mode":"js","data":null,"name":null},{"id":5,"value":"d3 = require('d3@5')","pinned":false,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}