> ## Documentation Index
> Fetch the complete documentation index at: https://hyperframes.heygen.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Bar Chart Race

> Ranked bars grow and overtake each other as a time series advances, with the axis rescaling as values climb and the accent handing over to whoever leads

export const InstallCommand = ({command}) => {
  const [copied, setCopied] = React.useState(false);
  const copy = async () => {
    try {
      if (navigator.clipboard && window.isSecureContext) {
        await navigator.clipboard.writeText(command);
      } else {
        const previous = document.activeElement;
        const scratch = document.createElement("textarea");
        scratch.value = command;
        scratch.setAttribute("readonly", "");
        scratch.style.position = "fixed";
        scratch.style.opacity = "0";
        document.body.appendChild(scratch);
        scratch.select();
        document.execCommand("copy");
        document.body.removeChild(scratch);
        previous?.focus?.();
      }
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch {}
  };
  return <div className="hf-install-command not-prose my-4 flex items-stretch overflow-hidden rounded-xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900">
      <code className="flex-1 overflow-x-auto whitespace-nowrap border-r border-zinc-200 px-4 py-3 font-mono text-sm text-zinc-800 dark:border-zinc-800 dark:text-zinc-100">
        {command}
      </code>
      <button type="button" onClick={copy} data-copied={copied ? "true" : "false"} aria-label={`Copy ${command} to the clipboard`} className="hf-install-copy">
        <svg className="hf-install-copy-clipboard" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M14.25 5.25H7.25C6.14543 5.25 5.25 6.14543 5.25 7.25V14.25C5.25 15.3546 6.14543 16.25 7.25 16.25H14.25C15.3546 16.25 16.25 15.3546 16.25 14.25V7.25C16.25 6.14543 15.3546 5.25 14.25 5.25Z" />
          <path d="M2.80103 11.998L1.77203 5.07397C1.61003 3.98097 2.36403 2.96397 3.45603 2.80197L10.38 1.77297C11.313 1.63397 12.19 2.16297 12.528 3.00097" />
        </svg>
        <svg className="hf-install-copy-check" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M2.75 9.5L6.5 13.25L15.25 4.5" />
        </svg>
      </button>
      <span className="hf-install-copy-status" role="status" aria-live="polite">
        {copied ? "Copied" : ""}
      </span>
    </div>;
};

<iframe className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800" title="bar-chart-race preview" loading="lazy" srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/bar-chart-race.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/bar-chart-race.png");document.body.appendChild(p)});<\/script></body></html>`} />

## Install

<InstallCommand command="npx hyperframes add bar-chart-race" />

That writes one file: `compositions/bar-chart-race.html`.

## Add it to your video

It runs for 12 seconds at 1920×1080. Paste this into your composition:

```html index.html theme={null}
<div
  data-composition-id="bar-chart-race"
  data-composition-src="compositions/bar-chart-race.html"
  data-start="0"
  data-duration="12"
  data-track-index="1"
  data-width="1920"
  data-height="1080"
></div>
```

Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.

## Change how it looks

Set these CSS variables on the block:

* `title` — Title. Defaults to `Streaming Subscribers by Service`.
* `subtitle` — Subtitle. Defaults to `Ranked by reported subscribers`.
* `periods` — Period labels (comma separated). Defaults to `2019, 2020, 2021, 2022, 2023, 2024`.
* `series` — Series (one per line: Name: v1, v2, ...). Defaults to `Northwind: 42, 58, 71, 96, 118, 131
  Cobalt: 30, 46, 68, 92, 126, 168
  Ferry: 55, 62, 66, 70, 74, 79
  Marlow: 18, 33, 52, 61, 88, 104
  Aster: 25, 28, 44, 58, 63, 72
  Pell: 12, 20, 39, 47, 55, 90
  Quill: 8, 11, 15, 24, 40, 66
  Dunmore: 35, 37, 38, 40, 42, 44`.
* `barCount` — Bars shown. Defaults to `6`.
* `periodDuration` — Seconds per period. Defaults to `2`.
* `valuePrefix` — Value prefix. Defaults to `$`.
* `valueSuffix` — Value suffix. Defaults to `M`.
* `valueDecimals` — Value decimals. Defaults to `0`.
* `accent` — Leader accent. Defaults to `#c8452d`.

## Variables

Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:

