{"id":"de13bdb9fdabe52a","slug":"learn-d3-animation","first_public_version":1534,"paused_version":null,"likes":96,"publish_level":"live","forks":60,"fork_of":null,"has_importers":true,"thumbnail":"f0499e61c7400c64db3fd0932fa8e0def6f169f77a04d314bc85e9a535f6ac25","default_thumbnail":"f0499e61c7400c64db3fd0932fa8e0def6f169f77a04d314bc85e9a535f6ac25","update_time":"2020-08-27T09:50:39.201Z","publish_time":"2023-10-26T10:24:34.138Z","publish_version":1535,"latest_version":1535,"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":""}],"files":[{"id":"68598443a16fce00a89f983b40c3b4c89b15827ac91726b864da942e4fad1b5cc19abaa1d3efd9855a7a2fadb3ef16f7c4365666eb572026a986b69d768bbc34","url":"https://static.observableusercontent.com/files/68598443a16fce00a89f983b40c3b4c89b15827ac91726b864da942e4fad1b5cc19abaa1d3efd9855a7a2fadb3ef16f7c4365666eb572026a986b69d768bbc34","download_url":"https://static.observableusercontent.com/files/68598443a16fce00a89f983b40c3b4c89b15827ac91726b864da942e4fad1b5cc19abaa1d3efd9855a7a2fadb3ef16f7c4365666eb572026a986b69d768bbc34?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27aapl-bollinger.csv","name":"aapl-bollinger.csv","create_time":"2020-02-21T19:10:18.814Z","mime_type":"text/csv","status":"public","size":48822,"content_encoding":"gzip","private_bucket_id":null}],"comments":[{"id":"2ba898bdd96cd3cd","content":"ok","node_id":0,"create_time":"2022-12-06T11:35:54.019Z","update_time":null,"resolved":true,"user":{"id":"d46afee90112ee76","avatar_url":"https://avatars.observableusercontent.com/avatar/570129f01a0d331a9849253b9668bcde3642b91755c2f93c92651f22736b6579","login":"hmthanh","name":"Minh Thanh","bio":"","home_url":"","tier":"public"}},{"id":"0e856752acef1304","content":"I don't understand the syntax in this cell: \"replay, html...\" -- what happens when a cell starts with an external object (replay) follower by a comma, etc.","node_id":53,"create_time":"2021-04-26T11:51:13.893Z","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":"adf83f96884096ff","content":"i don't understand the idea of including a reference to a button such as \"replay;\" at the beginning of a javascript block","node_id":651,"create_time":"2021-04-26T11:57:27.271Z","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":"5059552a61a988ac","content":"It may be helpful to further clarify the purpose of this line, since it's not necessary for this particular notebook. zx can be eliminated and replaced with x.\n\nconst zx = x.copy(); // x, but with a new domain.","node_id":691,"create_time":"2020-10-04T18:35:30.657Z","update_time":null,"resolved":true,"user":{"id":"08d6f3814b61fd22","avatar_url":"https://avatars.observableusercontent.com/avatar/ee75f139fd26f753fdf2b06b85171931dbe888b105aa4f0a6ce9f97bce80a8b5","login":"milesfrain","name":"","bio":"","home_url":"","tier":"public"}},{"id":"464825d801c69a7b","content":"Without the copy, setting the domain on update would mutate the scale x. We don’t want to do that because the scale x is used by other cells in this notebook.","node_id":691,"create_time":"2020-10-05T15:32:02.095Z","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":"ff7600c6970f7f38","content":"Thanks for clarifying. Didn't realize I needed to \"replay\" the earlier animations to expose the mutation issue with a shared x.","node_id":691,"create_time":"2020-10-05T22:23:47.376Z","update_time":null,"resolved":true,"user":{"id":"08d6f3814b61fd22","avatar_url":"https://avatars.observableusercontent.com/avatar/ee75f139fd26f753fdf2b06b85171931dbe888b105aa4f0a6ce9f97bce80a8b5","login":"milesfrain","name":"","bio":"","home_url":"","tier":"public"}},{"id":"a65d257311321c88","content":"@mbostock the overall approach here works a treat, but this anonymous chart.update cell seems to throw a spanner in the works when it comes to embedding (try embedding @d3/modifying-a-force-directed-graph, for example).\n\nI've found a fix in @bayre/splitting-the-bunch by using htl to call the update method directly from the input, but I'm keen to know if there's a better way to make this work.","node_id":703,"create_time":"2020-06-12T10:29:55.837Z","update_time":null,"resolved":true,"user":{"id":"b9928320d6e1a511","avatar_url":"https://avatars.observableusercontent.com/avatar/2da36ab607422e26532c254fbf46184b10db517e832d2f1e4b775481d4b1333c","login":"bayre","name":"Ben Ayre","bio":"","home_url":"","tier":"public"}},{"id":"ac8bc612ccff7c76","content":"Great point! I’ve updated this notebook to name the update cell so that you can import and embed it. If you’re using the Embed code action in the cell menu, you’ll need to edit the generated code to also evaluate the update cell. See Jeremy’s helper:\n\nhttps://observablehq.com/@jashkenas/handy-embed-code-generator\n\nIf you’re importing this chart into another notebook, you can say:\n\nimport {chart, update, viewof timeframe} from \"@d3/learn-d3-animation\"\n\nThen you’ll need to embed all three cells in your notebook.","node_id":703,"create_time":"2020-06-12T15:44:13.513Z","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":"228118f3f721aabf","content":"Wouldn't generator also recreate the entire graphic each time?","node_id":924,"create_time":"2020-10-14T04:24:42.934Z","update_time":null,"resolved":true,"user":{"id":"54649d8977fcf1ae","avatar_url":"https://avatars.observableusercontent.com/avatar/c6684bfd604737bde4eb05a174d5804d205c92922b16eccd82b09aedacf20b12","login":"fnick851","name":"Noah Song","bio":"","home_url":"https://noah-song.com","tier":"public"}},{"id":"1047857e6ae107c9","content":"It depends on how the generator is implemented. The cell above uses a generator (it yields), but it repeatedly yields the same SVG element after modifying it. Hence, it’s not necessary to recreate the entire graphic when using a generator, although it may be simpler to do so if the performance is acceptable.","node_id":924,"create_time":"2020-10-14T16:57:50.911Z","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"}}],"commenting_lock":null,"suggestions_to":[],"suggestion_from":null,"collections":[{"id":"ed39dbb66d4de9d3","type":"public","slug":"learn-d3","title":"Learn D3","description":"A guided tour of your first steps using D3.","update_time":"2020-03-24T15:08:25.176Z","pinned":false,"ordered":true,"custom_thumbnail":"f775b483011199b7b03ffb43c4f4d8dd129da4dd8781fc21278b6186c399bed2","default_thumbnail":"6782987c12640214fe1eedf5693751984e9dde1e277f7450436345a307d51067","thumbnail":"f775b483011199b7b03ffb43c4f4d8dd129da4dd8781fc21278b6186c399bed2","listing_count":9,"parent_collection_count":1,"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":1475,"title":"Learn D3: Animation","license":"isc","copyright":"Copyright 2020 Observable, Inc.","nodes":[{"id":168,"value":"md`# Learn D3: Animation\n\nUnlike graphics drawn on paper, computer graphics needn’t be static; like Frankenstein’s monster, they can come alive through animation! ✨`","pinned":false,"mode":"js","data":null,"name":null},{"id":599,"value":"viewof replay = html`<button>Replay`","pinned":false,"mode":"js","data":null,"name":null},{"id":53,"value":"replay, html`<svg viewBox=\"0 0 ${width} ${height}\">\n  ${d3.select(svg`<path d=\"${line(data)}\" fill=\"none\" stroke=\"steelblue\" stroke-width=\"1.5\" stroke-miterlimit=\"1\" stroke-dasharray=\"0,1\"></path>`).call(reveal).node()}\n  ${d3.select(svg`<g>`).call(xAxis).node()}\n  ${d3.select(svg`<g>`).call(yAxis).node()}\n</svg>`","pinned":true,"mode":"js","data":null,"name":null},{"id":0,"value":"md`The line chart above reveals progressively. This is somewhat gratuitous—motion should be used sparingly as it commands attention—but it at least reinforces that *x* represents time and introduces a touch of suspense to an otherwise ho-hum chart.`","pinned":false,"mode":"js","data":null,"name":null},{"id":182,"value":"md`The code here is similar to the axis rendering we saw previously: we select an SVG path element, call a function (*reveal*) to apply a transition, and lastly embed the element in the HTML template literal.`","pinned":false,"mode":"js","data":null,"name":null},{"id":98,"value":"reveal = path => path.transition()\n    .duration(5000)\n    .ease(d3.easeLinear)\n    .attrTween(\"stroke-dasharray\", function() {\n      const length = this.getTotalLength();\n      return d3.interpolate(`0,${length}`, `${length},${length}`);\n    })","pinned":true,"mode":"js","data":null,"name":null},{"id":455,"value":"md`Before we get into the minutiae of techniques, though, let’s take a step back to think about animation more generally.`","pinned":false,"mode":"js","data":null,"name":null},{"id":268,"value":"md`An animation is not a single graphic but a *sequence* of graphics over time. This sequence can be represented as a cell (or function) that returns the graphic for a given time *t*. For simplicity, we often use normalized time where *t* = 0 is the start of the animation and *t* = 1 is the end.`","pinned":false,"mode":"js","data":null,"name":null},{"id":317,"value":"viewof t = Scrubber(d3.ticks(0, 1, 200), {\n  autoplay: false,\n  loop: false,\n  initial: 50,\n  format: x => `t = ${x.toFixed(3)}`\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":273,"value":"html`<svg viewBox=\"0 0 ${width} ${height}\">\n  <path d=\"${line(data)}\" fill=\"none\" stroke=\"steelblue\" stroke-width=\"1.5\" stroke-miterlimit=\"1\" stroke-dasharray=\"${lineLength * t},${lineLength}\"></path>\n  ${d3.select(svg`<g>`).call(xAxis).node()}\n  ${d3.select(svg`<g>`).call(yAxis).node()}\n</svg>`","pinned":true,"mode":"js","data":null,"name":null},{"id":225,"value":"lineLength = svg`<path d=\"${line(data)}\">`.getTotalLength()","pinned":true,"mode":"js","data":null,"name":null},{"id":429,"value":"md`Our cell could theoretically return *any* graphic for a given time *t*, but often the graphic for time *t* is similar to the one for time *t* + ϵ. This similarity from frame to frame helps the viewer follow along. (Above, only the stroke-dasharray attribute animates; the rest of the graphic remains constant.) Hence, continuous animations are often defined by discrete keyframes with intermediate frames generated by interpolation, or [*tweening*](https://en.wikipedia.org/wiki/Inbetweening).`","pinned":false,"mode":"js","data":null,"name":null},{"id":388,"value":"md`Consider the stroke-dasharray attribute. It is two comma-separated numbers: the first is the length of the dash, and the second the length of the gap between dashes. If the dash length is zero, then the line will be invisible; if the dash length is as long as the line, then the line will be unbroken. By adjusting the dash length and keeping the gap at least as long as the line, we can control how much of the line is stroked. And we only need two keyframes: a zero-length dash and a line-length dash.`","pinned":false,"mode":"js","data":null,"name":null},{"id":771,"value":"md`To assist with animation (among other uses), D3 provides [interpolators](/collection/@d3/d3-interpolate). The most generic of these, [d3.interpolate](/@d3/d3-interpolate), accepts numbers, colors, strings-of-numbers, and even arrays and objects. Given a *start* and *end* value, d3.interpolate returns a function that takes a time 0 ≤ *t* ≤ 1 and returns the corresponding inbetween value.`","pinned":false,"mode":"js","data":null,"name":null},{"id":393,"value":"strokeDasharray = d3.interpolate(`0,${lineLength}`, `${lineLength},${lineLength}`)","pinned":true,"mode":"js","data":null,"name":null},{"id":399,"value":"strokeDasharray(t)","pinned":true,"mode":"js","data":null,"name":null},{"id":548,"value":"md`When defining a transition, you can either specify the interpolator explicitly (as above using [*transition*.attrTween](https://github.com/d3/d3-transition/blob/master/README.md#transition_attrTween)) or let D3 choose (using [*transition*.attr](https://github.com/d3/d3-transition/blob/master/README.md#transition_attr) or [*transition*.style](https://github.com/d3/d3-transition/blob/master/README.md#transition_style)). Being explicit allows more advanced interpolation methods such as [zooming](/@d3/d3-interpolatezoom), [gamma-corrected RGB blending](https://github.com/d3/d3-interpolate/blob/master/README.md#interpolate_gamma), or even [shape blending](/@mbostock/hello-flubber).`","pinned":false,"mode":"js","data":null,"name":null},{"id":595,"value":"ramp(d3.interpolateRgb(\"steelblue\", \"orange\"))","pinned":true,"mode":"js","data":null,"name":null},{"id":584,"value":"ramp(d3.interpolateRgb.gamma(2.2)(\"steelblue\", \"orange\"))","pinned":true,"mode":"js","data":null,"name":null},{"id":1197,"value":"md`Animation is more than interpolation, however: it’s also timing. We need to redraw sixty times a second, and to compute the normalized time *t* based on real time and the desired start time and duration of the animation.\n\nWe’ve seen two timing methods so far.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1211,"value":"md`The first relies on D3’s transitions, creating an initial graphic and then starting a transition to modify it (interpolating the stroke-dasharray).`","pinned":false,"mode":"js","data":null,"name":null},{"id":635,"value":"md`The second relies on Observable’s dataflow, recreating the graphic whenever the referenced *t* changes and relying on a [scrubber](/@mbostock/scrubber) for timing. This is less efficient than the previous method because the graphic is created from scratch each frame, but easier to write.`","pinned":false,"mode":"js","data":null,"name":null},{"id":886,"value":"md`Observable has another powerful tool for controlling animation: [*generators*](/@observablehq/introduction-to-generators). When a generator cell yields a value, its execution is suspended until the next animation frame, up to sixty times per second. The yielded value can be as simple as an integer—or it can be an incrementally-updating SVG element!`","pinned":false,"mode":"js","data":null,"name":null},{"id":645,"value":"viewof replay2 = html`<button>Replay`","pinned":false,"mode":"js","data":null,"name":null},{"id":651,"value":"{\n  replay2;\n  for (let i = 0, n = 300; i < n; ++i) {\n    yield i;\n  }\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":641,"value":"{\n  replay2;\n\n  const path = svg`<path d=\"${line(data)}\" fill=\"none\" stroke=\"steelblue\" stroke-width=\"1.5\" stroke-miterlimit=\"1\">`;\n\n  const chart = html`<svg viewBox=\"0 0 ${width} ${height}\">\n    ${path}\n    ${d3.select(svg`<g>`).call(xAxis).node()}\n    ${d3.select(svg`<g>`).call(yAxis).node()}\n  </svg>`;\n\n  for (let i = 0, n = 300; i < n; ++i) {\n    const t = (i + 1) / n;\n    path.setAttribute(\"stroke-dasharray\", `${t * lineLength},${lineLength}`);\n    yield chart;\n  }\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":1251,"value":"md`Given the variety of possible approaches to animation in Observable, which should you use? It depends!`","pinned":false,"mode":"js","data":null,"name":null},{"id":1260,"value":"md`If the graphic is simple enough that you can recreate it from scratch each frame—or if you don’t actually need animated transitions—then write the graphic declaratively. In other words, do nothing! Thanks to Observable’s dataflow, a “static” graphic can be made responsive, interactive, or animated without changing its code.`","pinned":false,"mode":"js","data":null,"name":null},{"id":924,"value":"md`On other hand, for dynamic graphics of greater complexity—where performance necessitates efficient incremental updates—employ transitions or generators.`","pinned":false,"mode":"js","data":null,"name":null},{"id":894,"value":"md`You can also combine approaches. The graphic below is initially static, but exposes a *chart*.update method to transition to a given *x*-domain; this method is called by another cell when the selected value of the radio input changes. (This code is written using d3-selection rather than HTML template literals, but the graphic structure is the same as earlier examples, so try inferring the code’s meaning by comparison.)`","pinned":false,"mode":"js","data":null,"name":null},{"id":685,"value":"viewof timeframe = {\n  const form = html`<form style=\"font: 12px var(--sans-serif); display: flex; height: 33px; align-items: center;\">\n  <label style=\"margin-right: 1em; display: inline-flex; align-items: center;\">\n    <input type=\"radio\" name=\"radio\" value=\"all\" style=\"margin-right: 0.5em;\" checked> All\n  </label>\n  <label style=\"margin-right: 1em; display: inline-flex; align-items: center;\">\n    <input type=\"radio\" name=\"radio\" value=\"2009\" style=\"margin-right: 0.5em;\"> 2009\n  </label>\n  <label style=\"margin-right: 1em; display: inline-flex; align-items: center;\">\n    <input type=\"radio\" name=\"radio\" value=\"2010\" style=\"margin-right: 0.5em;\"> 2010\n  </label>\n  <label style=\"margin-right: 1em; display: inline-flex; align-items: center;\">\n    <input type=\"radio\" name=\"radio\" value=\"2011\" style=\"margin-right: 0.5em;\"> 2011\n  </label>\n</form>`;\n  form.onchange = () => form.dispatchEvent(new CustomEvent(\"input\")); // Safari\n  form.oninput = event => {\n    switch (form.radio.value) {\n      case \"2009\": form.value = [new Date(\"2009-01-01\"), new Date(\"2010-01-01\")]; break;\n      case \"2010\": form.value = [new Date(\"2010-01-01\"), new Date(\"2011-01-01\")]; break;\n      case \"2011\": form.value = [new Date(\"2011-01-01\"), new Date(\"2012-01-01\")]; break;\n      default: form.value = d3.extent(data, d => d.date); break;\n    }\n  };\n  form.oninput();\n  return form;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":691,"value":"chart = {\n  const svg = d3.create(\"svg\")\n      .attr(\"viewBox\", [0, 0, width, height]);\n\n  const zx = x.copy(); // x, but with a new domain.\n\n  const line = d3.line()\n      .x(d => zx(d.date))\n      .y(d => y(d.close));\n\n  const path = svg.append(\"path\")\n      .attr(\"fill\", \"none\")\n      .attr(\"stroke\", \"steelblue\")\n      .attr(\"stroke-width\", 1.5)\n      .attr(\"stroke-miterlimit\", 1)\n      .attr(\"d\", line(data));\n\n  const gx = svg.append(\"g\")\n      .call(xAxis, zx);\n\n  const gy = svg.append(\"g\")\n      .call(yAxis, y);\n\n  return Object.assign(svg.node(), {\n    update(domain) {\n      const t = svg.transition().duration(750);\n      zx.domain(domain);\n      gx.transition(t).call(xAxis, zx);\n      path.transition(t).attr(\"d\", line(data));\n    }\n  });\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":703,"value":"update = chart.update(timeframe)","pinned":true,"mode":"js","data":null,"name":null},{"id":1038,"value":"md`By exposing one or more update methods, a chart can selectively animate transitions for specific value changes. If anything else changes, the chart falls back to passive reactivity and is redrawn from scratch.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1081,"value":"md`(If you’re wondering: you could define the update in a separate cell rather than exposing it as a method. However, this is not advised because editing the update code would not redraw the chart from scratch, which might result in nondeterministic behavior. 😱 And by defining the update within the chart cell, it can access local variables that persist across updates.)`","pinned":false,"mode":"js","data":null,"name":null},{"id":1448,"value":"md`This example demonstrates another handy feature of D3 axes: by switching to [*transition*.call](https://github.com/d3/d3-transition/blob/master/README.md#transition_call) instead of *selection*.call, the change in the *x*-axis is now animated rather than instantaneous, and synchronized with the transitioning path!`","pinned":false,"mode":"js","data":null,"name":null},{"id":1390,"value":"md`The animations above conveniently involve only a single element: the chart’s line. What if you want to animate multiple elements? And what if the set of elements changes over time, with new elements entering and old elements exiting? Read on!\n\n<a title=\"Learn D3: Joins\" style=\"display: inline-flex; align-items: center; font: 600 14px var(--sans-serif);\" href=\"/@d3/learn-d3-joins?collection=@d3/learn-d3\">Next<svg width=\"8\" height=\"16\" fill=\"none\" stroke-width=\"1.8\" style=\"margin-left: 0.25em; padding-top: 0.25em;\"><path d=\"M2.75 11.25L5.25 8.25L2.75 5.25\" stroke=\"currentColor\"></path></svg></a>`","pinned":false,"mode":"js","data":null,"name":null},{"id":56,"value":"md`---\n\n## Appendix\n\nIf you’re interested in designing effective animations, I highly recommend reading Heer and Robertson’s 2007 paper, [*Animated Transitions in Statistical Data Graphics*](http://vis.berkeley.edu/papers/animated_transitions/).`","pinned":false,"mode":"js","data":null,"name":null},{"id":72,"value":"data = d3.csvParse(await FileAttachment(\"aapl-bollinger.csv\").text(), d3.autoType)","pinned":true,"mode":"js","data":null,"name":null},{"id":74,"value":"line = d3.line().x(d => x(d.date)).y(d => y(d.close))","pinned":true,"mode":"js","data":null,"name":null},{"id":68,"value":"x = d3.scaleUtc()\n    .domain(d3.extent(data, d => d.date))\n    .range([margin.left, width - margin.right])","pinned":true,"mode":"js","data":null,"name":null},{"id":69,"value":"y = d3.scaleLinear()\n    .domain([0, d3.max(data, d => d.upper)])\n    .range([height - margin.bottom, margin.top])","pinned":true,"mode":"js","data":null,"name":null},{"id":60,"value":"xAxis = (g, scale = x) => g\n    .attr(\"transform\", `translate(0,${height - margin.bottom})`)\n    .call(d3.axisBottom(scale).ticks(width / 80).tickSizeOuter(0))","pinned":true,"mode":"js","data":null,"name":null},{"id":61,"value":"yAxis = (g, scale = y) => g\n    .attr(\"transform\", `translate(${margin.left},0)`)\n    .call(d3.axisLeft(scale).ticks(height / 40))\n    .call(g => g.select(\".domain\").remove())","pinned":true,"mode":"js","data":null,"name":null},{"id":63,"value":"height = 240","pinned":true,"mode":"js","data":null,"name":null},{"id":65,"value":"margin = ({top: 20, right: 30, bottom: 30, left: 40})","pinned":true,"mode":"js","data":null,"name":null},{"id":55,"value":"d3 = require(\"d3@6\")","pinned":true,"mode":"js","data":null,"name":null},{"id":315,"value":"import {Scrubber} from \"@mbostock/scrubber\"","pinned":true,"mode":"js","data":null,"name":null},{"id":582,"value":"import {ramp} from \"@mbostock/color-ramp\"","pinned":true,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}