Exclusivobeta

To Scale <mv-to-scale>

Torna compreensíveis quantidades abstratas, reformulando-as em uma escala humana que o leitor já conhece, ao lado do valor preciso intacto: 2,4 TB ≈ 800 horas de vídeo HD, 412 t CO₂e ≈ o CO₂ absorvido por 7.000 mudas de árvore, US$ 9.000 ≈ 6 meses de aluguel em Lisboa, 21.600 pedidos/dia ≈ um pedido a cada 4 segundos, 38% dos clientes ≈ 2 em cada 5 clientes. Envolva um número (glosa no texto ou card) ou percorra um texto inteiro em busca de spans data-scale. Uma biblioteca de comparações extensível cobre dados, energia, CO₂e, massa, tempo, distância, pessoas, dinheiro, taxas e proporções, com referências integradas cuja premissa e fonte são sempre mostradas; o app acrescenta as suas por público, locale ou persona (o aluguel na cidade do leitor, as unidades do seu produto). Cada candidata recebe uma nota de facilidade de compreensão, então a escolha cai em um número redondo entre um e algumas dezenas em vez de “0,003 de…” ou “612.345 de…”, e a referência muda com a ordem de grandeza; duas ou três alternativas ficam a um toque ou a uma passada de mouse, o cálculo da conversão e o arredondamento ficam em um disclosure, o sistema métrico ou imperial segue o locale, e os números da mesma família em um texto compartilham uma referência para poderem ser comparados entre si. Pontos proporcionais ou barra segmentada opcionais com uma legenda em texto, um evento mv-scale-pick cancelável, e uma função estática pura MvToScale.express(value, unit, options) que devolve a mesma frase, o mesmo cálculo e as mesmas alternativas no servidor.

CategoriaExibição de dados
TipoWeb Component (<mv-to-scale>)
Statusbeta
KitNúmeros que se explicam sozinhos
Keywordsexclusive, culture, numbers, units, comparison, human-scale, data-literacy, numeracy, explainer, co2, carbon, energy, storage, bytes, money, cost-of-living, rate, percentage, natural-frequency, metric, imperial, ssr, reports, journalism

When to use

  • A dashboard or plan page shows storage, bandwidth or quota figures that users cannot picture (GB, TB, requests)
  • A sustainability, energy or impact report must make tonnes of CO₂e or gigawatt-hours tangible without losing the exact figure
  • A price, budget or savings goal should be restated in terms of the reader's own cost of living, supplied per city or persona
  • An article or AI answer is full of large numbers, rates and percentages that should each get an honest everyday equivalent

Avoid when

  • The number is a headline KPI that should count up or animate on arrival rather than be restated → use Number Ticker instead
  • Readers need to audit how a figure was computed from its inputs, not what it is comparable to → use Curtain instead
  • Several options are weighed against each other point by point (pros and cons, build vs buy) → use Balance Sheet instead

Instalação

node scripts/add.mjs to-scale --out ./src/marvelous