| Variable                          | Default                               | Accepts            | What it does |
| --------------------------------- | ------------------------------------- | ------------------ | ------------ |
| `title`                           | `Streaming Subscribers by Service`    | string             |              |
| `subtitle`                        | `Ranked by reported subscribers`      | string             |              |
| `periods`                         | `2019, 2020, 2021, 2022, 2023, 2024`  | string             |              |
| `series`                          | \`Northwind: 42, 58, 71, 96, 118, 131 |                    |              |
| Cobalt: 30, 46, 68, 92, 126, 168  |                                       |                    |              |
| Ferry: 55, 62, 66, 70, 74, 79     |                                       |                    |              |
| Marlow: 18, 33, 52, 61, 88, 104   |                                       |                    |              |
| Aster: 25, 28, 44, 58, 63, 72     |                                       |                    |              |
| Pell: 12, 20, 39, 47, 55, 90      |                                       |                    |              |
| Quill: 8, 11, 15, 24, 40, 66      |                                       |                    |              |
| Dunmore: 35, 37, 38, 40, 42, 44\` | string                                |                    |              |
| `barCount`                        | `6`                                   | 3 to 12, step 1    |              |
| `periodDuration`                  | `2`                                   | 0.4 to 6, step 0.1 |              |
| `valuePrefix`                     | `$`                                   | string             |              |
| `valueSuffix`                     | `M`                                   | string             |              |
| `valueDecimals`                   | `0`                                   | 0 to 3, step 1     |              |
| `accent`                          | `#c8452d`                             | color              |              |

Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:

```html wrap theme={null}
<div
  data-composition-id="bar-chart-race"
  data-composition-src="compositions/bar-chart-race.html"
  data-variable-values='{"title":"Streaming Subscribers by Service","subtitle":"Ranked by reported subscribers","periods":"2019, 2020, 2021, 2022, 2023, 2024","series":"Northwind: 42, 58, 71, 96, 118, 131\nCobalt: 30, 46, 68, 92, 126, 168\nFerry: 55, 62, 66, 70, 74, 79\nMarlow: 18, 33, 52, 61, 88, 104\nAster: 25, 28, 44, 58, 63, 72\nPell: 12, 20, 39, 47, 55, 90\nQuill: 8, 11, 15, 24, 40, 66\nDunmore: 35, 37, 38, 40, 42, 44","barCount":6,"periodDuration":2,"valuePrefix":"$","valueSuffix":"M","valueDecimals":0,"accent":"#c8452d"}'
></div>
```

## Source

