<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andrew Courtice</title>
    <description>The latest articles on DEV Community by Andrew Courtice (@andrewcourtice).</description>
    <link>https://dev.to/andrewcourtice</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F468138%2F762423c2-2556-465c-83f8-a30e60ee7f89.jpeg</url>
      <title>DEV Community: Andrew Courtice</title>
      <link>https://dev.to/andrewcourtice</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andrewcourtice"/>
    <language>en</language>
    <item>
      <title>I built a chart for the web, then rendered it in my terminal by changing one line 🤯</title>
      <dc:creator>Andrew Courtice</dc:creator>
      <pubDate>Wed, 22 Jul 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/andrewcourtice/i-built-a-chart-for-the-web-then-rendered-it-in-my-terminal-by-changing-one-line-42no</link>
      <guid>https://dev.to/andrewcourtice/i-built-a-chart-for-the-web-then-rendered-it-in-my-terminal-by-changing-one-line-42no</guid>
      <description>&lt;p&gt;Here is something that has annoyed me for years; more than a decade at this point... &lt;/p&gt;

&lt;p&gt;When you start a graphics project on the web, one of the first decisions you make is also one of the most difficult to change: &lt;strong&gt;Canvas or SVG?&lt;/strong&gt; Canvas is fast and pixel-pushy but gives you nothing. No objects, no events, no hierarchy, just a bag of imperative draw calls you get to babysit forever. SVG is lovely and declarative and inspectable, right up until you put ten thousand nodes in the DOM and your framerate falls off a cliff.&lt;/p&gt;

&lt;p&gt;So you pick one. And because the two APIs look nothing alike, that pick calcifies. Six months later "can we also render this server-side?" turns into a rewrite instead of a config change. Of course, since the introduction of AI tooling a rewrite is far less daunting, but Ripl makes this a non-issue altogether.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.ripl.run" rel="noopener noreferrer"&gt;Ripl&lt;/a&gt; (pronounced &lt;em&gt;ripple&lt;/em&gt;) is a small library that refuses to make you choose. You write your drawing code &lt;strong&gt;once&lt;/strong&gt;, against a single API, and point it at whatever surface you want. Canvas. SVG. A terminal. Switching between them is a one-line change, and I promise that isn't headline exaggeration.&lt;/p&gt;

&lt;p&gt;I'm going to prove it by building an animated line chart, and then rendering that exact same chart in a terminal using braille characters. No rewrite. The drawing code doesn't move a muscle.&lt;/p&gt;

&lt;p&gt;Let's get into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole idea in three lines
&lt;/h2&gt;

&lt;p&gt;Ripl's mental model is refreshingly boring, which is exactly what you want when it comes to rendering on the web. There's a &lt;strong&gt;context&lt;/strong&gt; (the surface you draw on), there are &lt;strong&gt;elements&lt;/strong&gt; (the things you draw), and you &lt;strong&gt;render&lt;/strong&gt; the elements to the context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;createCircle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;createContext&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ripl/web&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.chart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;createCircle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#6366f1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@ripl/web&lt;/code&gt; gives you a Canvas context by default. Now watch what it takes to make that identical circle an SVG element instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ripl/svg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 👈 the only change&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createCircle&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ripl/web&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the whole trick. The &lt;code&gt;createCircle&lt;/code&gt; call, the properties, the &lt;code&gt;.render()&lt;/code&gt;: none of it knows or cares what it's drawing to. The context is an abstraction over "how do I actually put a shape on a surface," and everything above it is blissfully ignorant of the answer.&lt;/p&gt;

&lt;p&gt;If you've ever used Two.js, this idea will feel familiar. Ripl borrows its context-agnostic rendering from there, cribs its &lt;strong&gt;scales and interpolation&lt;/strong&gt; from D3, and models its &lt;strong&gt;hierarchy, style inheritance, events, and querying&lt;/strong&gt; on the DOM and CSSOM. If you know CSS, you already know how a &lt;code&gt;fill&lt;/code&gt; set on a group cascades down to its children. That's not an accident. It's the whole design philosophy.&lt;/p&gt;

&lt;p&gt;Right, enough philosophy. Let's build something real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting the stage: scene + renderer
&lt;/h2&gt;

&lt;p&gt;For a single static circle, &lt;code&gt;.render()&lt;/code&gt; is fine. For a chart with a dozen moving parts and an entry animation, you want two helpers doing the bookkeeping.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Scene&lt;/strong&gt; is a special group bound to a context. It owns the render lifecycle: clearing the surface, drawing children in z-order, and (critically for us later) re-rendering when the container resizes. A &lt;strong&gt;Renderer&lt;/strong&gt; wraps the scene in a &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop and gives you a &lt;code&gt;transition()&lt;/code&gt; method for animation. It's smart enough to stop ticking when nothing's happening, so it's not burning CPU while your chart sits still.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;createScene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;createRenderer&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ripl/web&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scene&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createScene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renderer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRenderer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines of setup, and we never have to think about the render loop again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping data to pixels with scales
&lt;/h2&gt;

&lt;p&gt;Every chart is, deep down, a function that turns data into coordinates. A revenue of &lt;code&gt;£82k&lt;/code&gt; needs to become "212 pixels from the top." D3 people call these functions &lt;strong&gt;scales&lt;/strong&gt;, and Ripl ships eleven of them.&lt;/p&gt;

&lt;p&gt;We only need the workhorse: &lt;code&gt;scaleContinuous&lt;/code&gt;, a linear map from an input &lt;strong&gt;domain&lt;/strong&gt; to an output &lt;strong&gt;range&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scaleContinuous&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ripl/web&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VALUE_SCALE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scaleContinuous&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MONTHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Feb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Apr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;May&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jun&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jul&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Aug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sep&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Oct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Nov&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Dec&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VALUES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;VALUE_SCALE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COLOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#6366f1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DURATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now here's the single most important idea in this whole article, and it's the reason the terminal trick works at the end:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every coordinate in the chart is derived from &lt;code&gt;context.width&lt;/code&gt; and &lt;code&gt;context.height&lt;/code&gt;. Nothing is hard-coded.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A chart built from fixed pixel values assumes it lives in a fixed-size box. A chart built from &lt;em&gt;proportions&lt;/em&gt; of its context fits any surface you give it: an 800px canvas, a fluid SVG, or an 80-column terminal. So all our geometry flows from the context's current size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getArea&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;marginX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;marginY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;marginX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;marginX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;marginY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;marginY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getScales&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;scaleContinuous&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;VALUES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;scaleContinuous&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;VALUES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getPoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;VALUES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;scales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;x&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;scales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;y&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;y&lt;/code&gt; scale runs from &lt;code&gt;area.bottom&lt;/code&gt; to &lt;code&gt;area.top&lt;/code&gt;, which is how you flip the browser's "y grows downward" into "up means more," for free, just by ordering the range.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drawing the furniture
&lt;/h2&gt;

