1. readsCoinGecko market caps and the day's moves
  2. becomescap → mass (area + ink); the move → polarity
  3. writes--w--cat
  4. you seebig caps take more page; direction is hue, magnitude is intensity

No canvas is drawn here — the field is invisible; these variables are its only output. Without it you would build: a treemap library plus a color scale you maintain.

Data honesty

Data source
Real CoinGecko market data (coingecko.com API) — market caps and 24h moves.
Snapshot
Committed snapshot in src/data/examples/, stamped snapshotAt on refresh via scripts/snapshot-examples.mjs — the build never fetches, so it is deterministic.
Live refresh
Upgrades to live in the browser: a polite poll (wireLiveChip) that skips hidden tabs and retires after failures. The committed snapshot is the SSR baseline and the no-JS truth.
Not claimed
The field reads market cap as mass and the day's move as polarity. It is not financial advice and makes no claim about value or direction — it only renders the numbers as size and hue.

Fundamental · invisible fields · market

The tape, felt.

A market table treats a $1.2T asset and a $4B asset as the same row height, the same ink. The only signal left is the sort order. This is the same snapshot from CoinGecko run as a field: market cap becomes gravitational mass, and mass is area — heavier assets take more of the page, with bigger tiles, darker ink, heavier type.

The day's move polarizes each body: direction is color (teal-green up, red-pink down), magnitude is intensity — a −0.1% drift barely registers, a −17% slide burns. Switch the window to 7d and the same tiles repolarize; reweight by volume and the mosaic re-tiers and re-settles around where the trading actually happened. Toggle the field off and it collapses back to an even grid — every asset the same size, the same ink, the live glow gone dark.

Field
Weight by
Move window
Data snapshot · November 3, 2026

area = market cap — the heavier the asset, the more page it takes · color = the 24-hour move — direction is hue, magnitude is intensity

  1. 01
    Bitcoin btc
    $77,265.00 $1.6T cap
    ▼ 0.97% ▼ 3.56%
  2. 02
    Ethereum eth
    $2,275.53 $278B cap
    ▼ 0.79% ▼ 2.12%
  3. 03
    Tether usdt
    $0.998623 $178.9B cap
    ▼ 0.04% ▼ 0.00%
  4. 04
    BNB bnb
    $700.22 $92.1B cap
    ▼ 0.82% ▼ 5.72%
  5. 05
    XRP xrp
    $1.28 $81.2B cap
    ▲ 2.13% ▼ 3.44%
  6. 06
    USDC usdc
    $0.999297 $70.8B cap
    ▼ 0.03% ▼ 0.01%
  7. 07
    Solana sol
    $93.07 $54.9B cap
    ▼ 1.49% ▼ 7.58%
  8. 08
    TRON trx
    $0.322235 $30.6B cap
    ▲ 1.88% ▼ 1.64%
  9. 09
    Figure Heloc figr_heloc
    $1.03 $26.9B cap
    ▲ 0.00% ▼ 0.35%
  10. 10
    Zcash zec
    $1,193.67 $20.3B cap
    ▼ 5.06% ▼ 5.69%
  11. 11
    Hyperliquid hype
    $84.58 $20.2B cap
    ▼ 1.62% ▲ 8.78%
  12. 12
    Dogecoin doge
    $0.075004 $11.7B cap
    ▼ 0.89% ▼ 3.65%
  13. 13
    Monero xmr
    $623.27 $11.7B cap
    ▲ 2.00% ▲ 5.38%
  14. 14
    USDS usds
    $0.999728 $9.6B cap
    ▼ 0.03% ▲ 0.03%
  15. 15
    WhiteBIT Coin wbt
    $74.32 $8.8B cap
    ▼ 0.97% ▼ 3.47%
  16. 17
    LEO Token leo
    $9.27 $8.5B cap
    ▼ 1.29% ▲ 2.73%
  17. 18
    Cardano ada
    $0.190931 $7.2B cap
    ▼ 0.43% ▼ 7.07%
  18. 19
    Stellar xlm
    $0.165213 $5.8B cap
    ▼ 2.00% ▼ 2.97%
  19. 20
    Bitcoin Cash bch
    $277.43 $5.6B cap
    ▼ 0.37% ▼ 1.55%
  20. 21
    NEAR Protocol near
    $3.88 $5.1B cap
    ▼ 6.47% ▼ 13.77%
  21. 22
    Dai dai
    $1.00 $4.5B cap
    ▼ 0.01% ▼ 0.01%
  22. 23
    Ethena USDe usde
    $0.998613 $4.5B cap
    ▲ 0.03% ▼ 0.01%
  23. 24
    Rain rain
    $0.006217 $4.4B cap
    ▼ 1.46% ▼ 12.23%

