Sextant
← Catalog
Block · marketing

Channel ROI

Channels ranked by return on ad spend against a break-even reference, with a blended-ROAS hero and the underwater channels called out.

State
Size
Mode

Q2 2026 channel ROI

2.5×

Blended ROAS · $388k spend

  • At/above break-even
  • Underwater

Paid Search returns 4.00 per $1 spent; 2 of 6 channels are below break-even.

Q2 2026 channel ROI — return on ad spend by channel
ChannelROASSpendRevenue
Paid Search4.0×$148k$592k
Referral3.5×$24k$85k
Email3.4×$18k$61k
Paid Social1.6×$96k$157k
Display0.8×$64k$52k
Affiliates0.7×$38k$25k

Acme analytics · synced from platforms hourly · last updated 10 min ago

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

How it computes

Return on ad spend per channel: ROAS = revenue ÷ spend, ranked, classified against a break-even multiple.

ROAS = revenue / spend;  blended = Σrevenue / Σspend

Assumptions

  • Attribution is complete and one-to-one per channel.
  • Channels are treated independently.

Honest about

  • ROAS is 0 (not ∞) when spend ≤ 0.
  • Blended ROAS is total-revenue ÷ total-spend — a spend-weighted figure, not the average of per-channel ratios.
  • The break-even line (default 1.0×) is a visual reference; the metric is computed identically above and below it.

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
channelstringyes
spendnumberyes
revenuenumberyes

Static props

import { MarketingChannelRoi } from "@/components/blocks/marketing-channel-roi/marketing-channel-roi";

<MarketingChannelRoi data={myData} />

Client fetch (SWR)

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

Server component (RSC)

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

<MarketingChannelRoi data={data} />

Map arbitrary rows

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

const out = mapRows(rows, { /* contractKey: "yourColumn" */ }, dataSchema);
if (out.ok) <MarketingChannelRoi 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 (above, below)

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

Format numbers (currency / locale)

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

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

Replace or hide the narrative

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

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

Restyle any region

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

Drop the card chrome (headless plot)

import { MarketingChannelRoiPlot } from "@/components/blocks/marketing-channel-roi/marketing-channel-roi";

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