&lt;p&gt;Now the elements. Ripl has all the primitives you'd expect: &lt;code&gt;arc&lt;/code&gt;, &lt;code&gt;circle&lt;/code&gt;, &lt;code&gt;rect&lt;/code&gt;, &lt;code&gt;line&lt;/code&gt;, &lt;code&gt;polyline&lt;/code&gt;, &lt;code&gt;polygon&lt;/code&gt;, &lt;code&gt;ellipse&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;. We'll use a handful.&lt;/p&gt;

&lt;p&gt;I'm creating every element with its geometry zeroed out and its opacity at &lt;code&gt;0&lt;/code&gt;. That's deliberate: we'll position everything in a single &lt;code&gt;layout()&lt;/code&gt; pass, and animate the opacity up as an entrance. Elements in Ripl are just stateful objects, so you can new them up now and mutate their properties whenever you like.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;createLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;createText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;createPolyline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;createCircle&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ripl/web&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gridLines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createLine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;y1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;x2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;y2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#a1a1aa&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lineWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tickLabels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gridLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#9ca3af&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;11px sans-serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;textAlign&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;textBaseline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;middle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// Label every OTHER month, so a narrow viewport (like a terminal!) stays legible.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;monthLabels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MONTHS&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#9ca3af&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;11px sans-serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;textAlign&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;textBaseline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createPolyline&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;COLOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lineWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;VALUES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createCircle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;COLOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lineWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That "label every other month" comment isn't decoration. It's foreshadowing. When this chart is 80 characters wide instead of 800 pixels, twelve labels won't fit but six will. Building for the narrowest target keeps you honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laying it all out
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;layout()&lt;/code&gt; is the one function that turns the current context size into concrete positions. Call it once now; we'll call it again on every resize.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;area&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getArea&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getScales&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scales&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxDomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;VALUES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ticks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gridLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxDomain&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;gridLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;gridLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;gridLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;y&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nx"&gt;gridLine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;gridLine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;gridLine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;gridLine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;tickLabels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;y&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;monthLabels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;x&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;gridLines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;tickLabels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;monthLabels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;markers&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scene.add()&lt;/code&gt; takes the whole flat list of elements. Under the hood the scene hoists them into a single buffer so rendering is O(n) rather than a recursive tree-walk, but you don't have to think about that. You just hand it your elements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it move
&lt;/h2&gt;

&lt;p&gt;Here's where Ripl stops being "nice" and starts being fun. &lt;code&gt;renderer.transition()&lt;/code&gt; animates an element (or an array of them) from its current state to a target &lt;code&gt;state&lt;/code&gt;, over a &lt;code&gt;duration&lt;/code&gt;, with an easing function. It returns a promise, so animations compose.&lt;/p&gt;

&lt;p&gt;First, the line drawing itself on, left to right. &lt;code&gt;interpolatePath&lt;/code&gt; is the magic ingredient. It interpolates the polyline's points so the stroke &lt;em&gt;grows&lt;/em&gt; along its length instead of just cross-fading into existence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;easeOutCubic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;interpolatePath&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ripl/web&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DURATION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;easeOutCubic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;interpolatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the markers. I want them to &lt;em&gt;pop&lt;/em&gt; in one after another, timed so each one appears just as the drawing line reaches it. &lt;code&gt;transition()&lt;/code&gt; accepts a &lt;strong&gt;callback&lt;/strong&gt; that runs per element and receives its index, so staggering is a one-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;DURATION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;easeOutCubic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;delay: (index / length) * DURATION&lt;/code&gt; is the entire stagger. First marker fires immediately, last one fires as the line finishes. Chef's kiss.&lt;/p&gt;

&lt;p&gt;Finally, fade the furniture up so the axes and labels arrive gently rather than blinking into place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gridLines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;easeOutCubic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;tickLabels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;monthLabels&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;easeOutCubic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One line for responsiveness
&lt;/h2&gt;

&lt;p&gt;Remember the scene emits a &lt;code&gt;resize&lt;/code&gt; event, and remember &lt;code&gt;layout()&lt;/code&gt; derives everything from the current context size? Wiring up full responsiveness is therefore this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resize the window, the container changes size, the scene fires &lt;code&gt;resize&lt;/code&gt;, &lt;code&gt;layout()&lt;/code&gt; re-runs against the new dimensions, and every element repositions itself. The renderer's already looping, so it just paints the new frame. No &lt;code&gt;ResizeObserver&lt;/code&gt; boilerplate, no debounce dance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff: same chart, different universe
&lt;/h2&gt;

&lt;p&gt;We now have a complete, animated, responsive line chart. Here's the moment I promised you.&lt;/p&gt;

&lt;p&gt;To render it as &lt;strong&gt;SVG&lt;/strong&gt; instead of Canvas, we change the context's import, and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ripl/svg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;createLine&lt;/code&gt;, every &lt;code&gt;scaleContinuous&lt;/code&gt;, every &lt;code&gt;renderer.transition&lt;/code&gt; above is byte-for-byte identical. The polyline that was a set of canvas draw commands is now a real &lt;code&gt;&amp;lt;polyline&amp;gt;&lt;/code&gt; node you can inspect in devtools. Same chart, different DOM.&lt;/p&gt;

&lt;p&gt;And now the party trick: &lt;strong&gt;the terminal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@ripl/terminal&lt;/code&gt; rasterizes your shapes onto a grid of Unicode braille characters (U+2800 to U+28FF). Each character cell packs a 2x4 grid of dots, so you get 8x the resolution of plain text, and it maps your colours to ANSI truecolor escape sequences.&lt;/p&gt;

&lt;p&gt;In Node, the drawing code is &lt;em&gt;still the same&lt;/em&gt;. Only the context creation changes, because a terminal needs an output to write to and a logical coordinate space to draw in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ripl/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;createNodeOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;createTerminalContext&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ripl/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createNodeOutput&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createTerminalContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Author in an 800x600 space; Ripl scales + letterboxes it into the character grid.&lt;/span&gt;
    &lt;span class="na"&gt;logicalWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;logicalHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;logicalWidth&lt;/code&gt; / &lt;code&gt;logicalHeight&lt;/code&gt; pair is the unsung hero. By default a terminal context's coordinate space &lt;em&gt;is&lt;/em&gt; the braille grid. An 80x24 terminal is a mere 160x96 pixels, and a chart authored in the hundreds would overflow it instantly. Give it a logical size and it uniformly scales and centres your scene into whatever grid it has, exactly the way a canvas maps CSS pixels onto its device backing store. Circles stay circular. Your 800x600 chart just fits.&lt;/p&gt;

