- readsCoinGecko market caps and the day's moves
- becomescap → mass (area + ink); the move → polarity
- writes
--w--cat - 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.
area = market cap — the heavier the asset, the more page it takes · color = the 24-hour move — direction is hue, magnitude is intensity
- Aa tile area = the weighting signal (cap or volume)
- color = the move — direction is hue, magnitude is intensity
- sparkline = the week's price path (tile-local scale, big tiles only)
- glow = the engine's live density (
--d) — hold a tile and the field gathers
- 01 Bitcoin btc$77,265.00 $1.6T cap▼ 0.97% ▼ 3.56%
- 02 Ethereum eth$2,275.53 $278B cap▼ 0.79% ▼ 2.12%
- 03 Tether usdt$0.998623 $178.9B cap▼ 0.04% ▼ 0.00%
- 04 BNB bnb$700.22 $92.1B cap▼ 0.82% ▼ 5.72%
- 05 XRP xrp$1.28 $81.2B cap▲ 2.13% ▼ 3.44%
- 06 USDC usdc$0.999297 $70.8B cap▼ 0.03% ▼ 0.01%
- 07 Solana sol$93.07 $54.9B cap▼ 1.49% ▼ 7.58%
- 08 TRON trx$0.322235 $30.6B cap▲ 1.88% ▼ 1.64%
- 09 Figure Heloc figr_heloc$1.03 $26.9B cap▲ 0.00% ▼ 0.35%
- 10 Zcash zec$1,193.67 $20.3B cap▼ 5.06% ▼ 5.69%
- 11 Hyperliquid hype$84.58 $20.2B cap▼ 1.62% ▲ 8.78%
- 12 Dogecoin doge$0.075004 $11.7B cap▼ 0.89% ▼ 3.65%
- 13 Monero xmr$623.27 $11.7B cap▲ 2.00% ▲ 5.38%
- 14 USDS usds$0.999728 $9.6B cap▼ 0.03% ▲ 0.03%
- 15 WhiteBIT Coin wbt$74.32 $8.8B cap▼ 0.97% ▼ 3.47%
- 16 Chainlink link$11.21 $8.6B cap▼ 1.32% ▼ 1.85%
- 17 LEO Token leo$9.27 $8.5B cap▼ 1.29% ▲ 2.73%
- 18 Cardano ada$0.190931 $7.2B cap▼ 0.43% ▼ 7.07%
- 19 Stellar xlm$0.165213 $5.8B cap▼ 2.00% ▼ 2.97%
- 20 Bitcoin Cash bch$277.43 $5.6B cap▼ 0.37% ▼ 1.55%
- 21 NEAR Protocol near$3.88 $5.1B cap▼ 6.47% ▼ 13.77%
- 22 Dai dai$1.00 $4.5B cap▼ 0.01% ▼ 0.01%
- 23 Ethena USDe usde$0.998613 $4.5B cap▲ 0.03% ▼ 0.01%
- 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
<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 capdata-c24/data-c7— the raw moves the lens reads--cat— the polarity color the CSS mixes indata-hot— the engine's live density (--d) gathers toward 1 while helddata-field-visual-for— binds the sparkline to its record as a measurement
2 — mass is area; polarity is 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. // once a minute (skipped while the tab
// is hidden) — the same CoinGecko
// endpoint the snapshot script uses:
const data = await (
await fetch(GECKO_URL)).json();
for (const c of data) {
const tile = byId.get(c.id);
if (!tile) continue; // unknown ids:
// not bodies on this page
tile.dataset.cap = String(c.market_cap);
price.textContent =
fmtPrice(c.current_price);
flash(price); // ▲ teal / ▼ red tick
spark.setAttribute("d",
sparkPath(c.sparkline_in_7d.price));
}
applyLens(); // --cat through the
// ACTIVE lens
recomputeWeights(); // --w — the scoped
// field re-measures
// a poll NEVER re-sorts or re-tiers —
// hostile under the cursor. Tiers
// re-settle on the next lens 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.