エクスクルーシブ

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

指標を 3 部構成の物語として伝え、次に起こることを変えるために作られています。Past(スパークライン、期間中の変化、注釈付きの最大の上昇または下落)、Now(現在値、前期間との差分、期間ごとのトレンド)、Ahead(現在のトレンドが続いた場合の着地点と、今日はゼロで期間ごとに広がる、明確にラベル付けされた不確実性の帯)です。

文化的リファレンス

クリスマス・キャロル、チャールズ・ディケンズ(1843年、書籍)。3 人の精霊がある男に過去、現在、そして何も変わらなければ訪れる未来を見せ、その未来を目にしたことで彼は生き方を改めます。UI は指標を同じように語ります:過去、現在、そして現在の傾向が続いた場合の未来を示し、その未来が実際に起こる前に変わる様子を確かめられるレバーをユーザーに渡します。

仕組み

次に起こることを変えるために作られた、3 部構成のストーリーとして語られる指標:Past(スパークライン、期間中の変化、最大の上昇または下落とその注釈)、Now(現在値、前期間との差分、期間ごとの傾向)、Ahead(現在の傾向が続いた場合に指標が行き着く値。明確にラベル付けされた不確実性の帯付きで、帯は今日はゼロで、期間ごとに広がります)。タイムスタンプ付きの履歴を渡すと直線または複利の傾向を自動で当てはめ、project(ctx) で独自のモデルを組み込むこともできます。オプションの目標を設定すると予測が平易な文になり(「Reaches $1M ARR around Jan 2028」「Cash runs out around Feb 2028」、可能性の高い日付範囲付き)、アプリ定義の 1 から 3 個のレバー(ネイティブの range 入力:解約率、週間登録数、支出削減、追加の節約…)で未来がライブで再描画されます。変わらない傾向は点線のゴーストとして残り、差がはっきり示されます(「3 months sooner than on the current trend」「instead of after Mar 2028」)。Ahead パネルは常に前提を言葉で示し、線は色だけでなく線種で区別され(実線は実績、破線は予測、点線は現在の傾向)、狭いコンテナではレイアウトが分割表示からアクセシブルなタブに切り替わります。

カテゴリーデータ表示
タイプWeb Component(<mv-yet-to-come>)
ステータス安定版
キット根拠を自ら語る数値
同時にインストールされるものbutton, 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

インストール

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

Marvelous UI MCP サーバーを使う AI エージェント:install_components({ slugs: ["yet-to-come"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。

コピーされるファイル(依存関係を含む):tokens/tokens.css, core/base.css, components/button/button.css, core/dom.js, core/element.js, components/slider/slider.js, components/slider/slider.css, 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。

使い方

クイックスタート、動作する最小限のマークアップ:

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

基本となるマークアップです。ここから属性、data-*、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>Kestrel 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", () => {
      if (close.getAttribute("aria-disabled") === "true") return;
      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.setAttribute("aria-disabled", "true"); // not disabled: the focus stays on the button
      close.textContent = "October closed";
    });
  </script>
</div>

API

Attributes

NameタイプDefaultDescription
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

NameタイプDescription
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, busy, count ({n} {unit}), unitDay, unitDays, unitWeek, unitWeeks, unitMonth, unitMonths, unitQuarter, unitQuarters, unitYear, unitYears (period words; the singular or plural form follows Intl.PluralRules for locale), adjDay, adjWeek, adjMonth, adjQuarter, adjYear (“monthly”…), quarterDate (“Q{q} {year}”). 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. Known limits: the built-in model is a straight line or compound growth only (seasonal or event-driven series need your own project()); the chart hover readout has no keyboard equivalent, the hidden tables carry the same values; period words follow Intl.PluralRules with one singular and one plural string, so languages with more plural forms need a custom count template.

このページは AI で翻訳されています。翻訳の問題を報告