How it's built

Every asset is an ordinary <li> with a few data attributes. Fundamental reads them, runs an invisible field over the mosaic, and the tiles carry the measurement — cap as area and ink, the move as color. No canvas, no chart library beyond one inline <path> per tile. Four channels do the work: --w is the server-computed weight, --cat the lens color, --d the engine's live local density (hold a tile and the field gathers), and --field-attention the pattern's eased attention metric. Each sparkline also declares its provenance with data-field-visual-for, so the binding between chart and record is inspectable.

1 — each asset is a field body

HTML
<li id="mk-bitcoin"
  data-body="attract"
  data-strength="1.87"
  data-feedback
  data-hot
  data-cap="1235425696510"
  data-price="61517"
  data-c24="-1.70" data-c7="-7.76"
  style="--w: 0.92; --cat: hsl(350 70% 64%);"
>
  <!-- name · price · badge -->
  <svg aria-hidden="true"
    data-field-visual-for="#mk-bitcoin"
    data-field-visual-role="measurement">
    <path d="…" pathLength="100" />
  </svg>
</li>
  • data-strength — gravitational mass from log-normalized cap
  • data-c24 / data-c7 — the raw moves the lens reads
  • --cat — the polarity color the CSS mixes in
  • data-hot — the engine's live density (--d) gathers toward 1 while held
  • data-field-visual-for — binds the sparkline to its record as a measurement

2 — mass is area; polarity is CSS

CSS
/* tier classes set the tile's footprint */
.mk-t1 { grid-column: span 3; grid-row: span 2; }
.mk-t2 { grid-column: span 2; grid-row: span 2; }

.mk-tile {
  /* --w = log-normalized cap (0..1).
     --d = the engine's LIVE local density —
     data-hot gathers it toward 1 on hold. */
  --live: var(--d, 0);
  opacity: calc(0.58 + var(--w) * 0.42);
  box-shadow: 0 0 calc(var(--live) * 14px) -6px
    color-mix(in srgb, var(--cat)
      calc(var(--live) * 50%), transparent);
}
/* the sparkline breathes with the tile */
.mk-spark path {
  stroke-width: calc(1.5 + var(--live) * 0.9);
  filter: drop-shadow(0 0
    calc(var(--live) * 6px) var(--cat));
}
/* entry draw-in — a keyframe, not a dash
   steady state: paths carry
   pathLength="100", the BASE rule sets no
   stroke-dasharray, and the dash lives
   only inside the animation — a drawn
   line computes stroke-dasharray: none */
@keyframes mk-spark-draw {
  from { stroke-dasharray: 100;
         stroke-dashoffset: 100;
         opacity: 1; }
  to   { stroke-dasharray: 100;
         stroke-dashoffset: 0; }
}

The runtime sets --cat from the chosen window — hue is direction, intensity is magnitude — and one color-mix() tints the tile against the hairline. --d is live: hold a tile and the halo and sparkline thicken as the field gathers, then ease back.

3 — re-tier on lens change; live updates in place

// recompute --w from cap or volume,
// swap the tier class, re-sort, then
// FLIP tiles whose size did not change:
const first = new Map(tiles.map(
  (t) => [t, t.getBoundingClientRect()]));
ordered.forEach((t) => list.appendChild(t));
ordered.forEach((t) => {
  const a = first.get(t);
  const b = t.getBoundingClientRect();
  t.style.transform = `translate(
    ${a.left - b.left}px,
    ${a.top - b.top}px)`;
  requestAnimationFrame(() =>
    (t.style.transform = ""));
});
// re-tiered tiles settle with a fade
// instead — translate cannot honestly
// animate a size change.

When the CoinGecko API is reachable, the runtime re-polls it once a minute and updates the snapshot's 24 tiles in place — prices, badges, colors, weights, sparklines. A poll never re-sorts or re-tiers the mosaic (that would be hostile under the cursor); tiers re-settle the next time you change the lens. Ids the snapshot does not know are ignored.