A net-zero pathway chart with a dashed target line and filled envelope, a self-drawing actuals line with a leading dot, and reduction-vs-target progress.
npx shadcn@latest add @paragon/emissions-target-tracker"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export interface EmissionsTargetTrackerProps extends React.ComponentProps<"div"> {
/** Actual emissions per period so far (nulls allowed for future gaps). */
actual?: (number | null)[];
/** The planned net-zero pathway across all periods. */
pathway?: (number | null)[];
/** Period labels (e.g. years). */
labels?: string[];
/** Baseline year value used to compute % reduction. */
baseline?: number;
unit?: string;
}
const DEFAULT_PATHWAY = [100, 88, 76, 63, 50, 38, 25, 12, 0];
const DEFAULT_ACTUAL = [100, 91, 79, 66, 54, null, null, null, null];
const DEFAULT_LABELS = ["'20", "'22", "'24", "'26", "'28", "'30", "'32", "'34", "'40"];
const W = 320;
const H = 150;
const PAD_L = 4;
const PAD_R = 4;
const PAD_T = 10;
const PAD_B = 18;
function scaleX(i: number, n: number) {
return PAD_L + (i / (n - 1)) * (W - PAD_L - PAD_R);
}
function scaleY(v: number, max: number) {
return PAD_T + (H - PAD_T - PAD_B) * (1 - v / max);
}
function linePath(values: (number | null)[], max: number, n: number) {
let d = "";
let started = false;
values.forEach((v, i) => {
if (v == null) return;
const cmd = started ? "L" : "M";
d += `${cmd} ${scaleX(i, n).toFixed(2)} ${scaleY(v, max).toFixed(2)} `;
started = true;
});
return d.trim();
}
/**
* A net-zero pathway chart: the planned reduction path drawn as a dashed target
* line with a filled envelope, and actuals-to-date drawn on top with a leading
* dot. The actual stroke draws itself in on mount (one-shot dash reveal, reduced
* motion safe). A progress readout shows reduction achieved vs. the target.
*/
export function EmissionsTargetTracker({
actual = DEFAULT_ACTUAL,
pathway = DEFAULT_PATHWAY,
labels = DEFAULT_LABELS,
baseline = 100,
unit = "%",
className,
...props
}: EmissionsTargetTrackerProps) {
const n = Math.max(pathway.length, actual.length, labels.length);
const present = [...pathway, ...actual].filter((v): v is number => v != null);
const max = Math.max(baseline, ...present);
const pathwayLine = linePath(pathway, max, n);
const actualLine = linePath(actual, max, n);
// Envelope under the pathway.
const area =
pathwayLine +
` L ${scaleX(n - 1, n).toFixed(2)} ${(H - PAD_B).toFixed(2)}` +
` L ${scaleX(0, n).toFixed(2)} ${(H - PAD_B).toFixed(2)} Z`;
const lastActualIdx = actual.reduce<number>(
(acc, v, i) => (v != null ? i : acc),
-1,
);
const lastActual =
lastActualIdx >= 0 ? (actual[lastActualIdx] as number) : baseline;
const targetAtNow = pathway[lastActualIdx] ?? baseline;
const reduction = Math.round(baseline - lastActual);
const onTrack = lastActual <= targetAtNow + 2;
return (
<div
data-slot="emissions-target-tracker"
className={cn(
"w-full max-w-md rounded-xl bg-card p-5 text-card-foreground shadow-border",
className,
)}
{...props}
>
<style href="paragon-emissions-target-tracker" precedence="paragon">{`
@keyframes paragon-etarget-draw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
[data-etarget-draw] { animation: none !important; stroke-dashoffset: 0 !important; }
}
`}</style>
<div className="flex items-start justify-between">
<div>
<h3 className="text-sm font-medium">Net-zero pathway</h3>
<p className="text-xs text-muted-foreground">
Scope 1 & 2 vs. 2020 baseline
</p>
</div>
<span
className={cn(
"rounded-full px-2.5 py-0.5 text-xs font-medium",
onTrack ? "bg-success/10 text-success" : "bg-warning/10 text-warning",
)}
>
{onTrack ? "On track" : "Behind"}
</span>
</div>
<div className="mt-3 flex items-baseline gap-2">
<span className="text-2xl font-semibold tabular-nums">
−{reduction}
{unit}
</span>
<span className="text-xs text-muted-foreground tabular-nums">
target −{Math.round(baseline - targetAtNow)}
{unit} by now
</span>
</div>
<svg
role="img"
aria-label={`Emissions reduced ${reduction} percent against a net-zero pathway`}
viewBox={`0 0 ${W} ${H}`}
className="mt-3 w-full"
>
<defs>
<linearGradient id="etarget-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--color-success)" stopOpacity="0.16" />
<stop offset="100%" stopColor="var(--color-success)" stopOpacity="0.01" />
</linearGradient>
</defs>
{[0, 0.5, 1].map((f) => (
<line
key={f}
x1={PAD_L}
x2={W - PAD_R}
y1={PAD_T + (H - PAD_T - PAD_B) * f}
y2={PAD_T + (H - PAD_T - PAD_B) * f}
className="stroke-border"
strokeWidth={1}
strokeDasharray="2 3"
/>
))}
<path d={area} fill="url(#etarget-fill)" />
<path
d={pathwayLine}
fill="none"
className="stroke-success/50"
strokeWidth={1.5}
strokeDasharray="4 3"
strokeLinecap="round"
/>
<path
data-etarget-draw
d={actualLine}
fill="none"
className="stroke-foreground"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
pathLength={1}
strokeDasharray={1}
strokeDashoffset={1}
style={{ animation: "paragon-etarget-draw 1s var(--ease-out) forwards" }}
/>
{lastActualIdx >= 0 && (
<circle
cx={scaleX(lastActualIdx, n)}
cy={scaleY(lastActual, max)}
r={3}
className="fill-foreground"
/>
)}
{labels.map((lbl, i) => (
<text
key={lbl + i}
x={scaleX(i, n)}
y={H - 5}
textAnchor={i === 0 ? "start" : i === n - 1 ? "end" : "middle"}
className="fill-muted-foreground"
style={{ fontSize: 9 }}
>
{lbl}
</text>
))}
</svg>
<div className="mt-2 flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1.5">
<span className="h-0.5 w-3 rounded-full bg-foreground" /> Actual
</span>
<span className="flex items-center gap-1.5">
<span className="h-0.5 w-3 rounded-full bg-success/50" /> Target path
</span>
</div>
</div>
);
}