Back to the library

Chart — live window

Variant: Live window

Not an entrance but the state a chart settles into: a window that advances one sample every 400ms in a 150ms slide, then holds still. A continuous glide would claim the series is continuous when it is a sequence of discrete readings, and the pause between beats is what makes each arrival countable. The newest bar holds clay for one beat and decays to oat over 300ms — colour is the only thing marking now, because on a chart that runs all day the leading edge has to be findable without being loud. Old readings age out under a gradient mask instead of being cut off at the frame, and the series repeats on a twelve-sample period, exactly one window wide, so the loop closes with no seam.

datachartstatussvgloop

The source

A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Chart — live window</title>
    <style>
      :root {
        /* Neutrals — fixed, never remixed. */
        --ivory: #faf9f5;
        --paper: #ffffff;
        --oat: #e3dacc;
        --slate: #141413;

        /* Knobs — the remix API. A knob exists iff declared here and used below. */
        --hue: 38.8;
        --lift: 0;
        --chroma: 1;
        --round: 1;
        --speed: 1;

        /* Accents — derived from the knobs with the family's offsets baked in,
           so one hue roll moves them together. Defaults reproduce the original hex. */
        --clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
        --clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
        --olive: oklch(
          calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3)
        );
        --rust: oklch(
          calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3)
        );

        --_muted: color-mix(in srgb, var(--slate) 45%, transparent);

        /* Clock — twelve samples at 400ms, which is exactly one window. The
           series repeats on that period, so the loop closes on an identical
           frame and there is no seam to hide. */
        --cycle: 4.8s;
        --t: calc(var(--cycle) / var(--speed));
        --_beat: calc(400ms / var(--speed));

        --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
      }

      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }

      body {
        min-height: 100vh;
        display: grid;
        place-items: center;
        background: var(--ivory);
        font-family:
          system-ui,
          -apple-system,
          'Segoe UI',
          Roboto,
          sans-serif;
        color: var(--slate);
      }

      .tile {
        width: min(320px, 90vw);
        display: grid;
        gap: 12px;
        padding: 18px 22px 20px;
        border: 1px solid var(--oat);
        border-radius: calc(14px * var(--round));
        background: var(--paper);
      }

      .label {
        font-size: 12px;
        font-weight: 600;
        letter-spacing: 0.14em;
        text-transform: uppercase;
        color: var(--_muted);
      }

      /* Readings age out of the left edge instead of being cut off by it. The
         mask is alpha rather than luminance, so it takes any opaque token and
         never touches hue. */
      svg {
        display: block;
        width: 100%;
        height: auto;
        -webkit-mask-image: linear-gradient(to right, transparent 0, var(--slate) 9%);
        mask-image: linear-gradient(to right, transparent 0, var(--slate) 9%);
      }

      .axis {
        fill: var(--oat);
      }

      /* One sample per beat, moved in 150ms and then held still. A continuous
         glide would claim the series is continuous; it is twelve discrete
         readings, and the window should advance the way they arrive. */
      .row {
        animation: scroll var(--t) linear infinite;
      }

      @keyframes scroll {
        0% {
          transform: translateX(0px);
          animation-timing-function: var(--ease-out);
        }
        3.125%,
        8.333% {
          transform: translateX(-24px);
          animation-timing-function: var(--ease-out);
        }
        11.458%,
        16.667% {
          transform: translateX(-48px);
          animation-timing-function: var(--ease-out);
        }
        19.792%,
        25% {
          transform: translateX(-72px);
          animation-timing-function: var(--ease-out);
        }
        28.125%,
        33.333% {
          transform: translateX(-96px);
          animation-timing-function: var(--ease-out);
        }
        36.458%,
        41.667% {
          transform: translateX(-120px);
          animation-timing-function: var(--ease-out);
        }
        44.792%,
        50% {
          transform: translateX(-144px);
          animation-timing-function: var(--ease-out);
        }
        53.125%,
        58.333% {
          transform: translateX(-168px);
          animation-timing-function: var(--ease-out);
        }
        61.458%,
        66.667% {
          transform: translateX(-192px);
          animation-timing-function: var(--ease-out);
        }
        69.792%,
        75% {
          transform: translateX(-216px);
          animation-timing-function: var(--ease-out);
        }
        78.125%,
        83.333% {
          transform: translateX(-240px);
          animation-timing-function: var(--ease-out);
        }
        86.458%,
        91.667% {
          transform: translateX(-264px);
          animation-timing-function: var(--ease-out);
        }
        94.792%,
        100% {
          transform: translateX(-288px);
        }
      }

      .bar {
        fill: var(--oat);
        rx: calc(2px * var(--round));
        animation: fresh var(--t) linear infinite;
        /* Negative, so every bar is already mid-cycle at load: bar n is lit as
           it slides into the right-hand slot, twelve beats before its turn comes
           round again. */
        animation-delay: calc((var(--_i) - 12) * var(--_beat));
      }

      /* Twelve phases, not twenty-four: a bar and the one twelve slots behind it
         are the same reading a window apart, so they share an index. */
      .bar:nth-child(12n + 1) {
        --_i: 0;
      }
      .bar:nth-child(12n + 2) {
        --_i: 1;
      }
      .bar:nth-child(12n + 3) {
        --_i: 2;
      }
      .bar:nth-child(12n + 4) {
        --_i: 3;
      }
      .bar:nth-child(12n + 5) {
        --_i: 4;
      }
      .bar:nth-child(12n + 6) {
        --_i: 5;
      }
      .bar:nth-child(12n + 7) {
        --_i: 6;
      }
      .bar:nth-child(12n + 8) {
        --_i: 7;
      }
      .bar:nth-child(12n + 9) {
        --_i: 8;
      }
      .bar:nth-child(12n + 10) {
        --_i: 9;
      }
      .bar:nth-child(12n + 11) {
        --_i: 10;
      }
      .bar:nth-child(12n + 12) {
        --_i: 11;
      }

      /* The bar lights when it lands, not when it starts moving: it rides in oat
         for the 150ms of the slide, holds clay for the beat it owns, then decays
         over 300ms as the next one takes over. Colour is the only thing marking
         now — nothing scales, nothing pulses, because on a chart that runs all
         day the leading edge has to be findable without being loud. The 150ms
         offset is also load-bearing: a bar and its twin twelve slots back share
         a phase, and by 150ms the twin has left the frame entirely. */
      @keyframes fresh {
        0%,
        3.125% {
          fill: var(--oat);
          animation-timing-function: var(--ease-out);
        }
        4.8%,
        11.458% {
          fill: var(--clay);
          animation-timing-function: var(--ease-out);
        }
        17.7%,
        100% {
          fill: var(--oat);
        }
      }

      @media (prefers-reduced-motion: reduce) {
        /* The window stops advancing — scrolling telemetry is the exact motion
           this setting is asking about. The leading sample keeps breathing on a
           slow cycle so the reading still reads as live. */
        .row {
          animation: none;
        }

        .bar {
          animation: none;
        }

        .bar:nth-child(12) {
          animation: still-fresh calc(var(--t) * 1.5) var(--ease-out) infinite;
        }

        @keyframes still-fresh {
          0%,
          22% {
            fill: var(--clay);
          }
          58%,
          100% {
            fill: var(--oat);
          }
        }
      }
    </style>
  </head>
  <body>
    <div class="tile">
      <span class="label">CPU / 5s</span>
      <svg
        viewBox="0 0 288 132"
        role="img"
        aria-label="A live bar chart whose window advances one sample every four hundred milliseconds, the newest bar held in clay"
      >
        <g class="row">
          <rect class="bar" x="4.5" y="82" width="15" height="38" />
          <rect class="bar" x="28.5" y="68" width="15" height="52" />
          <rect class="bar" x="52.5" y="76" width="15" height="44" />
          <rect class="bar" x="76.5" y="59" width="15" height="61" />
          <rect class="bar" x="100.5" y="65" width="15" height="55" />
          <rect class="bar" x="124.5" y="48" width="15" height="72" />
          <rect class="bar" x="148.5" y="56" width="15" height="64" />
          <rect class="bar" x="172.5" y="71" width="15" height="49" />
          <rect class="bar" x="196.5" y="62" width="15" height="58" />
          <rect class="bar" x="220.5" y="43" width="15" height="77" />
          <rect class="bar" x="244.5" y="54" width="15" height="66" />
          <rect class="bar" x="268.5" y="77" width="15" height="43" />
          <rect class="bar" x="292.5" y="82" width="15" height="38" />
          <rect class="bar" x="316.5" y="68" width="15" height="52" />
          <rect class="bar" x="340.5" y="76" width="15" height="44" />
          <rect class="bar" x="364.5" y="59" width="15" height="61" />
          <rect class="bar" x="388.5" y="65" width="15" height="55" />
          <rect class="bar" x="412.5" y="48" width="15" height="72" />
          <rect class="bar" x="436.5" y="56" width="15" height="64" />
          <rect class="bar" x="460.5" y="71" width="15" height="49" />
          <rect class="bar" x="484.5" y="62" width="15" height="58" />
          <rect class="bar" x="508.5" y="43" width="15" height="77" />
          <rect class="bar" x="532.5" y="54" width="15" height="66" />
          <rect class="bar" x="556.5" y="77" width="15" height="43" />
        </g>
        <rect class="axis" x="0" y="120" width="288" height="1.5" />
      </svg>
    </div>
  </body>
</html>

The variant family

Same idea, one axis moved. Compare them side by side before you commit.