Exclusivobeta
Yet To Come <mv-yet-to-come>
Una métrica contada como una historia en tres partes, pensada para cambiar lo que pasa después: Pasado (sparkline, variación en el periodo y el mayor salto o caída, con su anotación), Ahora (valor actual, delta frente al periodo anterior, tendencia por periodo) y Por venir (dónde acaba la métrica si la tendencia actual continúa, con una banda de incertidumbre claramente etiquetada que es cero hoy y se ensancha con cada periodo). Dale un historial con marcas de tiempo y ajusta por sí mismo una tendencia lineal o compuesta, o conecta tu propio modelo con project(ctx); un objetivo opcional convierte la previsión en una frase sencilla (“Alcanza 1 M $ de ARR hacia ene. de 2028”, “La caja se agota hacia feb. de 2028”, con el rango de fechas probable) y de 1 a 3 palancas definidas por la app (inputs range nativos: churn, altas semanales, recorte de gastos, ahorro extra…) redibujan el futuro en vivo, con la tendencia sin cambios conservada como fantasma punteado y la diferencia explicada (“3 meses antes que con la tendencia actual”, “en lugar de después de mar. de 2028”). El panel Por venir siempre expresa sus supuestos con palabras, las líneas se distinguen por el patrón del trazo (continua la real, discontinua la proyección, punteada la tendencia actual) y no solo por el color, y el layout pasa de una vista dividida a pestañas accesibles en contenedores estrechos.
| Categoría | Visualización de datos |
|---|---|
| Tipo | Web Component (<mv-yet-to-come>) |
| Estado | beta |
| Kit | Cifras que se explican solas |
| También instala | button, slider |
| Keywords | exclusive, culture, forecast, projection, trend, what-if, scenario, kpi, metrics, goal, target, runway, burn-rate, savings, sparkline, uncertainty, confidence-band, levers, dashboard, saas |
When to use
- A SaaS dashboard should show when ARR, users or MRR reach a goal on the current trend and what a lower churn would change
- A finance screen must show cash runway and let founders test a spending cut or a bridge round before deciding
- A savings, fitness or reading-streak app should turn a goal into a date and show what one extra weekly effort would buy
- A capacity or budget report (storage, carbon, headcount) needs a forecast with an honest range and its assumptions in words
Avoid when
- The data only needs to be plotted and explored, without a projection, a goal or levers → use Chart instead
- Several discrete scenarios with many inputs must be compared side by side → use Fork instead
- The series is seasonal, cyclical or driven by one-off events: a trend line would promise a false future unless project() models it
Instalación
node scripts/add.mjs yet-to-come --out ./src/marvelousAgente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["yet-to-come"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Archivos copiados (dependencias incluidas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, core/motion.js, core/observe.js, components/yet-to-come/yet-to-come.js, components/yet-to-come/yet-to-come.css, components/button/button.css, components/slider/slider.js, components/slider/slider.css.
Uso
Inicio rápido, el marcado mínimo que funciona:
<mv-yet-to-come label="Annual recurring revenue" format="currency" target="1000000">
<script type="application/json" data-history>[["2026-04-01", 812000], ["2026-05-01", 818000], ["2026-06-01", 822000], ["2026-07-01", 829000], ["2026-08-01", 834000], ["2026-09-01", 840360]]</script>
</mv-yet-to-come>Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables CSS:
<div id="ytc-demo" style="width:min(100%,70rem);margin-inline:auto">
<style>
#ytc-demo { display:grid; gap:1.5rem }
#ytc-demo .ytc-bar { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.75rem 1rem }
#ytc-demo .ytc-bar h3 { margin:0; font-size:1.0625rem; letter-spacing:-.01em }
#ytc-demo .ytc-bar p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
#ytc-demo .ytc-actions { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem .75rem }
#ytc-demo .ytc-row { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1.5rem; align-items:start }
#ytc-demo .ytc-col { display:grid; gap:.75rem; min-width:0 }
#ytc-demo .ytc-log { display:flex; align-items:center; gap:.5rem; color:var(--mv-fg-muted); font-size:.75rem; min-width:0 }
#ytc-demo .ytc-log span:last-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
@media (max-width:52rem) { #ytc-demo .ytc-row { grid-template-columns:minmax(0,1fr) } }
</style>
<section class="ytc-col" aria-labelledby="ytc-arr-h">
<div class="ytc-bar">
<div>
<h3 id="ytc-arr-h">Revenue outlook</h3>
<p>Lumen Analytics · B2B plan at $149 per customer per month</p>
</div>
<div class="ytc-actions">
<label class="mv-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="ytc-band" checked>
<span class="mv-choice-text"><span class="mv-choice-title">Range band</span></span>
</label>
<button class="mv-button" data-variant="outline" data-size="sm" type="button" id="ytc-close">Close October</button>
</div>
</div>
<mv-yet-to-come id="ytc-arr" label="Annual recurring revenue" format="currency" target="1000000" target-label="$1M ARR" horizon="18" period="month">
<script type="application/json" data-history>[
["2025-09-01", 712000], ["2025-10-01", 731000], ["2025-11-01", 744000], ["2025-12-01", 752000],
["2026-01-01", 761000], ["2026-02-01", 768000], ["2026-03-01", 801000, "Team plan launched"],
["2026-04-01", 812000], ["2026-05-01", 818000], ["2026-06-01", 822000], ["2026-07-01", 829000],
["2026-08-01", 834000], ["2026-09-01", 840360]
]</script>
<script type="application/json" data-levers>[
{ "id": "churn", "label": "Monthly churn", "min": 0.01, "max": 0.05, "step": 0.001, "value": 0.03, "format": "percent", "decimals": 1 },
{ "id": "signups", "label": "New customers per week", "min": 1, "max": 8, "step": 0.1, "value": 3.7, "decimals": 1 }
]</script>
</mv-yet-to-come>
<div class="ytc-log" aria-hidden="true"><span class="mv-badge" data-variant="secondary" data-shape="pill" id="ytc-status">mv-projection</span><span id="ytc-last">Waiting for the first projection…</span></div>
</section>
<div class="ytc-row">
<section class="ytc-col" aria-labelledby="ytc-cash-h">
<div class="ytc-bar"><div><h3 id="ytc-cash-h">Runway</h3><p>Built-in straight-line trend, a limit to stay clear of</p></div></div>
<mv-yet-to-come id="ytc-cash" label="Cash in the bank" format="currency" target="0" target-label="$0" target-mode="avoid" horizon="24">
<script type="application/json" data-history>[
["2025-09-30", 2460000], ["2025-10-31", 2388000], ["2025-11-30", 2312000], ["2025-12-31", 2241000],
["2026-01-31", 2152000], ["2026-02-28", 2074000], ["2026-03-31", 1981000, "Two senior hires"],
["2026-04-30", 1897000], ["2026-05-31", 1809000], ["2026-06-30", 1724000], ["2026-07-31", 1638000],
["2026-08-31", 1553000], ["2026-09-30", 1471000]
]</script>
<script type="application/json" data-levers>[
{ "id": "cut", "label": "Cut monthly spend by", "min": 0, "max": 40000, "step": 1000, "value": 0, "format": "currency", "effect": "add" },
{ "id": "bridge", "label": "Bridge round", "min": 0, "max": 750000, "step": 25000, "value": 0, "format": "currency", "effect": "shift" }
]</script>
</mv-yet-to-come>
</section>
<section class="ytc-col" aria-labelledby="ytc-save-h">
<div class="ytc-bar"><div><h3 id="ytc-save-h">Savings goal</h3><p>Weekly data, compact tabs layout</p></div></div>
<mv-yet-to-come id="ytc-save" label="Emergency fund" format="currency" target="10000" horizon="26" panel="past" layout="tabs">
<script type="application/json" data-history>[
["2026-06-07", 3120], ["2026-06-14", 3260], ["2026-06-21", 3410], ["2026-06-28", 3395, "Car repair"],
["2026-07-05", 3540], ["2026-07-12", 3700], ["2026-07-19", 3830], ["2026-07-26", 3990],
["2026-08-02", 4105], ["2026-08-09", 4280], ["2026-08-16", 4400], ["2026-08-23", 4560],
["2026-08-30", 4690], ["2026-09-06", 4850], ["2026-09-13", 4975], ["2026-09-20", 5140]
]</script>
<script type="application/json" data-levers>[
{ "id": "extra", "label": "Extra per week", "min": 0, "max": 150, "step": 5, "value": 0, "format": "currency", "effect": "add", "description": "Set aside on top of your usual deposits." }
]</script>
</mv-yet-to-come>
</section>
</div>
<script type="module">
await customElements.whenDefined("mv-yet-to-come");
const arr = document.getElementById("ytc-arr");
const cash = document.getElementById("ytc-cash");
const PRICE = 149;
// The app's own model: customers churn and sign up each month; ARR = 12 × MRR.
arr.project = ({ history, levers, steps }) => {
let customers = history.at(-1).v / (PRICE * 12);
const perMonth = (levers.signups * 52) / 12;
return {
points: steps.map(() => {
customers = customers * (1 - levers.churn) + perMonth;
return customers * PRICE * 12;
}),
assumptions: `Every customer pays $${PRICE} per month; ARR is 12 times the monthly revenue of active customers.`,
};
};
cash.strings = { avoidHit: "Cash runs out around {date}", avoidClear: "Cash lasts past {end}" };
const status = document.getElementById("ytc-status");
const last = document.getElementById("ytc-last");
arr.addEventListener("mv-projection", (e) => {
const { status: s, statement } = e.detail;
status.textContent = s;
status.dataset.variant = s === "reached" ? "success" : "secondary";
last.textContent = statement;
});
document.getElementById("ytc-band").addEventListener("change", (e) => {
for (const el of document.querySelectorAll("#ytc-demo mv-yet-to-come")) el.band = e.target.checked ? "true" : "false";
});
// New actuals arrive: the story moves one month forward.
const close = document.getElementById("ytc-close");
close.addEventListener("click", () => {
const h = arr.history;
const t = new Date(h.at(-1).t);
t.setUTCMonth(t.getUTCMonth() + 1);
arr.history = [...h, { t, v: h.at(-1).v + 6100 }];
close.disabled = true;
close.textContent = "October closed";
});
</script>
</div>Referencia cultural
Cuento de Navidad, Charles Dickens (1843, libro). Tres espíritus le muestran a un hombre su pasado, su presente y el futuro que llegará si nada cambia, y ver ese futuro es lo que le hace cambiar de rumbo. La UI cuenta una métrica de la misma forma, con el pasado, el ahora y el futuro según la tendencia actual, y le da al usuario palancas para ver cómo cambia ese futuro antes de que ocurra.
API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
label | string | Name of the metric (“Annual recurring revenue”), shown in the header and used as the accessible name of the group and of the tabs. | |
format | number | currency | percent | number | How values are written (Intl.NumberFormat). Values of 10,000 and more are shown in compact notation ($840K, 1.2M). percent expects fractions (0.42 = 42%). |
currency | ISO 4217 code | USD | Currency for format="currency" (also the default of currency levers). |
unit | string | Suffix appended to every value (“t CO₂e”, “GB”, “days”). Ignored for percent. | |
decimals | number | Fixed number of decimals for values under 10,000 (default: automatic). | |
locale | BCP 47 tag | en-US | Locale for numbers and dates (dates are read and written in UTC). |
period | day | week | month | quarter | year | Step of the series and of the projection (months, quarters and years follow the calendar). Guessed from the median gap between history points when omitted. | |
horizon | number (periods) | How many periods ahead to project. Defaults to the length of the history (3 to 60). | |
fit-window | number (points) | Only the last N history points feed the built-in trend and the range (e.g. 6 to follow a recent change of pace). Default: all. | |
model | linear | exponential | auto | linear | Built-in trend: a straight line (least squares, continuing from today’s actual value) or compound growth (log-linear fit, positive values only). auto keeps compound growth only when it fits clearly better. Also sizes the range band when project() is used. |
target | number | Goal or limit. The Ahead panel then says when the projection crosses it (interpolated between periods), the likely date range from the band, and draws a target line with a crossing marker. | |
target-label | string | Wording of the target in sentences and on the chart (“$1M ARR”, “$0”). Default: the formatted value. | |
target-mode | reach | avoid | reach | reach: crossing is the goal (“Reaches … around …”, success outcome). avoid: crossing is the danger (“Hits … around …”, danger outcome; “Stays clear of … through …” otherwise), for runway, budgets and quotas. |
band | "true" | "false" | true | "false" hides the uncertainty band, its legend entry, the date range and its sentence in the assumptions. |
confidence | number (0.5..0.99) | 0.8 | Coverage of the band. It is a random-walk-with-drift range: the spread of past period-to-period changes around the trend, zero today and widening with the square root of the distance plus the uncertainty on the trend itself. |
layout | auto | split | tabs | stack | auto | split: Past and Now stacked on the left, Ahead on the right. tabs: one panel at a time behind an ARIA tablist. stack: all three in one column. auto picks tabs when the element is narrower than 640px, split otherwise. |
panel | past | present | future | future | Selected panel in the tabs layout (reflects the user’s choice). |
heading-level | 1..6 | 3 | Level of the three panel headings, to fit the page outline. |
data-layout / data-reveal / data-changed / data-pending | set by the component | Resolved layout; story reveal state (pending until first seen, then done); present while a lever differs from its starting value; present while an async project() is running (aria-busy is set too). |
Properties
| Name | Tipo | Description |
|---|---|---|
history | Array<[t, v, note?] | { t, v, note? }> | Timestamped values (Date, ISO string or epoch ms). Sorted for you; a note marks the point in the Past panel and names the biggest jump when it falls on it. Can also come from a child <script type="application/json" data-history>. Reading it returns [{ t: Date, v, note }]. |
levers | Array<Lever> | Lever = { id, label, min = 0, max = 100, step, value, format?: "number" | "currency" | "percent" | (v) => string, decimals?, unit?, currency?, description?, assumption? (template with {label} {value} {base}), effect? }. value "auto" starts a rate lever at the fitted trend. effect (built-in model only): add (value added per period, default), rate (replaces the trend per period, or the growth rate for exponential), scale (multiplies the trend), shift (one-time amount added from the next period). Also from <script type="application/json" data-levers>. Setting them resets their values. |
values | Record<string, number> | Current lever values. Setting merges and clamps (e.g. restore a saved scenario); it re-projects and announces like a user change. |
project | (ctx) => number[] | Point[] | { points, assumptions? } | Promise<…> | Your model. ctx = { history, levers (values by id), steps (Date of each future period), period, horizon, baseline (true when computing the unchanged scenario), fit: { model, rate, slope } }. Return one value per step (or { v, lo?, hi? } to supply your own range; missing ranges are sized by the built-in fit). assumptions is a sentence prepended to the lever list. Async results show a pending state; stale ones are dropped. |
moments | Array<{ t, label }> | Extra annotated moments (a launch, a price change) marked on the sparkline; the one inside the biggest jump names it. Also from <script type="application/json" data-moments>. |
formatter | (value, { compact, signed, precise }) => string | Replaces format / currency / unit / decimals for every value. |
strings | Partial<Record<string, string>> | Overrides for every visible text and announcement, with {placeholders} (e.g. { avoidHit: "Cash runs out around {date}", avoidClear: "Cash lasts past {end}" }). Keys: past, present, future, futureIdle, futureChanged, tabs, empty, pastUp, pastDown, pastFlat, momentUp, momentDown, onDate, inDate, asOf, versusPrev, trend, perPeriod, reach, notReached, already, avoidHit, avoidClear, endpoint, rangeDates, rangeOpen, rangePossible, rangeValue, sooner, later, same, insteadOfNever, insteadOf, versus, assumptions, assumeLinear, assumeExp, assumeCustom, assumeLever, assumeLeverChanged, assumeBand, assumeNoBand, levers, reset, leverNow, legendActual, legendProjection, legendBand, legendBaseline, legendTarget, now, projected, tablePast, tableFuture, tableDate, tableValue, tableRange. English defaults. |
projection | object | null | Latest result, read-only: the same object as mv-projection’s detail. |
label / format / target / horizon / … | reflected | Every attribute has a matching property (camelCase: fitWindow, targetLabel, targetMode, headingLevel). |
Methods
| Name | Description |
|---|---|
setLever(id, value) | Sets one lever (clamped to its range) and re-projects. |
resetLevers() | Puts every lever back to its starting value (the “Reset” button does the same). |
refresh() | Re-runs the fit and the projection, e.g. after inputs of your project() changed outside the levers. |
show(panel) | Selects "past", "present" or "future" in the tabs layout. |
Events
| Name | Description |
|---|---|
mv-lever | Cancelable, on every input of a lever. detail: { id, value, previous, values }. preventDefault() puts the input back (e.g. a value your plan does not allow). |
mv-projection | After every projection, including the first. detail: { points: [{ t: Date, v, lo, hi }] (index 0 = today’s actual value), baseline (the unchanged-trend points when a lever moved, else null), status: "reached" | "not-reached" | "already" | "none", crossing: { date, earliest, latest, periods } | null, statement, range, assumptions, changed, levers }. |
Content structure
| Name | Description |
|---|---|
script[data-history] / [data-levers] / [data-moments] | Optional JSON children for plain-HTML use; the component’s own DOM is appended after them. |
CSS classes
| Name | Description |
|---|---|
mv-yet-to-come-root / -header / -label / -tabs / -tab / -panels | Card frame, header with the metric label and the tablist (tabs layout only). |
mv-yet-to-come-panel[data-panel] | past | present | future panels; -panel-head, -title (hollow, filled or dashed marker), -sub, -asof. |
mv-yet-to-come-spark / -summary / -moment | Past panel: sparkline with start, end, note and key-moment dots; summary sentences. |
mv-yet-to-come-value / -delta[data-direction] / -trend | Now panel: big value, delta chip with arrow icon and sign, trend per period. |
mv-yet-to-come-statement[data-outcome] / -outcome / -range / -chart / -plot / -legend / -assumptions | Ahead panel: outcome sentence (good | bad | neutral, with an icon), date or value range, chart (-history, -projection, -baseline, -band, -target-line, -now-line paths; -dot[data-kind], -target-label, -cross-label, -chip hover readout), legend and assumptions. |
mv-yet-to-come-levers / -lever[data-changed] / -lever-input / -lever-notch | Lever group; each lever is a native <input type="range" class="mv-slider">, and the notch marks its starting value once moved. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-yet-to-come-actual | var(--mv-fg) | Color of the actual (past) line and its area. |
--mv-yet-to-come-projection | var(--mv-accent) | Color of the projection, band edges, Ahead tint and changed lever values. |
--mv-yet-to-come-band | projection at 15% | Fill of the uncertainty band. |
--mv-yet-to-come-target | var(--mv-fg-muted) | Target line and label. |
--mv-yet-to-come-good / -bad | var(--mv-success) / var(--mv-danger) | Outcome colors (always paired with an icon and words). |
--mv-yet-to-come-chart-height | 13rem | Height of the Ahead chart. |
--mv-yet-to-come-padding | var(--mv-space-5) | Inner padding of the panels. |
Accessibility
The element is a labelled group (aria-labelledby the metric label) and every panel carries a real heading (heading-level) plus a text summary that stands on its own: Past says where the metric went and its biggest move, Now gives the value, the delta with its sign and the trend in words, Ahead gives the outcome sentence, the range and the assumptions. Charts are SVG with aria-hidden; each has a visually hidden table equivalent (date, value, and the range for projected points, sampled to 24 rows), and the hover readout is a pointer-only extra. Levers are native <input type="range"> elements with a <label>, an optional description linked by aria-describedby, and aria-valuetext set to the formatted value (“2.0%”), so arrows, Page Up/Down and Home/End work everywhere; the visible value is aria-hidden to avoid double reading. Projection changes are announced once in a polite live region, 700 ms after the last lever move (“Reaches $1M ARR around Jan 2028, instead of after Mar 2028. As early as …”), never on the initial render. In the tabs layout the panel switcher is a proper tablist (roving tabindex, arrows, Home/End, automatic activation, aria-controls/aria-selected) and panels are tabpanels labelled by their tab; in the split and stack layouts the panels are labelled groups. Nothing relies on color alone: actual, projection and current trend are solid, dashed and dotted lines (the legend repeats the patterns), the outcome has an icon and words, the delta has an arrow and a sign, panel markers differ by shape. The async state sets aria-busy. Reduced motion (OS or data-motion="reduce"): no story reveal, no line glide, tab panels switch instantly. Forced colors: lines map to CanvasText, Highlight and GrayText, the band keeps its dashed edges, the selected tab gets a Highlight outline.