&lt;p&gt;This is also the moment that "label every other month" pays off. On a canvas, cramming all twelve labels was fine. In 80 columns, six is all you get, and because we built for the narrow case from the start, the terminal version doesn't clip. It just works.&lt;/p&gt;

&lt;p&gt;So... the terminal setup is a &lt;em&gt;little&lt;/em&gt; more than one physical line, because a terminal isn't a DOM element, so you have to hand it an output and a logical size. But the part that actually matters does not change at all: &lt;strong&gt;your drawing code, your scales, your animations, your layout logic.&lt;/strong&gt; That's the promise. The surface is a detail you configure at the edge, and everything inside stays put.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually earns its keep
&lt;/h2&gt;

&lt;p&gt;The terminal thing is a fantastic demo, but it's not just a party trick:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD and monitoring&lt;/strong&gt;: render a trend chart straight into pipeline logs or a status dashboard, no browser, no screenshot service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI tools&lt;/strong&gt;: ship real visualisations in a dev tool without asking anyone to open a web page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side and headless&lt;/strong&gt;: generate the same chart on the server that you show in the browser, from one codebase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you don't fancy hand-rolling every axis and marker, &lt;code&gt;@ripl/charts&lt;/code&gt; gives you &lt;a href="https://www.ripl.run/charts/" rel="noopener noreferrer"&gt;25 pre-built&lt;/a&gt;, animated chart types (bar, area, pie, scatter, stock/candlestick, sankey, treemap, and more) with tooltips, legends, crosshairs and grids included. The from-scratch version we built here is the fun way to understand what's happening underneath.&lt;/p&gt;

&lt;p&gt;One honest caveat about the terminal target: it's the most constrained of the three. No pointer events, gradients, or transforms there yet, since braille dots can only do so much. On Canvas and SVG, the events, hit-testing, and interactivity are all there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go break it yourself
&lt;/h2&gt;

