{"id":"e9e3929cf7c50b45","slug":"bar-chart-race-explained","trashed":false,"description":"","likes":383,"publish_level":"live","forks":415,"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":true,"update_time":"2023-10-26T09:54:09.582Z","first_public_version":3122,"paused_version":null,"publish_time":"2023-10-26T09:54:44.550Z","publish_version":3122,"latest_version":3122,"thumbnail":"49e3b8e7b5c25c76853ac31b4ed41e895e2facb91573182f4e68e995d0ccb899","default_thumbnail":"49e3b8e7b5c25c76853ac31b4ed41e895e2facb91573182f4e68e995d0ccb899","roles":[],"sharing":null,"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"},"creator":{"id":"074c414ad1d825f5","avatar_url":"https://avatars.observableusercontent.com/avatar/82811927da99f8938001b2ef1f552ad2c47083e46ebc55a3a146a5a5848c4519","login":"mbostock","name":"Mike Bostock","bio":"Visualization toolmaker. Founder @observablehq. Creator @d3. Former @nytgraphics. Pronounced BOSS-tock.","home_url":"https://bost.ocks.org/mike/","tier":"pro"},"authors":[{"id":"074c414ad1d825f5","avatar_url":"https://avatars.observableusercontent.com/avatar/82811927da99f8938001b2ef1f552ad2c47083e46ebc55a3a146a5a5848c4519","name":"Mike Bostock","login":"mbostock","bio":"Visualization toolmaker. Founder @observablehq. Creator @d3. Former @nytgraphics. Pronounced BOSS-tock.","home_url":"https://bost.ocks.org/mike/","tier":"pro","approved":true,"description":""}],"collections":[{"id":"f7a05b8062c4400d","type":"public","slug":"industrial","title":"Industrial","description":"Industry 4.0 relies on data driven transformation. From ubiquitous IoT sensors, to optimized warehouses and traceable satellite orbits, industrial complexity needs to be seen to be understood. ","update_time":"2022-11-30T19:45:02.622Z","pinned":false,"ordered":true,"custom_thumbnail":null,"default_thumbnail":"76e567dbaf78a1e8e116f7dd1f63e919b707583f5a90bc3535315ccde55a19e8","thumbnail":"76e567dbaf78a1e8e116f7dd1f63e919b707583f5a90bc3535315ccde55a19e8","listing_count":36,"parent_collection_count":2,"owner":{"id":"f35c755083683fe5","avatar_url":"https://avatars.observableusercontent.com/avatar/5a51c3b908225a581d20577e488e2aba8cbc9541c52982c638638c370c3e5e8e","login":"observablehq","name":"Observable","bio":"The end-to-end solution for building and hosting better data apps, dashboards, and reports.","home_url":"https://observablehq.com","type":"team","tier":"enterprise_2024"}}],"files":[{"id":"aec3792837253d4c6168f9bbecdf495140a5f9bb1cdb12c7c8113cec26332634a71ad29b446a1e8236e0a45732ea5d0b4e86d9d1568ff5791412f093ec06f4f1","url":"https://static.observableusercontent.com/files/aec3792837253d4c6168f9bbecdf495140a5f9bb1cdb12c7c8113cec26332634a71ad29b446a1e8236e0a45732ea5d0b4e86d9d1568ff5791412f093ec06f4f1","download_url":"https://static.observableusercontent.com/files/aec3792837253d4c6168f9bbecdf495140a5f9bb1cdb12c7c8113cec26332634a71ad29b446a1e8236e0a45732ea5d0b4e86d9d1568ff5791412f093ec06f4f1?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27category-brands.csv","name":"category-brands.csv","create_time":"2019-11-10T20:33:27.802Z","mime_type":"text/csv","status":"public","size":69821,"content_encoding":"gzip","private_bucket_id":null}],"comments":[{"id":"23843342316211db","content":"could you add a pause button to this notebook?","node_id":0,"create_time":"2021-04-24T14:49:52.573Z","update_time":null,"resolved":true,"user":{"id":"bda3bb7cbffeec7d","avatar_url":"https://avatars.observableusercontent.com/avatar/34300050560dea2b36d63efd30e3f07e6f455717f9e917936080d33429d8b426","login":"dan-reznik","name":"Dan S. Reznik","bio":"Data Analytics Consultant, Instructor. Areas: Data Analytics, Geometry, Simulation, Visualization, Computer Vision, Robotics...","home_url":"http://www.dat-sci.com","tier":"public"}},{"id":"33c86b85ec6036fa","content":"D3 transitions don’t support pausing, but see this fork that uses a scrubber:\n\nhttps://observablehq.com/@mbostock/bar-chart-race-with-scrubber","node_id":0,"create_time":"2021-04-24T22:58:54.501Z","update_time":null,"resolved":true,"user":{"id":"074c414ad1d825f5","avatar_url":"https://avatars.observableusercontent.com/avatar/82811927da99f8938001b2ef1f552ad2c47083e46ebc55a3a146a5a5848c4519","login":"mbostock","name":"Mike Bostock","bio":"Visualization toolmaker. Founder @observablehq. Creator @d3. Former @nytgraphics. Pronounced BOSS-tock.","home_url":"https://bost.ocks.org/mike/","tier":"pro"}},{"id":"73d2b3fcf1b0465c","content":"still i am not able to understand from() and other function","node_id":880,"create_time":"2023-01-20T16:01:12.670Z","update_time":null,"resolved":false,"user":{"id":"1aacca419acd71c6","avatar_url":"https://avatars.observableusercontent.com/avatar/eb0b88fa8078437a8b00b3d5f58365a4f2b4953d5ab8e2e21e62a381b05921d3","login":"bilenray","name":"Bilen Ray","bio":"","home_url":"","tier":"public"}}],"commenting_lock":null,"suggestion_from":null,"suggestions_to":[],"version":3122,"title":"Bar Chart Race, Explained","license":"isc","copyright":"Copyright 2019–2023 Observable, Inc.","nodes":[{"id":239,"value":"# Bar Chart Race, Explained\n\nThis is a pedagogical implementation of an animated [bar chart race](/@d3/bar-chart-race). Read on to learn how it works, or fork this notebook and drop in your data!","pinned":false,"mode":"md","data":{},"name":""},{"id":144,"value":"data = d3.csvParse(await FileAttachment(\"category-brands.csv\").text(), d3.autoType)","pinned":true,"mode":"js","data":null,"name":null},{"id":1051,"value":"The data for the race is a CSV with columns *date* (in [YYYY-MM-DD format](https://www.ecma-international.org/ecma-262/9.0/index.html#sec-date-time-string-format)), *name*, *value* and optionally *category* (which if present determines color). To replace the data, click the file icon <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke-width=\"2\"><path d=\"M12.1637 11.8446L12.7364 11.286L12.1637 11.8446ZM5.97903 6.6495L11.591 12.4032L12.7364 11.286L7.12442 5.53232L5.97903 6.6495ZM10.6159 13.3544L4.37343 6.95428L3.22804 8.07146L9.47048 14.4715L10.6159 13.3544ZM7.44625 4.02933L13.4139 10.4536L14.5861 9.36462L8.61851 2.94039L7.44625 4.02933ZM4.23394 4.1499C5.0428 3.13633 6.5637 3.07925 7.44625 4.02933L8.61851 2.94039C7.0703 1.27372 4.40228 1.37385 2.98335 3.15189L4.23394 4.1499ZM4.37343 6.95428C3.62641 6.1884 3.56661 4.98612 4.23394 4.1499L2.98335 3.15189C1.81269 4.61883 1.91759 6.72792 3.22804 8.07146L4.37343 6.95428ZM11.591 13.3544C11.3237 13.6284 10.8832 13.6284 10.6159 13.3544L9.47048 14.4715C10.3657 15.3893 11.8412 15.3893 12.7364 14.4715L11.591 13.3544ZM11.591 12.4032C11.8491 12.6678 11.8491 13.0898 11.591 13.3544L12.7364 14.4715C13.6006 13.5855 13.6006 12.172 12.7364 11.286L11.591 12.4032Z\" fill=\"currentColor\"></path></svg> in the cell above.","pinned":false,"mode":"md","data":{},"name":""},{"id":982,"value":"viewof replay = html`<button>Replay`","pinned":false,"mode":"js","data":null,"name":null},{"id":1058,"value":"## Best Global Brands\n\nValue in $M; color indicates sector. Data: [Interbrand](https://www.interbrand.com/best-brands/)","pinned":false,"mode":"md","data":{},"name":"title"},{"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].value]);\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":1080,"value":"The chart consists of four parts. From bottom to top in *z*-order: the bars, the *x*-axis, the labels, and the ticker showing the current date. I’ve separated these parts so that they’ll be easier to explain individually below.","pinned":false,"mode":"md","data":{},"name":""},{"id":1597,"value":"The animation iterates over each of the keyframes, delegating updates to each of the four chart components and awaiting the [transition’s end](/@d3/transition-end). Linear [easing](/@d3/easing-animations?collection=@d3/d3-ease) enures the animation runs at constant speed.","pinned":false,"mode":"md","data":{},"name":""},{"id":1514,"value":"(Observable aside: notebooks [run in topological order](/@observablehq/how-observable-runs), hence the chart cell *above* can depend on cells defined *below*. We write notebooks in whatever order we like and let the computer figure out how to run them. Hooray, [literate programming](http://www.literateprogramming.com/)! You can edit this notebook and the chart will re-run automatically: on [invalidation](/@observablehq/invalidation), the animation is interrupted and a new one starts.)","pinned":false,"mode":"md","data":{},"name":""},{"id":408,"value":"duration = 250","pinned":true,"mode":"js","data":null,"name":null},{"id":2617,"value":"You can make the animation faster or slower by adjusting the duration between keyframes in milliseconds.","pinned":false,"mode":"md","data":{},"name":""},{"id":2503,"value":"## Data\n\nBut what are these keyframes? Data, derived from the source data!","pinned":false,"mode":"md","data":{},"name":""},{"id":1834,"value":"Take another look at the source data by inspecting the array below. Note that it does not include a *rank* column—we will compute it.","pinned":false,"mode":"md","data":{},"name":""},{"id":1828,"value":"data","pinned":true,"mode":"js","data":null,"name":null},{"id":1842,"value":"For any given brand, such as Apple, there are multiple entries in the dataset: one per year. We can also see this by [grouping](/@d3/d3-group) by name.","pinned":false,"mode":"md","data":{},"name":""},{"id":1843,"value":"d3.group(data, d => d.name)","pinned":true,"mode":"js","data":null,"name":null},{"id":1848,"value":"While most brands are defined for the full duration (from 2000 to 2019), and thus have twenty entries, some brands are occasionally missing. Heineken, for instance, is missing from 2005 to 2009 because it fell out of the top 100 tracked by Interbrand.","pinned":false,"mode":"md","data":{},"name":""},{"id":2180,"value":"data.filter(d => d.name === \"Heineken\")","pinned":true,"mode":"js","data":null,"name":null},{"id":2003,"value":"Why do we care about the top 100 when the chart only shows the top ${n}? Having data beyond the top ${n} allows bars that enter or exit to correctly transition from the previous value or to the next value *outside* the top group. And besides, there’s little cost to processing the larger set. If you like, you can increase the value of *n* below for a bigger race.","pinned":false,"mode":"md","data":{},"name":""},{"id":411,"value":"n = 12","pinned":true,"mode":"js","data":null,"name":null},{"id":596,"value":"Here’s the full [set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) of brand names covering twenty years. It’s larger than yearly top 100 because there’s turnover. (Farewell, Motorola, *we hardly knew ye*.)","pinned":false,"mode":"md","data":{},"name":""},{"id":2135,"value":"names = new Set(data.map(d => d.name))","pinned":true,"mode":"js","data":null,"name":null},{"id":923,"value":"Similarly, here’s the set of dates. But our approach here is different. We’ll construct a nested [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) from date and name to value. Then we’ll convert this to an array to order the data chronologically.","pinned":false,"mode":"md","data":{},"name":""},{"id":880,"value":"datevalues = Array.from(d3.rollup(data, ([d]) => d.value, 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":2132,"value":"(Dates are objects, so we have to do a little dance to construct the map. The dates are first coerced to numbers using + for keys, and then converted back into dates using the Date constructor.)","pinned":false,"mode":"md","data":{},"name":""},{"id":888,"value":"Now we’re ready to compute the zero-based rank for each brand. The *rank* function below takes a *value* accessor function, retrieves each brand’s value, sorts the result by descending value, and then assigns rank.","pinned":false,"mode":"md","data":{},"name":""},{"id":862,"value":"function rank(value) {\n  const data = Array.from(names, name => ({name, value: value(name)}));\n  data.sort((a, b) => d3.descending(a.value, b.value));\n  for (let i = 0; i < data.length; ++i) data[i].rank = Math.min(n, i);\n  return data;\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":1918,"value":"Here’s an example, computing the ranked brands for the first date in the dataset. (Inspect the array below to see the result.)","pinned":false,"mode":"md","data":{},"name":""},{"id":1916,"value":"rank(name => datevalues[0][1].get(name))","pinned":true,"mode":"js","data":null,"name":null},{"id":1245,"value":"Why bother with a *value* accessor function? Well, because we’re about to do something interesting. 🌶\n\nRank is an *ordinal* value: a brand can be rank 2 or 3, but never rank 2.345. In the source data, ranks change once per year. If we animated rank changes over the year (${(duration * k) / 1000} seconds), many bars would move up or down simultaneously, making the race hard to follow. Hence we generate interpolated frames within the year to animate rank changes more quickly (${duration} milliseconds), improving readability.\n\nTry disabling interpolation by setting *k* to 1 below, then scroll up to see how this affects the animation.","pinned":false,"mode":"md","data":{},"name":""},{"id":830,"value":"k = 10","pinned":true,"mode":"js","data":null,"name":null},{"id":762,"value":"Since our *rank* helper above takes a function, so we can use it to interpolate values [linearly](https://en.wikipedia.org/wiki/Linear_interpolation). If ${tex`a`} is the starting value and ${tex`b`} is the ending value, then we vary the parameter ${tex`t \\in [0,1]`} to compute the interpolated value ${tex`a(1 - t) + bt`}. For any missing data—remember, turnover—we treat the value as zero.","pinned":false,"mode":"md","data":{},"name":""},{"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) || 0) * (1 - t) + (b.get(name) || 0) * t)\n      ]);\n    }\n  }\n  keyframes.push([new Date(kb), rank(name => b.get(name) || 0)]);\n  return keyframes;\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":737,"value":"The last data-processing step—we’re almost there!—is to prepare for enter and exit. An *enter* transition occurs when a brand enters the top ${n}, and an *exit* transition occurs when a brand exits the top ${n}.\n\nFor example, between 2001 and 2002, Toyota enters the top 12 (moving from rank 14 to 12) while AT&T exits the top 12 (moving from rank 10 to 17). When animating Toyota’s entrance, we need to know the rank that it was coming from (14), and similarly when animating AT&T’s exit, we need to know the rank it is going to (17).","pinned":false,"mode":"md","data":{},"name":""},{"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":2474,"value":"## Bars\n\nEnough with the data. Let’s draw!","pinned":false,"mode":"md","data":{},"name":""},{"id":1582,"value":"The four chart components, starting with the bars here, are implemented as functions that are passed a [selection](https://d3js.org/d3-selection) of the chart’s root SVG element. This function *initializes* the component, such as by adding a G element, and returns an *update* function which will be called repeatedly to implement transitions.","pinned":false,"mode":"md","data":{},"name":""},{"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).value) - 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).value) - x(0))\n    )\n    .call(bar => bar.transition(transition)\n      .attr(\"y\", d => y(d.rank))\n      .attr(\"width\", d => x(d.value) - x(0)));\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":2476,"value":"The update function applies a data-join: D3’s pattern for manipulating the DOM based on data. The key (the second argument to *selection*.data) is the *name*, ensuring that the data is bound consistently. We then use [*selection*.join](/@d3/selection-join) to handle enter, update and exit separately. As discussed above, when bars enter or exit, they transition from the *previous* value on enter or to the *next* value on exit.","pinned":false,"mode":"md","data":{},"name":""},{"id":2482,"value":"D3 allows you to minimize DOM changes to improve performance. Hence, any attribute that is shared by all bars is applied to the parent G element (fill-opacity). And any attribute that is constant for the life of a given bar but varies between bars is assigned on enter (fill, height, x). Hence, only the minimal set of attributes are transitioned (y, width). To avoid code duplication, enter and update transitions are shared using the merged result of *selection*.join.","pinned":false,"mode":"md","data":{},"name":""},{"id":2674,"value":"Each time the update function is called by the chart, we re-assign the *bar* selection to the result of *selection*.join, thereby maintaining the current selection of bars. We use [*selection*.call](https://d3js.org/d3-selection/control-flow#selection_call) to initiate transitions without breaking the method chain.","pinned":false,"mode":"md","data":{},"name":""},{"id":2385,"value":"The parent *transition* is passed in by the chart, allowing the child transitions to inherit timing parameters.","pinned":false,"mode":"md","data":{},"name":""},{"id":2165,"value":"## Labels\n\nAs you might expect, the labels are implemented similarly to the bars.","pinned":false,"mode":"md","data":{},"name":""},{"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).value)},${y((prev.get(d) || d).rank)})`)\n        .attr(\"y\", y.bandwidth() / 2)\n        .attr(\"x\", -6)\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\", -6)\n          .attr(\"dy\", \"1.15em\")),\n      update => update,\n      exit => exit.transition(transition).remove()\n        .attr(\"transform\", d => `translate(${x((next.get(d) || d).value)},${y((next.get(d) || d).rank)})`)\n        .call(g => g.select(\"tspan\")\n                    .textTween((d) => d3.interpolateRound(d.value, (next.get(d) || d).value))\n             )\n    )\n    .call(bar => bar.transition(transition)\n      .attr(\"transform\", d => `translate(${x(d.value)},${y(d.rank)})`)\n      .call(g => g.select(\"tspan\")\n                  .textTween((d) => (t) => formatNumber(\n                    d3.interpolateNumber((prev.get(d) || d).value, d.value)(t)\n                  ))\n           )\n    )\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":2684,"value":"There are two labels per bar: the name and the value; a TSPAN element is used for the latter. We set the *x* attribute of both elements so they are right-aligned, and use the *transform* attribute (and *y* and *dy*) to position text. (See the [SVG specification](https://www.w3.org/TR/SVG11/text.html#TextElement) for more on text elements.)","pinned":false,"mode":"md","data":{},"name":""},{"id":1151,"value":"To transition the text labels, we use D3’s [*transition*.textTween](https://d3js.org/d3-transition/modifying#transition_textTween).","pinned":false,"mode":"md","data":{},"name":""},{"id":2397,"value":"Since the value labels change sixty times per second, we use [tabular figures](https://practicaltypography.com/alternate-figures.html#tabular-and-proportional-figures) to reduce jitter and improve readability. Try commenting out the [font-variant-numeric](https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-numeric) style above to see its effect!","pinned":false,"mode":"md","data":{},"name":""},{"id":2405,"value":"The function below is used to [format](https://d3js.org/d3-format) values as whole numbers. If you want decimal values, adjust accordingly.","pinned":false,"mode":"md","data":{},"name":""},{"id":1142,"value":"formatNumber = d3.format(\",d\")","pinned":true,"mode":"js","data":null,"name":null},{"id":1800,"value":"## Axis\n\nOur *x*-axis is top-anchored and slightly customized.","pinned":false,"mode":"md","data":{},"name":""},{"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      .tickSizeOuter(0)\n      .tickSizeInner(-barSize * (n + y.padding()));\n\n  return (_, transition) => {\n    g.transition(transition).call(axis);\n    g.select(\".tick:first-of-type text\").remove();\n    g.selectAll(\".tick:not(:first-of-type) line\").attr(\"stroke\", \"white\");\n    g.select(\".domain\").remove();\n  };\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":2439,"value":"Not much to say here. We use D3’s [margin convention](/@d3/chart-template). The suggested tick count is derived from Observable’s responsive [*width*](https://github.com/observablehq/stdlib/blob/master/README.md#width), so it works on both small and large screens. The tick size is negative so that the tick lines overlay the bars. And we use [post-selection](https://observablehq.com/@d3/styled-axes)—modifying the elements generated by the axis—to remove the domain path and change the tick line color.","pinned":false,"mode":"md","data":{},"name":""},{"id":2162,"value":"## Ticker\n\nThe “ticker” in the bottom-right corner shows the current date.","pinned":false,"mode":"md","data":{},"name":""},{"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 - 0.45))\n      .attr(\"dy\", \"0.32em\")\n      .text(formatDate(keyframes[0][0]));\n\n  return ([date], transition) => {\n    transition.end().then(() => now.text(formatDate(date)));\n  };\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":2769,"value":"The keyframe’s *date* represents the date at the *end* of the transition; hence, the displayed date is updated when the *transition*.end promise resolves.","pinned":false,"mode":"md","data":{},"name":""},{"id":2806,"value":"The function below is used to [format](https://d3js.org/d3-time-format) dates as four-digit years. If you want a more precise display for shorter time periods, adjust as appropriate.","pinned":false,"mode":"md","data":{},"name":""},{"id":1287,"value":"formatDate = d3.utcFormat(\"%Y\")","pinned":true,"mode":"js","data":null,"name":null},{"id":1492,"value":"## Color\n\nThat concludes our chart components! Only a few odds and ends left, such as this [ordinal scale](/@d3/d3-scaleordinal?collection=@d3/d3-scale) mapping from category name to color. I chose the Tableau10 [scheme](/@d3/color-schemes) because it is less saturated than Category10.","pinned":false,"mode":"md","data":{},"name":""},{"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":2096,"value":"This code adapts to the data: if the data defines a *category* field, this field determines the color; otherwise, the *name* field is used. This means your replacement data can omit the category field and you’ll still have varying color, making it easier to follow bars as they move up or down.","pinned":false,"mode":"md","data":{},"name":""},{"id":2829,"value":"I’ve assumed that the category for a given name never changes. If that’s not true of your data, you’ll need to change this scale implementation and implement fill transitions in the bar component above.","pinned":false,"mode":"md","data":{},"name":""},{"id":641,"value":"## Position\n\nThe *x*-scale is linear. The chart mutates the domain as the animation runs.","pinned":false,"mode":"md","data":{},"name":""},{"id":638,"value":"x = d3.scaleLinear([0, 1], [margin.left, width - margin.right])","pinned":true,"mode":"js","data":null,"name":null},{"id":644,"value":"The *y*-scale is a [band scale](/@d3/d3-scaleband?collection=@d3/d3-scale), but it’s a bit unusual in that the domain covers *n* + 1 = ${n + 1} ranks, so that bars can enter and exit.","pinned":false,"mode":"md","data":{},"name":""},{"id":542,"value":"y = d3.scaleBand()\n    .domain(d3.range(n + 1))\n    .rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])\n    .padding(0.1)","pinned":true,"mode":"js","data":null,"name":null},{"id":649,"value":"This chart’s also a little unusual in that the height is specified indirectly: it’s based on the *bar* height (below) and the number of bars (*n*). This means we can easily change the number of bars and the chart will resize automatically.","pinned":false,"mode":"md","data":{},"name":""},{"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: 0})","pinned":true,"mode":"js","data":null,"name":null},{"id":1174,"value":"## Libraries\n\nWe’re using d3@7 for its lovely new [d3.group](/@d3/d3-group) method.","pinned":false,"mode":"md","data":{},"name":""},{"id":5,"value":"d3 = require(\"d3@7\")","pinned":true,"mode":"js","data":null,"name":null},{"id":2850,"value":"Thanks for reading! 🙏\n\nPlease send any corrections or comments via [suggestion](/@observablehq/suggestions-and-comments), or let me know your thoughts and questions on [Twitter](https://twitter.com/mbostock).","pinned":false,"mode":"md","data":{},"name":""}],"resolutions":[],"schedule":null,"last_view_time":null}