Agente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["to-scale"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/dismiss.js, core/dom.js, core/element.js, core/position.js, components/to-scale/to-scale.js, components/to-scale/to-scale.css.

Uso

Início rápido, a menor marcação que funciona:

<p>Your backup holds <mv-to-scale value="2.4 TB">2.4 TB</mv-to-scale>.</p>

Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:

<div id="ts-demo" style="width:min(100%,64rem);margin-inline:auto">
  <style>
    #ts-demo { display:grid; gap:1.25rem; align-content:start }
    #ts-demo .ts-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1.25rem; align-items:start }
    #ts-demo .ts-col { display:grid; gap:.75rem; min-width:0 }
    #ts-demo .ts-bar { display:flex; align-items:center; justify-content:space-between; gap:.5rem .75rem; flex-wrap:wrap }
    #ts-demo .ts-bar h3 { margin:0; font-size:.9375rem; font-weight:650; letter-spacing:-.01em }
    #ts-demo .ts-bar p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #ts-demo .ts-note { margin:0; padding:0 .25rem; color:var(--mv-fg-muted); font-size:.875rem; line-height:1.6 }
    #ts-demo .ts-report { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs); overflow:hidden }
    #ts-demo .ts-report-head { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:1rem 1.25rem; border-bottom:1px solid var(--mv-border) }
    #ts-demo .ts-report-head h3 { display:flex; align-items:center; gap:.5rem; margin:0; font-size:1rem; font-weight:650; letter-spacing:-.01em }
    #ts-demo .ts-report-head p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #ts-demo .ts-report mv-to-scale { padding:1.125rem 1.25rem .25rem; max-width:46rem }
    #ts-demo #ts-report > p { margin:0 0 1rem; font-size:.9375rem; line-height:1.85; text-wrap:pretty }
    #ts-demo .ts-report [data-scale] { font-weight:600; font-variant-numeric:tabular-nums; white-space:nowrap }
    #ts-demo .ts-foot { display:flex; align-items:center; justify-content:space-between; gap:.5rem 1rem; flex-wrap:wrap; padding:.75rem 1.25rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
    #ts-demo .ts-log { margin:0; min-height:1.25rem; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono) }
    #ts-demo .mv-choice { font-size:.8125rem }
    @media (max-width:46rem) { #ts-demo .ts-grid { grid-template-columns:minmax(0,1fr) } }
  </style>

  <div class="ts-grid">
    <!-- Storage: the yardstick changes with the magnitude -->
    <section class="ts-col" aria-labelledby="ts-storage-t">
      <div class="ts-bar">
        <div><h3 id="ts-storage-t">Cloud backup</h3><p>Same number, human scale.</p></div>
        <mv-segmented id="ts-plan" aria-label="Backup size" value="2.4 TB">
          <button value="250 GB">250 GB</button>
          <button value="2.4 TB">2.4 TB</button>
          <button value="18 TB">18 TB</button>
        </mv-segmented>
      </div>
      <mv-to-scale id="ts-storage" variant="card" label="Photo library backup" value="2.4 TB" visual="dots"></mv-to-scale>
      <p class="ts-note">This month you added <mv-to-scale value="86" unit="GB">86 GB</mv-to-scale> across 3 devices.</p>
    </section>

    <!-- Money: the app supplies the reader's own reference set -->
    <section class="ts-col" aria-labelledby="ts-fund-t">
      <div class="ts-bar">
        <div><h3 id="ts-fund-t">Savings goal</h3><p>Priced in the reader’s city.</p></div>
        <mv-segmented id="ts-city" aria-label="Reader’s city" value="lisbon">
          <button value="lisbon">Lisbon</button>
          <button value="austin">Austin</button>
          <button value="bengaluru">Bengaluru</button>
        </mv-segmented>
      </div>
      <mv-to-scale id="ts-fund" variant="card" label="Emergency fund target" value="9000" unit="USD" visual="bar" audience="lisbon" defaults="false"></mv-to-scale>
    </section>
  </div>

  <!-- Report: every [data-scale] figure in the text gets a gloss; one yardstick per unit family -->
  <section class="ts-report" aria-labelledby="ts-report-t">
    <header class="ts-report-head">
      <div>
        <h3 id="ts-report-t">Q3 2026 impact report <span class="mv-badge" data-variant="secondary" data-size="sm">Audited</span></h3>
        <p>Northwind Cloud · published Oct 5, 2026</p>
      </div>
      <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="ts-metric"> Metric units</label>
    </header>
    <mv-to-scale id="ts-report" scan visual="dots">
      <p>Our three data centers used <span data-scale>1.8 GWh</span> of electricity this quarter and emitted <span data-scale>412 t CO2e</span>, down from <span data-scale>530 t CO2e</span> in Q3 2025.</p>
      <p>At peak, the storefront handled <span data-scale>21,600 orders/day</span>, and <span data-scale>38%</span> of customers picked carbon-neutral shipping. Delivery vans covered <span data-scale>3.2 million km</span>, and employees volunteered <span data-scale>2,400 hours</span> with local schools.</p>
    </mv-to-scale>
    <div class="ts-foot">
      <p class="ts-log" id="ts-log" aria-live="polite">Hover or tap a gloss to see alternatives and the math.</p>
    </div>
  </section>

  <script type="module">
    const storage = document.getElementById("ts-storage");
    const fund = document.getElementById("ts-fund");
    const report = document.getElementById("ts-report");
    const log = document.getElementById("ts-log");

    document.getElementById("ts-plan").addEventListener("mv-change", (e) => { storage.value = e.detail.value; });

    // The reader's reference set, supplied by the app (demo figures, in USD).
    const src = "Demo reference set, 2026. Replace with your own data.";
    const city = (id, name, rent, groceries, transit) => [
      { id: `${id}-rent`, audience: id, unit: "USD", per: rent, one: `month of rent in ${name}`, other: `months of rent in ${name}`, assumption: `a one-bedroom apartment in central ${name} at about $${rent.toLocaleString("en-US")} a month`, source: src, weight: 1.4 },
      { id: `${id}-groceries`, audience: id, unit: "USD", per: groceries, one: `month of groceries in ${name}`, other: `months of groceries in ${name}`, assumption: `groceries for one person in ${name} at about $${groceries} a month`, source: src },
      { id: `${id}-transit`, audience: id, unit: "USD", per: transit, one: `monthly transit pass in ${name}`, other: `monthly transit passes in ${name}`, assumption: `an unlimited monthly public transit pass in ${name} at about $${transit}`, source: src, weight: 0.8 },
    ];
    fund.comparisons = [
      ...city("lisbon", "Lisbon", 1500, 380, 45),
      ...city("austin", "Austin", 1750, 450, 41),
      ...city("bengaluru", "Bengaluru", 420, 170, 25),
    ];
    document.getElementById("ts-city").addEventListener("mv-change", (e) => { fund.audience = e.detail.value; });

    document.getElementById("ts-metric").addEventListener("change", (e) => {
      report.system = e.target.checked ? "metric" : "imperial";
    });

    document.getElementById("ts-demo").addEventListener("mv-scale-pick", (e) => {
      log.textContent = `mv-scale-pick · ${e.detail.original} → ${e.detail.text}`;
    });
  </script>
</div>

Referência cultural

As Viagens de Gulliver, Jonathan Swift (1726, livro). O mesmo homem é um gigante em Lilliput e um brinquedo em Brobdingnag: o tamanho dele só significa algo em relação às pessoas que o medem. Na interface, uma quantidade bruta é medida de novo com referências que o leitor já conhece, e o componente escolhe a referência em que o número vira um valor redondo e fácil de entender, com outras referências a um toque de distância.

API

Attributes

NameTipoDefaultDescription
valuenumber | stringThe quantity. With unit, a machine number (“.” decimal, optional “,” grouping). Without unit, a full quantity string: "2.4 TB", "$9,000", "$4.2M", "1.2 million users", "21,600 orders/day", "38%", "412 t CO2e". Ignored with scan.
unitstringUnit of value: data (B, KB, MB, GB, TB, PB, KiB…GiB), energy (Wh…TWh, J, kJ, MJ, GJ, kcal, BTU, therm), CO₂e (g/kg/t/Mt CO2e, lb CO2e), mass (mg, g, kg, t, lb, oz), time (ms, s, min, h, days, weeks, months, years), distance (mm, cm, m, km, in, ft, yd, mi), people (people, users, customers, visitors, members…), share (%), money (ISO code or $, €, £, ¥, ₹), rates (“orders/day”, “per hour”, “sign-ups per week”).
variantinline | cardinlineinline: the precise value followed by a “≈ 800 hours of HD video” gloss button that opens a popover (alternatives, picture, math). card: a block with label, large precise value, the comparison sentence, alternatives as chips and the math disclosure.
scanbooleanContainer mode: every descendant [data-scale] element (its attribute value, else its text, is the quantity) gets a gloss right after it and shares one popover. The app's text is never rewritten. Added, removed or edited figures are picked up automatically.
visualnone | dots | barnoneTiny proportional picture of the pick, always with a text key: dots (whole things, or round groups of them when many: “Each dot is 100 hours of HD video”), bar (the same as a segmented bar). Shares draw an icon array (2 of 5 filled); rates shown as intervals of 0.3-30 s draw a dot that pulses at that tempo (static under reduced motion).
localeBCP 47 tagclosest [lang], else en-USNumber formatting (grouping, “1.5 million”) and the default measurement system.
systemauto | metric | imperialautoWhich built-in yardsticks apply (miles driven vs kilometers driven, gallons vs liters, US football fields vs soccer pitches). auto: imperial for US, Liberia and Myanmar locales, metric elsewhere.
audiencestring (space or comma separated)Activates the app comparisons tagged with a matching audience (a city, persona, country, customer segment). Audience comparisons get a strong bonus so the reader's own reference points win when they fit.
prefercomparison idsBias the pick toward these ids (in order) whenever they give a sensible figure. data-scale-prefer does the same on one scanned figure.
excludecomparison idsNever use these ids.
alternativesnumber3How many other yardsticks are offered next to the pick (only those scoring close enough to the best).
consistent"true" | "false"trueIn scan mode, figures of the same family (and currency) use one shared yardstick when it fits all of them, so “412 t” and “530 t” read as 7,000 and 9,000 tree seedlings; picking an alternative on one applies it to the others where it fits. "false": each figure gets its own best pick.
defaults"true" | "false"true"false" drops the built-in library: only your comparisons (property, MvToScale.register) are used, e.g. for money in a non-US audience.
subjectstring ("order|orders" or a plural)What is being counted, for rates, shares and people: “one order every 4 seconds”, “2 in 5 customers”. Inferred from the unit (“orders/day”, “users”) when omitted. data-scale-subject on a scanned figure.
labelstringCard variant: caption above the precise value (“Photo library backup”).
data-scale / data-scale-unit / data-scale-subject / data-scale-preferon scanned elementsMarks a figure in scan mode. data-scale holds the quantity (or leave it empty to read the element's text); data-scale-unit gives the unit separately; the others override subject and prefer for that figure. The component sets data-scale-state="ready | none | invalid" on it.
data-state / data-variant / data-familyset by the componentOn the host: ready | none (no comparison fits) | invalid (unknown unit); inline | card | scan; the unit family of the first figure.

Properties

NameTipoDescription
comparisonsComparison[]App comparisons merged with the built-ins (same id replaces a built-in). Comparison: { id, unit, per (how many units one thing is), one, other (noun labels), phrase? (sentence with {about} and {np}, e.g. "the length of {about}{np}"), assumption? (noun phrase shown as “Assumes …”), source?, href? (http(s) only), weight? (familiarity, default 1), audience?, system?, locales?, min? / max? (sensible count range), fractions? (false forbids “half a …”), exact? (a definition, not an estimate), article? }.
resultResult | nullRead-only: the first figure's result, the same object express() returns: { ok, value, unit, family, currency, original, locale, system, pick, alternatives, options }. pick and each alternative: { id, origin, n (exact count), rounded, count, approx, deviation, np, text, gloss, per, math[], assumption, source, href, visual, score }.
resultsArray<{ element } & Result>Read-only: one result per figure (scan mode), with the scanned element.
stringsPartial<Record<string, string>>Overrides for visible texts, announcements and engine words (keys: exact, pictureAs, math, assumes, source, none, invalid, eachDot, eachSegment, shareKey, tempo, glossHint, lensLabel, announce, announceGroup, announceGroupOne, aboutSr, about, one, rounded, even, arithmetic, every, everyNoun, perUnit, perUnitNoun, inN, inNSubject, events). English defaults.
value / unit / variant / scan / visual / locale / system / audience / prefer / exclude / alternatives / consistent / defaults / subject / labelreflectedMirror the attributes; setting value, unit, audience or system recomputes at once.

Methods

NameDescription
MvToScale.express(value, unit, options)Static and pure (no DOM, SSR-safe; also a named export express). express(2.4, "TB") or express("2.4 TB", null, { locale, system, audience, comparisons, defaults, prefer, exclude, pick, alternatives, subject, strings }). Returns the Result object with the sentence, math, assumption, source and alternatives, ready to render in an email, a PDF or server HTML.
MvToScale.parse(text)Static: "$4.2M" → { value: 4200000, unit: "USD" }; "1.2 million users" → { value: 1200000, unit: "users" }; null when no number is found (also a named export parseQuantity).
MvToScale.register(comparisons)Static: adds comparisons for every instance on the page (same id replaces), then refreshes the live ones.
MvToScale.libraryStatic getter: copies of the built-in comparisons with their family, assumption and source, to document or audit them.
pick(id, target?)Shows comparison id for a figure (scanned element, selector, index; default the first). Emits the cancelable mv-scale-pick with trigger "api". Returns false if vetoed or if the comparison does not fit this value.
open(target?) / close()Opens or closes the popover of an inline or scanned figure.
refresh()Rescans data-scale figures and recomputes (after changing comparisons in place).

Events

NameDescription
mv-scale-pickCancelable, before another yardstick is shown. detail: { element, id, previous, text, n, value, unit, original, family, choice, trigger: "user" | "api" }. preventDefault() keeps the current one; use it to persist a reader's preference.
mv-scale-toggleThe popover opened or closed. detail: { open, element, trigger ("hover" | "click" | "api") when opening }.

Content structure

NameDescription
(content)Standalone: optional text shown as the precise value (“2.4 TB”, also the no-JS fallback); when empty the value is formatted for you. Scan mode: any rich text containing [data-scale] figures.

CSS classes

NameDescription
mv-to-scale-valueThe precise value (standalone), untouched app text when given.
mv-to-scale-glossInline role="button" span after the figure: .mv-to-scale-sign (≈ or =) and .mv-to-scale-gloss-text; aria-expanded while its popover is open.
mv-to-scale-lensThe popover (Popover API, role=dialog), placed right after its gloss in the DOM: .mv-to-scale-exact, then the panel.
mv-to-scale-cardCard variant: .mv-to-scale-head (.mv-to-scale-label, .mv-to-scale-value) and .mv-to-scale-body.
mv-to-scale-say / mv-to-scale-countThe comparison sentence and its highlighted figure.
mv-to-scale-visualPicture: .mv-to-scale-art (aria-hidden) with .mv-to-scale-dot / .mv-to-scale-seg (--_f fill 0..1) or .mv-to-scale-pulse, and the .mv-to-scale-key caption; data-kind count | share | tempo, data-style dots | bar.
mv-to-scale-alts / mv-to-scale-alt“Picture it as” group of toggle buttons (aria-pressed): .mv-to-scale-alt-check, -alt-text, -alt-per (the assumption in short: “3 GB each”).
mv-to-scale-math<details> “How this is calculated”: .mv-to-scale-steps (ordered steps, with the rounding), .mv-to-scale-assume, .mv-to-scale-source.

CSS variables

NameDefaultDescription
--mv-to-scale-accentvar(--mv-accent)Tint of the gloss, dots, bar and selected alternative.
--mv-to-scale-inkaccent mixed with --mv-fgText-safe version of the tint (≈ sign, check marks).

Accessibility

The comparison is always text, never only a picture or a color: the gloss is an inline role="button" (tabindex 0, Enter and Space, a span rather than a <button> so it wraps with the sentence on narrow screens) whose name reads “about 800 hours of HD video” (the ≈ sign is aria-hidden and replaced by “about” for screen readers), described by a hint saying it opens other comparisons and the math, with aria-haspopup="dialog", aria-expanded and aria-controls. The precise value stays in the text next to it and is repeated as “Exact value” in the popover. The popover is a labelled non-modal dialog placed right after its gloss in the DOM, so Tab walks into it naturally; opening it from the keyboard moves focus to the selected alternative, Escape closes it and returns focus to the gloss, and focus leaving it or a click outside closes it. Hover only previews (after a short intent delay, mouse only) and never steals focus; touch and click open it pinned. Alternatives are toggle buttons (aria-pressed) in a labelled group, reachable with Tab or the arrow keys, Home and End; choosing one is announced politely (“Now showing about 600,000 photos. Also applied to 1 other figure.”). The math is a native <details> disclosure with ordered steps, the rounding made explicit, the assumption and the source. Dots and bars are aria-hidden and always come with a visible text key (“Each dot is 100 hours of HD video.”), so nothing depends on color or shape. Reduced motion (OS or data-motion="reduce"): the tempo dot stops pulsing and the popover appears without sliding. Forced colors: glosses use button colors, dots and bars are drawn in CanvasText, the selected alternative gets a Highlight border.

Esta página foi traduzida com IA. Informar um problema de tradução