&lt;p&gt;The best part is that you don't have to take my word for any of this. The &lt;a href="https://www.ripl.run/playground.html#JTdCJTIyY29kZSUyMiUzQSUyMmltcG9ydCUyMCU3QiU1Q24lMjAlMjAlMjAlMjBjcmVhdGVDaXJjbGUlMkMlNUNuJTIwJTIwJTIwJTIwY3JlYXRlTGluZSUyQyU1Q24lMjAlMjAlMjAlMjBjcmVhdGVQb2x5bGluZSUyQyU1Q24lMjAlMjAlMjAlMjBjcmVhdGVUZXh0JTJDJTVDbiUyMCUyMCUyMCUyMGVhc2VPdXRDdWJpYyUyQyU1Q24lMjAlMjAlMjAlMjBpbnRlcnBvbGF0ZVBhdGglMkMlNUNuJTIwJTIwJTIwJTIwc2NhbGVDb250aW51b3VzJTJDJTVDbiU3RCUyMGZyb20lMjAnJTQwcmlwbCUyRndlYiclM0IlNUNuJTVDbiU1Q25jb25zdCUyME1PTlRIUyUyMCUzRCUyMCU1QidKYW4nJTJDJTIwJ0ZlYiclMkMlMjAnTWFyJyUyQyUyMCdBcHInJTJDJTIwJ01heSclMkMlMjAnSnVuJyUyQyUyMCdKdWwnJTJDJTIwJ0F1ZyclMkMlMjAnU2VwJyUyQyUyMCdPY3QnJTJDJTIwJ05vdiclMkMlMjAnRGVjJyU1RCUzQiU1Q25jb25zdCUyMFZBTFVFX1NDQUxFJTIwJTNEJTIwc2NhbGVDb250aW51b3VzKCU1QjAlMkMlMjAxJTVEJTJDJTIwJTVCNDAlMkMlMjAxMjAlNUQpJTNCJTIwJTJGJTJGJTIwZm9yJTIwY29sb3IlMjBpbnRlcnBvbGF0aW9uJTVDbmNvbnN0JTIwVkFMVUVTJTIwJTNEJTIwQXJyYXkuZnJvbSglN0IlMjBsZW5ndGglM0ElMjAxMiUyMCU3RCUyQyUyMCgpJTIwJTNEJTNFJTIwVkFMVUVfU0NBTEUoTWF0aC5yYW5kb20oKSkpJTNCJTVDbmNvbnN0JTIwQ09MT1IlMjAlM0QlMjAnJTIzNjM2NmYxJyUzQiU1Q25jb25zdCUyMERVUkFUSU9OJTIwJTNEJTIwMTYwMCUzQiU1Q24lNUNuJTJGJTJGJTIwQWxsJTIwZ2VvbWV0cnklMjBkZXJpdmVzJTIwZnJvbSUyMHRoZSUyMGNvbnRleHQlMjBzaXplJTJDJTIwc28lMjB0aGUlMjBjaGFydCUyMGZpdHMlMjBhbnklNUNuJTJGJTJGJTIwcmVuZGVyZXIlMjAlRTIlODAlOTQlMjBjYW52YXMlMkMlMjBTVkclMkMlMjBvciUyMGElMjB0ZXJtaW5hbC4lNUNuZnVuY3Rpb24lMjBnZXRBcmVhKCklMjAlN0IlNUNuJTIwJTIwJTIwJTIwY29uc3QlMjBtYXJnaW5YJTIwJTNEJTIwY29udGV4dC53aWR0aCUyMColMjAwLjElM0IlNUNuJTIwJTIwJTIwJTIwY29uc3QlMjBtYXJnaW5ZJTIwJTNEJTIwY29udGV4dC5oZWlnaHQlMjAqJTIwMC4xMiUzQiU1Q24lNUNuJTIwJTIwJTIwJTIwcmV0dXJuJTIwJTdCJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGxlZnQlM0ElMjBtYXJnaW5YJTJDJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMHJpZ2h0JTNBJTIwY29udGV4dC53aWR0aCUyMC0lMjBtYXJnaW5YJTJDJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMHRvcCUzQSUyMG1hcmdpblklMkMlNUNuJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwYm90dG9tJTNBJTIwY29udGV4dC5oZWlnaHQlMjAtJTIwbWFyZ2luWSUyQyU1Q24lMjAlMjAlMjAlMjAlN0QlM0IlNUNuJTdEJTVDbiU1Q25mdW5jdGlvbiUyMGdldFNjYWxlcyhhcmVhKSUyMCU3QiU1Q24lMjAlMjAlMjAlMjByZXR1cm4lMjAlN0IlNUNuJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIweCUzQSUyMHNjYWxlQ29udGludW91cyglNUIwJTJDJTIwVkFMVUVTLmxlbmd0aCUyMC0lMjAxJTVEJTJDJTIwJTVCYXJlYS5sZWZ0JTJDJTIwYXJlYS5yaWdodCU1RCklMkMlNUNuJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIweSUzQSUyMHNjYWxlQ29udGludW91cyglNUIwJTJDJTIwTWF0aC5tYXgoLi4uVkFMVUVTKSUyMColMjAxLjElNUQlMkMlMjAlNUJhcmVhLmJvdHRvbSUyQyUyMGFyZWEudG9wJTVEKSUyQyU1Q24lMjAlMjAlMjAlMjAlN0QlM0IlNUNuJTdEJTVDbiU1Q25mdW5jdGlvbiUyMGdldFBvaW50cyhzY2FsZXMpJTIwJTdCJTVDbiUyMCUyMCUyMCUyMHJldHVybiUyMFZBTFVFUy5tYXAoKHZhbHVlJTJDJTIwaW5kZXgpJTIwJTNEJTNFJTIwJTVCc2NhbGVzLngoaW5kZXgpJTJDJTIwc2NhbGVzLnkodmFsdWUpJTVEKSUzQiU1Q24lN0QlNUNuJTVDbiUyRiUyRiUyMC0tLSUyMEJ1aWxkJTIwdGhlJTIwY2hhcnQlMjBlbGVtZW50cyUyMChnZW9tZXRyeSUyMGlzJTIwYXNzaWduZWQlMjBieSUyMGxheW91dCUyMGJlbG93KSUyMC0tLSU1Q24lNUNuY29uc3QlMjBncmlkTGluZXMlMjAlM0QlMjAlNUIwJTJDJTIwMSUyQyUyMDIlMkMlMjAzJTVELm1hcCgoKSUyMCUzRCUzRSUyMGNyZWF0ZUxpbmUoJTdCJTVDbiUyMCUyMCUyMCUyMHgxJTNBJTIwMCUyQyU1Q24lMjAlMjAlMjAlMjB5MSUzQSUyMDAlMkMlNUNuJTIwJTIwJTIwJTIweDIlM0ElMjAwJTJDJTVDbiUyMCUyMCUyMCUyMHkyJTNBJTIwMCUyQyU1Q24lMjAlMjAlMjAlMjBzdHJva2UlM0ElMjAnJTIzYTFhMWFhJyUyQyU1Q24lMjAlMjAlMjAlMjBsaW5lV2lkdGglM0ElMjAxJTJDJTVDbiUyMCUyMCUyMCUyMG9wYWNpdHklM0ElMjAwJTJDJTVDbiU3RCkpJTNCJTVDbiU1Q25jb25zdCUyMHRpY2tMYWJlbHMlMjAlM0QlMjBncmlkTGluZXMubWFwKCgpJTIwJTNEJTNFJTIwY3JlYXRlVGV4dCglN0IlNUNuJTIwJTIwJTIwJTIweCUzQSUyMDAlMkMlNUNuJTIwJTIwJTIwJTIweSUzQSUyMDAlMkMlNUNuJTIwJTIwJTIwJTIwY29udGVudCUzQSUyMCcnJTJDJTVDbiUyMCUyMCUyMCUyMGZpbGwlM0ElMjAnJTIzOWNhM2FmJyUyQyU1Q24lMjAlMjAlMjAlMjBmb250JTNBJTIwJzExcHglMjBzYW5zLXNlcmlmJyUyQyU1Q24lMjAlMjAlMjAlMjB0ZXh0QWxpZ24lM0ElMjAncmlnaHQnJTJDJTVDbiUyMCUyMCUyMCUyMHRleHRCYXNlbGluZSUzQSUyMCdtaWRkbGUnJTJDJTVDbiUyMCUyMCUyMCUyMG9wYWNpdHklM0ElMjAwJTJDJTVDbiU3RCkpJTNCJTVDbiU1Q24lMkYlMkYlMjBMYWJlbCUyMGV2ZXJ5JTIwb3RoZXIlMjBtb250aCUyMHNvJTIwbmFycm93JTIwdmlld3BvcnRzJTIwKGxpa2UlMjBhJTIwdGVybWluYWwpJTIwc3RheSUyMGxlZ2libGUuJTVDbmNvbnN0JTIwbW9udGhMYWJlbHMlMjAlM0QlMjBNT05USFMuZmlsdGVyKChfJTJDJTIwaW5kZXgpJTIwJTNEJTNFJTIwaW5kZXglMjAlMjUlMjAyJTIwJTNEJTNEJTNEJTIwMCkubWFwKG1vbnRoJTIwJTNEJTNFJTIwY3JlYXRlVGV4dCglN0IlNUNuJTIwJTIwJTIwJTIweCUzQSUyMDAlMkMlNUNuJTIwJTIwJTIwJTIweSUzQSUyMDAlMkMlNUNuJTIwJTIwJTIwJTIwY29udGVudCUzQSUyMG1vbnRoJTJDJTVDbiUyMCUyMCUyMCUyMGZpbGwlM0ElMjAnJTIzOWNhM2FmJyUyQyU1Q24lMjAlMjAlMjAlMjBmb250JTNBJTIwJzExcHglMjBzYW5zLXNlcmlmJyUyQyU1Q24lMjAlMjAlMjAlMjB0ZXh0QWxpZ24lM0ElMjAnY2VudGVyJyUyQyU1Q24lMjAlMjAlMjAlMjB0ZXh0QmFzZWxpbmUlM0ElMjAndG9wJyUyQyU1Q24lMjAlMjAlMjAlMjBvcGFjaXR5JTNBJTIwMCUyQyU1Q24lN0QpKSUzQiU1Q24lNUNuY29uc3QlMjBiYXNlbGluZSUyMCUzRCUyMGNyZWF0ZUxpbmUoJTdCJTVDbiUyMCUyMCUyMCUyMHgxJTNBJTIwMCUyQyU1Q24lMjAlMjAlMjAlMjB5MSUzQSUyMDAlMkMlNUNuJTIwJTIwJTIwJTIweDIlM0ElMjAwJTJDJTVDbiUyMCUyMCUyMCUyMHkyJTNBJTIwMCUyQyU1Q24lMjAlMjAlMjAlMjBzdHJva2UlM0ElMjAnJTIzNzE3MTdhJyUyQyU1Q24lMjAlMjAlMjAlMjBsaW5lV2lkdGglM0ElMjAxJTJDJTVDbiUyMCUyMCUyMCUyMG9wYWNpdHklM0ElMjAwJTJDJTVDbiU3RCklM0IlNUNuJTVDbmNvbnN0JTIwbGluZSUyMCUzRCUyMGNyZWF0ZVBvbHlsaW5lKCU3QiU1Q24lMjAlMjAlMjAlMjBwb2ludHMlM0ElMjAlNUIlNUQlMkMlNUNuJTIwJTIwJTIwJTIwJTJGJTJGJTIwcmVuZGVyZXIlM0ElMjAnc3BsaW5lJyUyQyU1Q24lMjAlMjAlMjAlMjBzdHJva2UlM0ElMjBDT0xPUiUyQyU1Q24lMjAlMjAlMjAlMjBsaW5lV2lkdGglM0ElMjAzJTJDJTVDbiU3RCklM0IlNUNuJTVDbmNvbnN0JTIwbWFya2VycyUyMCUzRCUyMFZBTFVFUy5tYXAoKCklMjAlM0QlM0UlMjBjcmVhdGVDaXJjbGUoJTdCJTVDbiUyMCUyMCUyMCUyMGN4JTNBJTIwMCUyQyU1Q24lMjAlMjAlMjAlMjBjeSUzQSUyMDAlMkMlNUNuJTIwJTIwJTIwJTIwcmFkaXVzJTNBJTIwMCUyQyU1Q24lMjAlMjAlMjAlMjBmaWxsJTNBJTIwJyUyM2ZmZmZmZiclMkMlNUNuJTIwJTIwJTIwJTIwc3Ryb2tlJTNBJTIwQ09MT1IlMkMlNUNuJTIwJTIwJTIwJTIwbGluZVdpZHRoJTNBJTIwMiUyQyU1Q24lN0QpKSUzQiU1Q24lNUNuJTJGJTJGJTIwUG9zaXRpb25zJTIwZXZlcnklMjBlbGVtZW50JTIwZm9yJTIwdGhlJTIwY3VycmVudCUyMGNvbnRleHQlMjBzaXplLiU1Q25mdW5jdGlvbiUyMGxheW91dCgpJTIwJTdCJTVDbiUyMCUyMCUyMCUyMGNvbnN0JTIwYXJlYSUyMCUzRCUyMGdldEFyZWEoKSUzQiU1Q24lMjAlMjAlMjAlMjBjb25zdCUyMHNjYWxlcyUyMCUzRCUyMGdldFNjYWxlcyhhcmVhKSUzQiU1Q24lMjAlMjAlMjAlMjBjb25zdCUyMHBvaW50cyUyMCUzRCUyMGdldFBvaW50cyhzY2FsZXMpJTNCJTVDbiUyMCUyMCUyMCUyMCUyRiUyRiUyMEV2ZW5seSUyMHNwYWNlZCUyMHRpY2slMjB2YWx1ZXMlMjBhYm92ZSUyMHRoZSUyMGJhc2VsaW5lJTIwKHdoaWNoJTIwbWFya3MlMjB6ZXJvJTIwaXRzZWxmKS4lNUNuJTIwJTIwJTIwJTIwY29uc3QlMjBtYXhEb21haW4lMjAlM0QlMjBNYXRoLm1heCguLi5WQUxVRVMpJTIwKiUyMDEuMSUzQiU1Q24lMjAlMjAlMjAlMjBjb25zdCUyMHRpY2tzJTIwJTNEJTIwZ3JpZExpbmVzLm1hcCgoXyUyQyUyMGluZGV4KSUyMCUzRCUzRSUyMChtYXhEb21haW4lMjAqJTIwKGluZGV4JTIwJTJCJTIwMSkpJTIwJTJGJTIwZ3JpZExpbmVzLmxlbmd0aCklM0IlNUNuJTVDbiUyMCUyMCUyMCUyMGdyaWRMaW5lcy5mb3JFYWNoKChncmlkTGluZSUyQyUyMGluZGV4KSUyMCUzRCUzRSUyMCU3QiU1Q24lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBjb25zdCUyMHklMjAlM0QlMjBzY2FsZXMueSh0aWNrcyU1QmluZGV4JTVEKSUzQiU1Q24lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBncmlkTGluZS54MSUyMCUzRCUyMGFyZWEubGVmdCUzQiU1Q24lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBncmlkTGluZS54MiUyMCUzRCUyMGFyZWEucmlnaHQlM0IlNUNuJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwZ3JpZExpbmUueTElMjAlM0QlMjB5JTNCJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGdyaWRMaW5lLnkyJTIwJTNEJTIweSUzQiU1Q24lMjAlMjAlMjAlMjAlN0QpJTNCJTVDbiU1Q24lMjAlMjAlMjAlMjB0aWNrTGFiZWxzLmZvckVhY2goKGxhYmVsJTJDJTIwaW5kZXgpJTIwJTNEJTNFJTIwJTdCJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGxhYmVsLmNvbnRlbnQlMjAlM0QlMjBNYXRoLnJvdW5kKHRpY2tzJTVCaW5kZXglNUQpJTNCJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGxhYmVsLnglMjAlM0QlMjBhcmVhLmxlZnQlMjAtJTIwOCUzQiU1Q24lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBsYWJlbC55JTIwJTNEJTIwc2NhbGVzLnkodGlja3MlNUJpbmRleCU1RCklM0IlNUNuJTIwJTIwJTIwJTIwJTdEKSUzQiU1Q24lNUNuJTIwJTIwJTIwJTIwbW9udGhMYWJlbHMuZm9yRWFjaCgobGFiZWwlMkMlMjBpbmRleCklMjAlM0QlM0UlMjAlN0IlNUNuJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbGFiZWwueCUyMCUzRCUyMHNjYWxlcy54KGluZGV4JTIwKiUyMDIpJTNCJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGxhYmVsLnklMjAlM0QlMjBhcmVhLmJvdHRvbSUyMCUyQiUyMDglM0IlNUNuJTIwJTIwJTIwJTIwJTdEKSUzQiU1Q24lNUNuJTIwJTIwJTIwJTIwYmFzZWxpbmUueDElMjAlM0QlMjBhcmVhLmxlZnQlM0IlNUNuJTIwJTIwJTIwJTIwYmFzZWxpbmUueDIlMjAlM0QlMjBhcmVhLnJpZ2h0JTNCJTVDbiUyMCUyMCUyMCUyMGJhc2VsaW5lLnkxJTIwJTNEJTIwYXJlYS5ib3R0b20lM0IlNUNuJTIwJTIwJTIwJTIwYmFzZWxpbmUueTIlMjAlM0QlMjBhcmVhLmJvdHRvbSUzQiU1Q24lNUNuJTIwJTIwJTIwJTIwbGluZS5wb2ludHMlMjAlM0QlMjBwb2ludHMlM0IlNUNuJTVDbiUyMCUyMCUyMCUyMG1hcmtlcnMuZm9yRWFjaCgobWFya2VyJTJDJTIwaW5kZXgpJTIwJTNEJTNFJTIwJTdCJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMG1hcmtlci5jeCUyMCUzRCUyMHBvaW50cyU1QmluZGV4JTVEJTVCMCU1RCUzQiU1Q24lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBtYXJrZXIuY3klMjAlM0QlMjBwb2ludHMlNUJpbmRleCU1RCU1QjElNUQlM0IlNUNuJTIwJTIwJTIwJTIwJTdEKSUzQiU1Q24lNUNuJTIwJTIwJTIwJTIwcmV0dXJuJTIwcG9pbnRzJTNCJTVDbiU3RCU1Q24lNUNuY29uc3QlMjBwb2ludHMlMjAlM0QlMjBsYXlvdXQoKSUzQiU1Q24lNUNuc2NlbmUuYWRkKCU1Qi4uLmdyaWRMaW5lcyUyQyUyMC4uLnRpY2tMYWJlbHMlMkMlMjAuLi5tb250aExhYmVscyUyQyUyMGJhc2VsaW5lJTJDJTIwbGluZSUyQyUyMC4uLm1hcmtlcnMlNUQpJTNCJTVDbiU1Q24lMkYlMkYlMjAtLS0lMjBFbnRyeSUyMGFuaW1hdGlvbiUzQSUyMHRoZSUyMGxpbmUlMjBkcmF3cyUyMG9uJTIwbGVmdCVFMiU4NiU5MnJpZ2h0JTIwd2hpbGUlMjBtYXJrZXJzJTIwcG9wJTIwaW4lNUNuJTJGJTJGJTIwYXMlMjB0aGUlMjBmcm9udCUyMHBhc3NlcyUyMHRoZW0lMkMlMjBhbmQlMjB0aGUlMjBjaGFydCUyMGZ1cm5pdHVyZSUyMGZhZGVzJTIwdXAuJTIwLS0tJTVDbiU1Q25yZW5kZXJlci50cmFuc2l0aW9uKGxpbmUlMkMlMjAlN0IlNUNuJTIwJTIwJTIwJTIwZHVyYXRpb24lM0ElMjBEVVJBVElPTiUyQyU1Q24lMjAlMjAlMjAlMjBlYXNlJTNBJTIwZWFzZU91dEN1YmljJTJDJTVDbiUyMCUyMCUyMCUyMHN0YXRlJTNBJTIwJTdCJTIwcG9pbnRzJTNBJTIwaW50ZXJwb2xhdGVQYXRoKHBvaW50cyklMjAlN0QlMkMlNUNuJTdEKSUzQiU1Q24lNUNucmVuZGVyZXIudHJhbnNpdGlvbihtYXJrZXJzJTJDJTIwKG1hcmtlciUyQyUyMGluZGV4JTJDJTIwbGVuZ3RoKSUyMCUzRCUzRSUyMCglN0IlNUNuJTIwJTIwJTIwJTIwZHVyYXRpb24lM0ElMjAzNTAlMkMlNUNuJTIwJTIwJTIwJTIwZGVsYXklM0ElMjAoaW5kZXglMjAlMkYlMjBsZW5ndGgpJTIwKiUyMERVUkFUSU9OJTJDJTVDbiUyMCUyMCUyMCUyMGVhc2UlM0ElMjBlYXNlT3V0Q3ViaWMlMkMlNUNuJTIwJTIwJTIwJTIwc3RhdGUlM0ElMjAlN0IlMjByYWRpdXMlM0ElMjA0JTIwJTdEJTJDJTVDbiU3RCkpJTNCJTVDbiU1Q25yZW5kZXJlci50cmFuc2l0aW9uKGdyaWRMaW5lcyUyQyUyMCU3QiU1Q24lMjAlMjAlMjAlMjBkdXJhdGlvbiUzQSUyMDYwMCUyQyU1Q24lMjAlMjAlMjAlMjBlYXNlJTNBJTIwZWFzZU91dEN1YmljJTJDJTVDbiUyMCUyMCUyMCUyMHN0YXRlJTNBJTIwJTdCJTIwb3BhY2l0eSUzQSUyMDAuMzUlMjAlN0QlMkMlNUNuJTdEKSUzQiU1Q24lNUNucmVuZGVyZXIudHJhbnNpdGlvbiglNUIuLi50aWNrTGFiZWxzJTJDJTIwLi4ubW9udGhMYWJlbHMlMkMlMjBiYXNlbGluZSU1RCUyQyUyMCU3QiU1Q24lMjAlMjAlMjAlMjBkdXJhdGlvbiUzQSUyMDYwMCUyQyU1Q24lMjAlMjAlMjAlMjBlYXNlJTNBJTIwZWFzZU91dEN1YmljJTJDJTVDbiUyMCUyMCUyMCUyMHN0YXRlJTNBJTIwJTdCJTIwb3BhY2l0eSUzQSUyMDElMjAlN0QlMkMlNUNuJTdEKSUzQiU1Q24lNUNuc2NlbmUub24oJ3Jlc2l6ZSclMkMlMjBsYXlvdXQpJTNCJTVDbiUyMiUyQyUyMm1vZGUlMjIlM0ElMjIyZCUyMiUyQyUyMmNvbnRleHRUeXBlJTIyJTNBJTIyY2FudmFzJTIyJTdE" rel="noopener noreferrer"&gt;Ripl playground&lt;/a&gt; has this exact chart loaded and ready. There's a dropdown to flip the render target between Canvas, SVG, and Terminal, no import editing required. Just pick one and watch the same code repaint itself onto a completely different surface.&lt;/p&gt;