<Accordion title={`bar-chart-race.html`}>
  ```html theme={null}
  <!doctype html>
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=1920, height=1080" />
      <title>Bar Chart Race</title>
      <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
      <style>
        *,
        *::before,
        *::after {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }
        html,
        body {
          width: 1920px;
          height: 1080px;
          overflow: hidden;
          background-color: #f5f3ef;
        }
      </style>
    </head>
    <body>
      <div
        id="bcr-root"
        data-composition-id="bar-chart-race"
        data-width="1920"
        data-height="1080"
        data-start="0"
        data-duration="12"
        data-composition-variables='[
          {"id":"title","type":"string","label":"Title","default":"Streaming Subscribers by Service"},
          {"id":"subtitle","type":"string","label":"Subtitle","default":"Ranked by reported subscribers"},
          {"id":"periods","type":"string","label":"Period labels (comma separated)","default":"2019, 2020, 2021, 2022, 2023, 2024"},
          {"id":"series","type":"string","label":"Series (one per line: Name: v1, v2, ...)","default":"Northwind: 42, 58, 71, 96, 118, 131\nCobalt: 30, 46, 68, 92, 126, 168\nFerry: 55, 62, 66, 70, 74, 79\nMarlow: 18, 33, 52, 61, 88, 104\nAster: 25, 28, 44, 58, 63, 72\nPell: 12, 20, 39, 47, 55, 90\nQuill: 8, 11, 15, 24, 40, 66\nDunmore: 35, 37, 38, 40, 42, 44"},
          {"id":"barCount","type":"number","label":"Bars shown","default":6,"min":3,"max":12,"step":1},
          {"id":"periodDuration","type":"number","label":"Seconds per period","default":2,"min":0.4,"max":6,"step":0.1},
          {"id":"valuePrefix","type":"string","label":"Value prefix","default":"$"},
          {"id":"valueSuffix","type":"string","label":"Value suffix","default":"M"},
          {"id":"valueDecimals","type":"number","label":"Value decimals","default":0,"min":0,"max":3,"step":1},
          {"id":"accent","type":"color","label":"Leader accent","default":"#c8452d"}
        ]'
      >
        <div id="bcr-stage" class="clip" data-start="0" data-duration="12" data-track-index="0">
          <div id="bcr-bg"></div>

          <div id="bcr-head">
            <div id="bcr-head-left">
              <h1 id="bcr-title">Streaming Subscribers by Service</h1>
              <p id="bcr-subtitle">Ranked by reported subscribers</p>
            </div>
            <div id="bcr-head-right">
              <span id="bcr-period-caption">Period</span>
              <span id="bcr-period">2019</span>
            </div>
          </div>

          <!-- Rows live here, one per series, positioned by rank. -->
          <div id="bcr-plot"></div>

          <!-- Axis is painted above the rows: the tick labels sliding left is the
               visible evidence that the scale is rescaling as values climb. -->
          <div id="bcr-axis"></div>

          <p id="bcr-source">Placeholder data</p>
        </div>

        <style>
          #bcr-root {
            position: relative;
            width: 1920px;
            height: 1080px;
            overflow: hidden;
            font-family: Inter, sans-serif;
            color: #1f1d1b;
          }

          #bcr-stage {
            position: absolute;
            inset: 0;
          }

          /* Scene fill goes on a full-bleed child, never the composition root. */
          #bcr-bg {
            position: absolute;
            inset: 0;
            background-color: #f5f3ef;
          }

          #bcr-head {
            position: absolute;
            top: 58px;
            left: 96px;
            width: 1728px;
            display: flex;
            align-items: flex-start;
            justify-content: space-between;
          }

          #bcr-title {
            font-size: 46px;
            font-weight: 700;
            letter-spacing: -0.015em;
            line-height: 1.1;
          }

          #bcr-subtitle {
            margin-top: 10px;
            font-size: 19px;
            font-weight: 400;
            color: #6b6560;
          }

          #bcr-head-right {
            display: flex;
            flex-direction: column;
            align-items: flex-end;
          }

          #bcr-period-caption {
            font-size: 15px;
            font-weight: 600;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            color: #6b6560;
          }

          #bcr-period {
            margin-top: 4px;
            font-size: 72px;
            font-weight: 700;
            line-height: 1;
            font-variant-numeric: tabular-nums;
            letter-spacing: -0.02em;
          }

          #bcr-plot {
            position: absolute;
            left: 0;
            top: 236px;
            width: 1920px;
            height: 760px;
            overflow: hidden;
          }

          .bcr-row {
            position: absolute;
            left: 0;
            top: 0;
            width: 1920px;
          }

          /* Rows are transparent so both bars of an overtake stay visible sliding
             past each other; only the two TEXT blocks carry an opaque backing, so
             the front row's label covers the one it crosses instead of tangling
             with it. */
          .bcr-name {
            position: absolute;
            left: 96px;
            width: 272px;
            top: 50%;
            transform: translateY(-50%);
            text-align: right;
            font-size: 26px;
            font-weight: 600;
            line-height: 1.7;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            background-color: #f5f3ef;
          }

          .bcr-bar {
            position: absolute;
            left: 400px;
            top: 50%;
            transform: translateY(-50%);
            border-radius: 3px;
            background-color: #1f1d1b;
          }

          .bcr-value {
            position: absolute;
            left: 404px;
            top: 50%;
            padding: 0 12px;
            font-size: 26px;
            font-weight: 600;
            line-height: 1.7;
            font-variant-numeric: tabular-nums;
            white-space: nowrap;
            background-color: #f5f3ef;
          }

          #bcr-axis {
            position: absolute;
            inset: 0;
            z-index: 5000;
          }

          .bcr-tick-line {
            position: absolute;
            top: 232px;
            left: 0;
            width: 1px;
            height: 768px;
            background-color: rgba(31, 29, 27, 0.11);
          }

          .bcr-tick-line.bcr-tick-zero {
            background-color: rgba(31, 29, 27, 0.5);
          }

          .bcr-tick-label {
            position: absolute;
            top: 198px;
            left: 0;
            font-size: 17px;
            font-weight: 500;
            font-variant-numeric: tabular-nums;
            color: #6b6560;
            white-space: nowrap;
          }

          #bcr-source {
            position: absolute;
            left: 96px;
            top: 1014px;
            font-size: 16px;
            color: #6b6560;
          }
        </style>

        <script>
          (function () {
            "use strict";

            // ---- variables -------------------------------------------------
            // The declaration attribute is the single source of truth: when the
            // runtime is absent (raw file open) we parse the same attribute.
            function readVars() {
              // Read the declaration off THIS block's own root, never
              // document.documentElement: mounted as a sub-composition the block's
              // <html> is discarded and documentElement is the HOST's, so a
              // documentElement read returns the wrong element (or null) and the
              // block renders with no series at all. Runtime / host / --variables
              // values are layered on top as overrides, so the block does not
              // depend on the runtime being injected before this script runs.
              var out = {};
              var root = document.getElementById("bcr-root");
              try {
                JSON.parse(root.getAttribute("data-composition-variables")).forEach(function (d) {
                  out[d.id] = d.default;
                });
              } catch (err) {
                // Declaration unreadable; the per-variable literal fallbacks below
                // still produce a renderable composition.
              }
              var hf = window.__hyperframes;
              var overrides =
                hf && typeof hf.getVariables === "function"
                  ? hf.getVariables()
                  : window.__hfVariables;
              if (overrides && typeof overrides === "object") {
                for (var key in overrides) {
                  if (overrides[key] !== undefined) out[key] = overrides[key];
                }
              }
              return out;
            }

            function num(value, fallback, lo, hi) {
              var n = typeof value === "number" ? value : Number.parseFloat(value);
              if (!Number.isFinite(n)) n = fallback;
              return Math.min(hi, Math.max(lo, n));
            }

            function str(value, fallback) {
              return typeof value === "string" && value.length > 0 ? value : fallback;
            }

            var V = readVars();
            var ACCENT = str(V.accent, "#c8452d");
            var BAR_COLOR = "#1f1d1b";
            var BAR_COUNT = Math.round(num(V.barCount, 6, 1, 24));
            var PERIOD_DURATION = num(V.periodDuration, 2, 0.1, 30);
            var DECIMALS = Math.round(num(V.valueDecimals, 0, 0, 6));
            var PREFIX = typeof V.valuePrefix === "string" ? V.valuePrefix : "";
            var SUFFIX = typeof V.valueSuffix === "string" ? V.valueSuffix : "";

            // ---- data format ------------------------------------------------
            // Wide table, one series per line: "Name: v1, v2, v3".
            // `;` is accepted as a line separator so the whole table survives a
            // single-line text input. Short rows hold their last value.
            function parsePeriods(text) {
              return String(text)
                .split(",")
                .map(function (s) {
                  return s.trim();
                })
                .filter(function (s) {
                  return s.length > 0;
                });
            }

            function parseSeries(text, periodCount) {
              var out = [];
              var lines = String(text).split(/[\n;]+/);
              for (var i = 0; i < lines.length; i++) {
                var line = lines[i].trim();
                if (!line) continue;
                var split = line.indexOf(":");
                if (split < 0) continue;
                var label = line.slice(0, split).trim();
                if (!label) continue;
                var values = line
                  .slice(split + 1)
                  .split(",")
                  .map(function (s) {
                    return Number.parseFloat(s.trim());
                  })
                  .filter(function (n) {
                    return Number.isFinite(n);
                  });
                if (values.length === 0) continue;
                while (values.length < periodCount) values.push(values[values.length - 1]);
                out.push({ label: label, values: values.slice(0, periodCount) });
              }
              return out;
            }

            var PERIODS = parsePeriods(str(V.periods, "2019, 2020, 2021, 2022, 2023, 2024"));
            if (PERIODS.length === 0) PERIODS = ["1"];
            var SERIES = parseSeries(str(V.series, ""), PERIODS.length);
            var T = PERIODS.length;
            var N = SERIES.length;

            // ---- closed-form state ------------------------------------------
            // k = 10 interpolated keyframes per period (Bostock's number). Rank is
            // computed at each keyframe from the interpolated value and baked once;
            // a bar's on-screen row is then solved FROM rank, so two bars can never
            // cross without swapping. Everything below is a pure function of t.
            var K = 10;
            var KF_DURATION = PERIOD_DURATION / K;
            var KF_COUNT = T > 1 ? (T - 1) * K + 1 : 1;
            var RACE_SECONDS = T > 1 ? (T - 1) * PERIOD_DURATION : 0;

            function clamp(v, lo, hi) {
              return v < lo ? lo : v > hi ? hi : v;
            }

            function valueAt(t, series) {
              if (T < 2) return series.values[0];
              var u = t / PERIOD_DURATION;
              var i = clamp(Math.floor(u), 0, T - 2);
              var f = clamp(u - i, 0, 1);
              return series.values[i] + (series.values[i + 1] - series.values[i]) * f;
            }

            // ranks[m][j] = integer rank of series j at keyframe m (0 = leader).
            var RANKS = [];
            for (var m = 0; m < KF_COUNT; m++) {
              var tm = m * KF_DURATION;
              var order = SERIES.map(function (s, j) {
                return { j: j, v: valueAt(tm, s) };
              });
              // Descending by value; index breaks ties so the sort is total and
              // therefore identical on every evaluation.
              order.sort(function (a, b) {
                return b.v - a.v || a.j - b.j;
              });
              var row = new Array(N);
              for (var r = 0; r < order.length; r++) row[order[r].j] = r;
              RANKS.push(row);
            }

            function smoothstep(x) {
              return x * x * (3 - 2 * x);
            }

            // Continuous row position solved from the baked ranks. A rank change
            // between two adjacent keyframes IS the swap, and it plays out over one
            // keyframe interval (PERIOD_DURATION / 10) instead of snapping.
            function rankPosAt(t, j) {
              if (KF_COUNT < 2) return RANKS[0][j];
              var m = t / KF_DURATION;
              var m0 = clamp(Math.floor(m), 0, KF_COUNT - 2);
              var e = smoothstep(clamp(m - m0, 0, 1));
              var a = RANKS[m0][j];
              var b = RANKS[m0 + 1][j];
              return a + (b - a) * e;
            }

            // ---- formatting --------------------------------------------------
            function formatValue(v) {
              return (
                PREFIX +
                v.toLocaleString("en-US", {
                  minimumFractionDigits: DECIMALS,
                  maximumFractionDigits: DECIMALS,
                }) +
                SUFFIX
              );
            }

            function hexToRgb(hex) {
              var h = String(hex).trim().replace("#", "");
              if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
              var n = Number.parseInt(h, 16);
              if (!Number.isFinite(n)) return [31, 29, 27];
              return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
            }

            // Accent is the only colour in the piece and it carries exactly one
            // meaning: this bar currently leads. It is binary, not blended — a
            // half-accent bar would assert "half leading", which is not a fact.
            // It hands over at the instant the two values cross, which is also the
            // instant the two bars are the same length, so the handover lands on
            // the overtake instead of drifting near it. Flat fill, never a
            // gradient: a gradient inside a bar makes the longest bar the palest
            // exactly where the eye is comparing lengths.
            function hexToCss(hex) {
              var rgb = hexToRgb(hex);
              return "rgb(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ")";
            }

            var BAR_FILL = hexToCss(BAR_COLOR);
            var ACCENT_FILL = hexToCss(ACCENT);

            function niceStep(x) {
              var e = Math.pow(10, Math.floor(Math.log10(x)));
              var f = x / e;
              return e * (f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10);
            }

            // ---- geometry ----------------------------------------------------
            var TRACK_X = 400;
            var TRACK_W = 1180;
            var PLOT_H = 760;
            var PITCH = PLOT_H / BAR_COUNT;
            var BAR_H = Math.max(12, PITCH * 0.62);
            var TICK_POOL = 12;

            // ---- DOM ---------------------------------------------------------
            var plot = document.getElementById("bcr-plot");
            var axis = document.getElementById("bcr-axis");
            var periodEl = document.getElementById("bcr-period");
            document.getElementById("bcr-title").textContent = str(
              V.title,
              "Streaming Subscribers by Service",
            );
            document.getElementById("bcr-subtitle").textContent = str(
              V.subtitle,
              "Ranked by reported subscribers",
            );

            var rows = SERIES.map(function (s) {
              var row = document.createElement("div");
              row.className = "bcr-row";
              row.style.height = PITCH + "px";

              // Declared, not silenced: two rows mid-overtake genuinely occupy the
              // same band for ~200ms. Readability is handled by the opaque chip on
              // each label, so the front one is always legible.
              var name = document.createElement("div");
              name.className = "bcr-name";
              name.setAttribute("data-layout-allow-overlap", "");
              name.textContent = s.label;

              var bar = document.createElement("div");
              bar.className = "bcr-bar";
              bar.style.height = BAR_H + "px";

              var value = document.createElement("div");
              value.className = "bcr-value";
              value.setAttribute("data-layout-allow-overlap", "");

              row.appendChild(name);
              row.appendChild(bar);
              row.appendChild(value);
              plot.appendChild(row);
              return { row: row, bar: bar, value: value };
            });

            var ticks = [];
            for (var k = 0; k < TICK_POOL; k++) {
              var line = document.createElement("div");
              line.className = "bcr-tick-line" + (k === 0 ? " bcr-tick-zero" : "");
              var label = document.createElement("div");
              label.className = "bcr-tick-label";
              axis.appendChild(line);
              axis.appendChild(label);
              ticks.push({ line: line, label: label });
            }

            // ---- render ------------------------------------------------------
            function render(t) {
              if (N === 0) return;

              var values = SERIES.map(function (s) {
                return valueAt(t, s);
              });
              // Paint order follows the CURRENT value, not the row position: the
              // bar that has just edged ahead comes forward, so at the instant two
              // bars are superimposed the frame shows the one that is overtaking
              // (its bar and its label together) rather than the one it passed.
              var byValue = values
                .map(function (v, i) {
                  return i;
                })
                .sort(function (a, b) {
                  return values[b] - values[a] || a - b;
                });
              var zRank = new Array(values.length);
              for (var q = 0; q < byValue.length; q++) zRank[byValue[q]] = q;
              var leader = byValue[0];
              var maxValue = values[leader];
              // Headroom keeps the leader off the frame edge; the domain is a
              // continuous function of t, so the axis glides instead of stepping.
              var scaleMax = Math.max(maxValue * 1.06, 1e-6);
              var step = niceStep(scaleMax / 4);

              for (var i = 0; i < TICK_POOL; i++) {
                var tv = i * step;
                var visible = tv <= scaleMax;
                var tx = TRACK_X + (tv / scaleMax) * TRACK_W;
                ticks[i].line.style.transform = "translateX(" + tx.toFixed(2) + "px)";
                ticks[i].line.style.opacity = visible ? "1" : "0";
                ticks[i].label.style.transform =
                  "translateX(" + tx.toFixed(2) + "px) translateX(-50%)";
                ticks[i].label.style.opacity = visible ? "1" : "0";
                ticks[i].label.textContent = visible ? formatValue(tv) : "";
              }

              for (var j = 0; j < rows.length; j++) {
                var rankPos = rankPosAt(t, j);
                var barW = (values[j] / scaleMax) * TRACK_W;
                var el = rows[j];

                el.row.style.transform = "translateY(" + (rankPos * PITCH).toFixed(2) + "px)";
                // Fades out only as it slides past the last visible slot.
                el.row.style.opacity = clamp(BAR_COUNT - rankPos, 0, 1).toFixed(3);
                el.row.style.zIndex = String(1000 - zRank[j]);

                el.bar.style.width = barW.toFixed(2) + "px";
                el.bar.style.backgroundColor = j === leader ? ACCENT_FILL : BAR_FILL;

                el.value.style.transform = "translate(" + barW.toFixed(2) + "px, -50%)";
                el.value.textContent = formatValue(values[j]);
              }

              var pIndex = clamp(Math.floor(t / PERIOD_DURATION), 0, T - 1);
              periodEl.textContent = PERIODS[pIndex];
            }

            // ---- timeline ----------------------------------------------------
            // A property SETTER drives the per-frame work: tl.eventCallback("onUpdate")
            // does not fire on tl.seek(), a tweened accessor does.
            var driver = { _t: 0 };
            Object.defineProperty(driver, "t", {
              get: function () {
                return this._t;
              },
              set: function (value) {
                this._t = value;
                render(value);
              },
            });

            var tl = gsap.timeline({ paused: true });
            if (RACE_SECONDS > 0) {
              tl.to(driver, { t: RACE_SECONDS, duration: RACE_SECONDS, ease: "none" }, 0);
            }
            render(0);

            window.__timelines = window.__timelines || {};
            window.__timelines["bar-chart-race"] = tl;
          })();
        </script>
      </div>
    </body>
  </html>
  ```
</Accordion>

Tagged `data` `chart` `statistics` `ranking`.

## Related topics

* [Browse the complete Catalog](/catalog)
* [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
* [Build a richer composition](/go-further)


## Related topics

- [Verified example prompts](/prompting/examples.md)
- [Data and maps](/prompting/data-and-maps.md)
- [Prompt Guide](/prompting/overview.md)
