Sextant
← Catalog
Block · marketing

Spend Saturation

Spend vs conversions with a fitted diminishing-returns curve that marks where each extra dollar stops paying off.

State
Size
Mode

Paid acquisition saturation curve

$111

marginal cost / conversion

  • Weekly actuals
  • Response curve

Returns flatten beyond $14k/wk — the next conversion costs $111 vs $45 at low spend.

Acme marketing · Q1 2026 spend vs qualified conversions · √ response fit

Demo data is illustrative. Replace with your own typed data prop.

How it computes

Least-squares fit of a concave diminishing-returns curve: conversions ≈ a·√spend + b.

u = √spend;  a = (n·Σuy − Σu·Σy) / (n·Σu² − (Σu)²);  b = (Σy − a·Σu) / n

Assumptions

  • The relationship is linear in √spend — i.e. returns are concave / diminishing.
  • Observations have roughly homogeneous variance.
  • Spend is positive.

Honest about

  • Non-positive spend is filtered out before fitting.
  • If all spend is identical the denominator is 0 and the slope a defaults to 0 (no spurious fit).
  • The diminishing-returns "knee" is a heuristic clamped to 4× the minimum observed spend, not an extrapolation past your data.
  • Marginal cost-per-conversion clamps at 0 when marginal yield goes negative.

Use it with your data

Data contract

Passed via the data prop — an array of objects: validated at runtime, so a bad shape degrades to the error state rather than crashing.

FieldTypeRequired
spendnumberyes
conversionsnumberyes

Static props

import { MarketingSpendSaturation } from "@/components/blocks/marketing-spend-saturation/marketing-spend-saturation";

<MarketingSpendSaturation data={myData} />

Client fetch (SWR)

const { data, isLoading, error } = useSWR("/api/metric", fetcher);
// route isLoading/error through <ChartStates>, then:
<MarketingSpendSaturation data={data} />

Server component (RSC)

const data = await fetch(url, {
  next: { revalidate: 3600 },
}).then((r) => r.json());

<MarketingSpendSaturation data={data} />

Map arbitrary rows

import { mapRows } from "@/lib/sextant/column-map";

const out = mapRows(rows, { /* contractKey: "yourColumn" */ }, dataSchema);
if (out.ok) <MarketingSpendSaturation data={out.data} />;

Make it yours

Every customization below is an optional prop — omit them all and you get the defaults shown above. Recolor, reformat, restyle, or drop the card chrome without forking the component.

Recolor or rename a series (fit, actual)

<MarketingSpendSaturation
  series={{ fit: { color: "var(--chart-3)", label: "…" } }}
/>

Format numbers (currency / locale)

const eur = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR",
  notation: "compact",
});

<MarketingSpendSaturation valueFormatter={(n) => eur.format(n)} />

Replace or hide the narrative

<MarketingSpendSaturation narrative="Your own one-liner." />

// …or hide it entirely:
<MarketingSpendSaturation narrative={false} />

Restyle any region

<MarketingSpendSaturation
  className="max-w-xl"
  classNames={{ body: "bg-muted/20" }}
/>

Drop the card chrome (headless plot)

import { MarketingSpendSaturationPlot } from "@/components/blocks/marketing-spend-saturation/marketing-spend-saturation";

// Just the chart body — bring your own card/layout:
<MarketingSpendSaturationPlot data={data} config={config} />