&lt;p&gt;Go change the target. Watch the braille dots assemble themselves into your line chart. Then tell me you're not a little bit delighted.&lt;/p&gt;

&lt;p&gt;I was. That's why I wrote this. And if after all that you're still not convinced, why not have a peek at how this extends into &lt;a href="https://www.ripl.run/docs/3d/" rel="noopener noreferrer"&gt;3D space&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts in the comments, or if you create something cool in the Ripl playground, post the link in the comments also.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cancelling network requests using Harlem actions</title>
      <dc:creator>Andrew Courtice</dc:creator>
      <pubDate>Wed, 10 Nov 2021 07:03:23 +0000</pubDate>
      <link>https://dev.to/andrewcourtice/cancelling-network-requests-using-harlem-actions-gka</link>
      <guid>https://dev.to/andrewcourtice/cancelling-network-requests-using-harlem-actions-gka</guid>
      <description>&lt;p&gt;Cancelling asynchronous tasks isn't necessarily a topic that comes up all too often in the JavaScript world but anyone who has ever attempted to come up with an elegant cancellation pattern will know it's a little more difficult to implement than it seems on the surface.&lt;/p&gt;

&lt;p&gt;In a lot of cases not cancelling stale asynchronous tasks has few consequences, but in that case of network requests this can lead to unexpected behaviour such as race conditions.&lt;/p&gt;

