Exclusifbêta

Yet To Come <mv-yet-to-come>

Une métrique racontée comme une histoire en trois temps, conçue pour changer la suite : Past (sparkline, évolution sur la période et plus forte hausse ou baisse, avec son annotation), Now (valeur actuelle, écart par rapport à la période précédente, tendance par période) et Ahead (où la métrique arrive si la tendance actuelle se poursuit, avec une bande d’incertitude clairement étiquetée, nulle aujourd’hui et qui s’élargit à chaque période). Fournissez un historique horodaté et le composant ajuste lui-même une tendance linéaire ou composée, ou branchez votre propre modèle avec project(ctx) ; un objectif optionnel transforme la prévision en phrase simple (« Reaches $1M ARR around Jan 2028 », « Cash runs out around Feb 2028 », avec la plage de dates probable), et 1 à 3 leviers définis par l’application (des champs range natifs : churn, inscriptions hebdomadaires, baisse des dépenses, économies supplémentaires…) redessinent l’avenir en direct, la tendance inchangée restant visible en fantôme pointillé et l’écart étant formulé en toutes lettres (« 3 months sooner than on the current trend », « instead of after Mar 2028 »). Le panneau Ahead énonce toujours ses hypothèses en mots, les courbes se distinguent par leur type de trait (plein pour le réel, tirets pour la projection, pointillés pour la tendance actuelle) et pas seulement par la couleur, et la mise en page passe d’une vue scindée à des onglets accessibles dans les conteneurs étroits.

CatégorieAffichage de données
TypeWeb Component (<mv-yet-to-come>)
Statutbêta
KitDes chiffres qui s’expliquent
Installe aussibutton, slider
Keywordsexclusive, 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

Installation

node scripts/add.mjs yet-to-come --out ./src/marvelous

Agent IA avec le serveur MCP Marvelous UI : install_components({ slugs: ["yet-to-come"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Fichiers copiés (dépendances comprises) : 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.

Utilisation

Démarrage rapide, le balisage minimal qui fonctionne :

<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>

Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les 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>

Référence culturelle

Un chant de Noël, Charles Dickens (1843, livre). Trois esprits montrent à un homme son passé, son présent et l’avenir qui adviendra si rien ne change, et c’est la vision de cet avenir qui le pousse à changer de cap. L’interface raconte une métrique de la même façon, passé, présent et avenir selon la tendance actuelle, et donne à l’utilisateur des leviers pour voir cet avenir changer avant qu’il n’advienne.

API

Attributes

NameTypeDefaultDescription
labelstringName of the metric (“Annual recurring revenue”), shown in the header and used as the accessible name of the group and of the tabs.
formatnumber | currency | percentnumberHow 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%).
currencyISO 4217 codeUSDCurrency for format="currency" (also the default of currency levers).
unitstringSuffix appended to every value (“t CO₂e”, “GB”, “days”). Ignored for percent.
decimalsnumberFixed number of decimals for values under 10,000 (default: automatic).
localeBCP 47 tagen-USLocale for numbers and dates (dates are read and written in UTC).
periodday | week | month | quarter | yearStep of the series and of the projection (months, quarters and years follow the calendar). Guessed from the median gap between history points when omitted.
horizonnumber (periods)How many periods ahead to project. Defaults to the length of the history (3 to 60).
fit-windownumber (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.
modellinear | exponential | autolinearBuilt-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.
targetnumberGoal 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-labelstringWording of the target in sentences and on the chart (“$1M ARR”, “$0”). Default: the formatted value.
target-modereach | avoidreachreach: 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.
confidencenumber (0.5..0.99)0.8Coverage 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.
layoutauto | split | tabs | stackautosplit: 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.
panelpast | present | futurefutureSelected panel in the tabs layout (reflects the user’s choice).
heading-level1..63Level of the three panel headings, to fit the page outline.
data-layout / data-reveal / data-changed / data-pendingset by the componentResolved 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

NameTypeDescription
historyArray<[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 }].
leversArray<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.
valuesRecord<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.
momentsArray<{ 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 }) => stringReplaces format / currency / unit / decimals for every value.
stringsPartial<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.
projectionobject | nullLatest result, read-only: the same object as mv-projection’s detail.
label / format / target / horizon / …reflectedEvery attribute has a matching property (camelCase: fitWindow, targetLabel, targetMode, headingLevel).

Methods

NameDescription
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

NameDescription
mv-leverCancelable, 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-projectionAfter 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

NameDescription
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

NameDescription
mv-yet-to-come-root / -header / -label / -tabs / -tab / -panelsCard 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 / -momentPast panel: sparkline with start, end, note and key-moment dots; summary sentences.
mv-yet-to-come-value / -delta[data-direction] / -trendNow 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 / -assumptionsAhead 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-notchLever group; each lever is a native <input type="range" class="mv-slider">, and the notch marks its starting value once moved.

CSS variables

NameDefaultDescription
--mv-yet-to-come-actualvar(--mv-fg)Color of the actual (past) line and its area.
--mv-yet-to-come-projectionvar(--mv-accent)Color of the projection, band edges, Ahead tint and changed lever values.
--mv-yet-to-come-bandprojection at 15%Fill of the uncertainty band.
--mv-yet-to-come-targetvar(--mv-fg-muted)Target line and label.
--mv-yet-to-come-good / -badvar(--mv-success) / var(--mv-danger)Outcome colors (always paired with an icon and words).
--mv-yet-to-come-chart-height13remHeight of the Ahead chart.
--mv-yet-to-come-paddingvar(--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.

Cette page a été traduite par IA. Signaler un problème de traduction