Charts

Data Mixture

A four-state training-mixture card: one composition bar per training stage split by data domain, with per-domain epochs so upsampling reads as the gap between unique tokens and tokens seen.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import type { ChartDataMixtureData, ChartDataMixtureStage } from "./chart-data-mixture.contract"

/** Which measure the bars partition. */
export type ChartDataMixtureView = "seen" | "unique"

export interface ChartDataMixtureProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">,
    ChartDataMixtureData {
  /** Decimals on every printed share. Clamped 0–2, default 1. */

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/chart-data-mixture.json

Prompt

Build a React + TypeScript + Tailwind "ChartDataMixture" card with zod. No chart
library: each stage is one horizontal composition bar laid out with flex
percentages, so text lands in HTML rather than in SVG.

Contract
- One zod schema is the single source of truth:
  { status: "loading" | "empty" | "error" | "ready"; title: string;
    defaultView?: "unique" | "seen";
    stages: { id, label, totalTokens >= 0,
              domains: { id, label, tokens >= 0, epochs > 0 }[] }[] }.
- Props = z.infer of that schema plus precision? (decimals on every share,
  clamped 0-2, default 1), onRetry?: () => void, className and the rest of the
  div props spread on the root. No hand-written parallel interface.
- tokens and epochs stay separate numbers on purpose: tokens seen =
  tokens * epochs, and the whole card is about the gap between the two.
- totalTokens is a check figure, not a denominator. Shares are apportioned over
  the domains actually listed; a stage whose declared total is more than 0.5%
  away from its domains is called out in the footnote.

Behavior
- Four first-class branches inside one bg-card panel:
  - loading: a fixed silhouette of four label rows plus segmented tracks, the
    same shape the real bars take, aria-hidden, with an sr-only role=status.
  - empty: two hollow bars plus a line explaining what a mixture payload needs.
    A "ready" payload with no stages, or with no positive tokens anywhere,
    renders this branch rather than a blank card.
  - error: message plus a "Try again" button rendered only when onRetry exists.
  - ready: header, one bar per stage, the domain key, a readout line, a footnote
    and an sr-only table.
- A radiogroup in the header switches the measure between "unique" (partition
  the corpus) and "seen" (partition tokens * epochs). Roving tabindex, arrow
  keys, Home and End; only handled keys call preventDefault. Every bar, every
  label and every stage total re-apportions with it. defaultView seeds it once,
  then the control owns the choice.
- Pointing at a segment, or focusing a key row, lights that domain in every
  stage; activating the key row pins it (aria-pressed) and Escape clears both
  the pin and the focus highlight, so the footnote's keyboard instruction is
  true. Unselected segments mix their own token toward the card rather than
  dropping opacity, so the highlight outline and the label halo stay at full
  strength. The readout line shows the pinned domain stage by stage, otherwise
  names the domain whose share grows most under replay, and when no share moves
  reads off the replay factor instead — a run where every domain shares one
  epoch count is not a single-pass run.
- Derived in-component, never passed in: tokens seen per domain and per stage,
  the replay factor (seen / unique), how many domains a stage passes over more
  than once, the run-wide token-weighted mean epochs per domain, and the shift
  in points between a domain's two shares.
- Degenerate input is absorbed, not thrown: a non-finite or negative token count
  reads as 0, a non-positive epoch count reads as 1, both are counted and
  reported in the footnote, and a domain repeated inside one stage merges rather
  than drawing two identically coloured segments.

Rendering & styling
- Shares are apportioned by largest remainder on a 10^precision grid, so each
  bar totals exactly 100 in the labels and in the geometry — every segment is
  laid out with the same rounded number it prints.
- Colour comes only from the chart tokens: domain i uses
  var(--chart-{(i % 5) + 1}) for its segment, its key swatch and its highlight,
  where i is the domain's position in the run-wide order, so a domain keeps its
  colour across stages. Past the fifth domain a stripe and then a crosshatch
  built from color-mix with var(--card) takes over instead of repeating a hue.
- In-bar labels appear only when the segment has the room: name plus share above
  64px, share alone above 40px, nothing below that. Width comes from a
  ResizeObserver on the stage column, disconnected on unmount and before every
  rebuild. Text on a fill carries a text-shadow ring in var(--card), the HTML
  equivalent of paint-order:stroke, which is what makes one text colour legal on
  all five fills in both themes.
- Panel is rounded-xl border bg-card; every number is tabular-nums; segment
  width transitions are wrapped in motion-reduce:transition-none, and the
  skeleton pulse in motion-reduce:animate-none.
- Each bar is role="img" with a label naming every domain and share; the exact
  numbers live in an sr-only table (stage, domain, unique tokens, epochs, tokens
  seen, both shares) whose sr-only class sits on the wrapper, never the table.

Customization levers
- Density: BAR height (36px) and the two label thresholds decide how much lands
  inside the bars; raise them for a compact card, drop the in-bar labels
  entirely and let the key carry every number.
- Sub-blocks: the domain key, the readout line and the footnote are independent
  — a model card usually keeps the key, a slide usually keeps only the bars.
- Measure: the two views are a literal union; add a third (for example "tokens
  seen per parameter") by extending VIEWS and the shareOf/amountOf pair.
- Precision: precision 0 gives whole percents on a crowded bar, 2 keeps a 0.05%
  domain distinguishable; the largest-remainder grid follows it.
- Palette: replace domainInk to pin a fixed token per domain id (brand colours
  for in-house corpora) instead of cycling by position; the texture tiers are
  what keep a mixture past five domains apart.
- Units: formatTokens scales to T / B / M / k — swap it for documents, hours or
  bytes and the whole card follows, since nothing else formats a magnitude.

Concepts

  • Unique against seen — the same mixture partitioned twice: once over the corpus as it exists, once over tokens × epochs as the optimiser reads it. Upsampling is invisible in the first view and is the entire story in the second, so the card refuses to pick one and makes the switch a first-class control.
  • Replay factor — tokens seen divided by unique tokens, per stage and for the run. It is the one number that says how much of a training budget was spent re-reading data rather than reading new data.
  • Largest-remainder apportionment — every share gets its floor first and the leftover units go to the biggest fractional parts, so a bar totals exactly 100 in both views. Rounding each share on its own is what makes a full bar print 99.9% and leaves a gap at the end of the geometry.
  • Position from the printed value — a segment is laid out with the same rounded share it prints and the key repeats, so nothing on the card is measured against a number the reader cannot see.
  • Cross-stage domain highlight — a domain keeps its colour, its palette slot and its texture tier across every stage, so pinning it draws one continuous reading of how its share moves from pretrain to preference.
  • Check figure, not denominator — the declared stage total is reconciled against the domains listed rather than trusted, because a mixture table that does not add up is the most common defect in a real training report.

On This Page