&lt;p&gt;This issue can be further complicated when network requests are spawned from a state management system where the result of the request is assigned to state. If uncancelled requests are allowed to complete, they may finish in an order you may not anticipate and thus corrupt your application state.&lt;/p&gt;

&lt;p&gt;Thankfully these days we have the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" rel="noopener noreferrer"&gt;AbortController&lt;/a&gt; which can be used as a cancellation token for things like network requests and DOM event listeners.&lt;/p&gt;

&lt;p&gt;In this article I will explain how &lt;a href="https://harlemjs.com" rel="noopener noreferrer"&gt;Harlem&lt;/a&gt; leverages the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" rel="noopener noreferrer"&gt;AbortController&lt;/a&gt; to create a robust and elegant async cancellation pattern through the use of actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Harlem
&lt;/h2&gt;

&lt;p&gt;I won't go into a great amount of detail here but in a nutshell Harlem is a simple, unopinionated, lightweight and extensible state management solution for Vue 3. It's suitable for applications and architectures of all sizes, and if you're a TypeScript user then you will feel right at home.&lt;/p&gt;

&lt;p&gt;Head on over to the &lt;a href="https://harlemjs.com" rel="noopener noreferrer"&gt;Harlem docs&lt;/a&gt; to learn more or check out a demo of it in action &lt;a href="https://andrewcourtice.github.io/harlem" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Let's start by installing a few packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @harlem/core @harlem/extension-action
&lt;span class="c"&gt;# or&lt;/span&gt;
yarn add @harlem/core @harlem/extension-action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a quick summary of the packages we just added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/harlem"&gt;@harlem&lt;/a&gt;/core&lt;/strong&gt;: this is the main Harlem package for creating and using a store in Vue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/harlem"&gt;@harlem&lt;/a&gt;/extension-action&lt;/strong&gt;: this extension adds async actions to your store&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Harlem has a good selection of official extensions you can use to extend your stores with some powerful features such as cancellable actions, async getters, local/session storage sync, tracing etc.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You can also add the devtools plugin (&lt;a class="mentioned-user" href="https://dev.to/harlem"&gt;@harlem&lt;/a&gt;/plugin-devtools) for inspecting your stores during development if you wish.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://harlemjs.com/extensibility/extensions/introduction.html#official-extensions" rel="noopener noreferrer"&gt;here&lt;/a&gt; for the full list of official extensions and plugins for Harlem.&lt;/p&gt;

&lt;p&gt;Once the above packages are installed you'll need to register Harlem with your Vue application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Harlem&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@harlem/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;devtoolsPlugin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@harlem/plugin-devtools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Optional&lt;/span&gt;

&lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Harlem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nf"&gt;devtoolsPlugin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// Optional&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating a simple store
&lt;/h2&gt;

&lt;p&gt;Once you've installed the packages and registered Harlem with your app it's time to create a store. Create a &lt;code&gt;store.ts&lt;/code&gt; (or js) file somewhere in your app and add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;actionExtension&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@harlem/extension-action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;createStore&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@harlem/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;people&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;starwars&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;STATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;actionExtension&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more information on stores, getters, mutations etc. check out the &lt;a href="https://harlemjs.com/guide/core-concepts/stores.html#defining-a-store" rel="noopener noreferrer"&gt;Harlem docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating an action
&lt;/h2&gt;

&lt;p&gt;Once we've got our store we can now add our action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadStarwarsCharacters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;load-starwars-characters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://swapi.dev/api/people&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;characters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;characters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how we assign &lt;code&gt;controller.signal&lt;/code&gt; to the &lt;code&gt;signal&lt;/code&gt; property to the body of the fetch request.&lt;/p&gt;

&lt;p&gt;Internally Harlem creates a new instance of an &lt;code&gt;AbortController&lt;/code&gt; each time the action is run so that when an instance of the action is cancelled, any child actions or network requests can be synchronised and cancelled as well. See &lt;a href="https://github.com/andrewcourtice/harlem/blob/main/packages/task/src/index.ts" rel="noopener noreferrer"&gt;here&lt;/a&gt; for how Harlem uses the &lt;code&gt;AbortController&lt;/code&gt; and makes it compatible with JavaScript's native &lt;code&gt;async/await&lt;/code&gt; syntax.&lt;/p&gt;

&lt;p&gt;The default behaviour for Harlem actions is to cancel running instances of actions upon subsequent calls. In other words, if you run the action 3 times consecutively and the first 2 instances haven't completed by the time the 3rd instance is started, Harlem will automatically abort the first 2.&lt;/p&gt;

&lt;p&gt;To change this behaviour simply specify &lt;code&gt;parallel: true&lt;/code&gt; in the action options like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadStarwarsCharacters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;load-starwars-characters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Our action body&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's all there is to it. You've now got a simple store that can handle complex async workflows as a first-class citizen as the application scales up.&lt;/p&gt;

&lt;p&gt;For more information about actions in Harlem, how to check their status, advanced cancellation patterns or child actions, check out the &lt;a href="https://harlemjs.com/extensibility/extensions/action.html" rel="noopener noreferrer"&gt;actions extension documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Have a great day and happy coding :)&lt;/p&gt;

</description>
      <category>vue</category>
      <category>typescript</category>
      <category>statemanagement</category>
    </item>
    <item>
      <title>Vue 3 State Management using Harlem</title>
      <dc:creator>Andrew Courtice</dc:creator>
      <pubDate>Sat, 07 Nov 2020 05:26:05 +0000</pubDate>
      <link>https://dev.to/andrewcourtice/vue-3-state-management-using-harlem-58hj</link>
      <guid>https://dev.to/andrewcourtice/vue-3-state-management-using-harlem-58hj</guid>
      <description>&lt;p&gt;Hi everyone&lt;/p&gt;

&lt;p&gt;Recently I shared with you a weather app called &lt;a href="https://github.com/andrewcourtice/ocula" rel="noopener noreferrer"&gt;Ocula&lt;/a&gt; which I built using Vue 3. Today I would like to announce another project I have been working on that was born out of &lt;a href="https://github.com/andrewcourtice/ocula" rel="noopener noreferrer"&gt;Ocula&lt;/a&gt;. The project is called &lt;a href="https://github.com/andrewcourtice/harlem" rel="noopener noreferrer"&gt;Harlem&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/andrewcourtice/harlem" rel="noopener noreferrer"&gt;Harlem&lt;/a&gt; is a new state management solution for Vue 3 that aims to be intuitive, lightweight, immutable and extensible. I wrote &lt;a href="https://github.com/andrewcourtice/harlem" rel="noopener noreferrer"&gt;Harlem&lt;/a&gt; because I wanted a simple state management solution that I could extend when necessary without having to incur the upfront cost in my bundle size for unused features. To achieve this &lt;a href="https://github.com/andrewcourtice/harlem" rel="noopener noreferrer"&gt;Harlem&lt;/a&gt; is split into 2 distinct categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The core package&lt;/li&gt;
&lt;li&gt;Plugins that extend core functionality&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The core package gives you a nice functional API for managing state in your application by creating stores and using state/getters/mutations to interact with said stores. Let's take a look at an example of how to create a simple store using &lt;a href="https://github.com/andrewcourtice/harlem" rel="noopener noreferrer"&gt;Harlem&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// store.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;createStore&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@harlem/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Smith&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;once&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;STATE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The createStore function returns all the methods necessary to now safely interact with state. By default all access to state is immutable and can only be modified through mutations. This immutability ensures changes to your state are predictable and (most importantly) auditable.&lt;/p&gt;

&lt;p&gt;Now that our store is created let's go ahead and define a simple getter for joining the first name and last name to output the user's full name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// getters.ts&lt;/span&gt;

&lt;span class="cm"&gt;/*
The state parameter here is immutable and cannot be assigned to.
*/&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fullname&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's also define some mutations for updating the first and last name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// mutations.ts&lt;/span&gt;

&lt;span class="cm"&gt;/*
The &amp;lt;string&amp;gt; generic tells harlem that we are expecting a payload of type string. This is optional and only applicable to TypeScript.

It's also worth noting that the state parameter here is the only place in Harlem that state is mutable.
*/&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setFirstName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;setFirstName&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setLastName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;setLastName&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all there is to it. You now have managed state that can be imported into Vue components. Here's a quick example of how you could use the above store in a component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Hello {{ fullName }}&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"First name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Last name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;defineComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;computed&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;fullName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;setFirstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;setLastName&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./stores/user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineComponent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;

    &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;setFirstName&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;setLastName&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;fullName&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here you can extend Harlem through the use of plugins to include &lt;a href="https://github.com/andrewcourtice/harlem/blob/main/plugins/devtools" rel="noopener noreferrer"&gt;Vue devtools integration&lt;/a&gt;, &lt;a href="https://github.com/andrewcourtice/harlem/blob/main/plugins/ssr" rel="noopener noreferrer"&gt;server-side rendering&lt;/a&gt;, &lt;a href="https://github.com/andrewcourtice/harlem/blob/main/plugins/storage" rel="noopener noreferrer"&gt;storage sync&lt;/a&gt;, &lt;a href="https://github.com/andrewcourtice/harlem/blob/main/plugins/transaction" rel="noopener noreferrer"&gt;transactions&lt;/a&gt;, &lt;a href="https://github.com/andrewcourtice/harlem/blob/main/plugins/snapshot" rel="noopener noreferrer"&gt;snapshots&lt;/a&gt; and &lt;a href="https://github.com/andrewcourtice/harlem/blob/main/plugins/reset" rel="noopener noreferrer"&gt;resetting&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Head over to the &lt;a href="https://github.com/andrewcourtice/harlem" rel="noopener noreferrer"&gt;repo&lt;/a&gt; to see how to install Harlem, the list of available plugins, or even how to write and publish your own plugin. Alternatively you can go to &lt;a href="https://harlemjs.com" rel="noopener noreferrer"&gt;harlemjs.com&lt;/a&gt; to see a live demo.&lt;/p&gt;

&lt;p&gt;Thanks for reading and happy coding!&lt;/p&gt;

</description>
      <category>vue</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Ocula - A weather app built on Vue 3</title>
      <dc:creator>Andrew Courtice</dc:creator>
      <pubDate>Thu, 24 Sep 2020 10:26:59 +0000</pubDate>
      <link>https://dev.to/andrewcourtice/ocula-a-free-weather-app-written-in-vue-3-2mdl</link>
      <guid>https://dev.to/andrewcourtice/ocula-a-free-weather-app-written-in-vue-3-2mdl</guid>
      <description>&lt;p&gt;Hi Everybody&lt;/p&gt;

&lt;p&gt;I wanted to share with you a project that I have been working on over the past few weeks: &lt;a href="https://app.ocula.io" rel="noopener noreferrer"&gt;Ocula&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ocula is a free PWA weather app written using the latest version of Vue. It is designed to look and feel native on mobile devices while also offering a nice desktop experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fcze4u4md6b3zozqjckey.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fcze4u4md6b3zozqjckey.png" alt="Alt Text" width="800" height="1732"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some of the features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No location restrictions - available worldwide&lt;/li&gt;
&lt;li&gt;Daily forecast for up to 7 days&lt;/li&gt;
&lt;li&gt;Hourly forecast data for up to 24 hours&lt;/li&gt;
&lt;li&gt;Trend charts for hourly temp, rainfall and wind&lt;/li&gt;
&lt;li&gt;Interactive weather maps with 6 different map types (radar, precipitation, temp, cloud, wind, pressure)&lt;/li&gt;
&lt;li&gt;Frame-by-frame playback for radar images to visualise incoming rain&lt;/li&gt;
&lt;li&gt;Dark/Light Themes. Default theme changes based on current time of day&lt;/li&gt;
&lt;li&gt;Options to reorder or hide forecast sections, set your preferred map type, units and more&lt;/li&gt;
&lt;li&gt;Open-source, privacy friendly, and best of all - free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app is currently in an alpha phase but I'm hoping to get some early feedback from the community to address some glaring bugs/UX issues.&lt;/p&gt;

&lt;p&gt;Head over to &lt;a href="https://app.ocula.io" rel="noopener noreferrer"&gt;app.ocula.io&lt;/a&gt; to check out the app and let me know what you think. The source is also available &lt;a href="https://github.com/andrewcourtice/ocula" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks! &lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
