← back to Open Seo
src/client/features/backlinks/BacklinksPageCharts.tsx
199 lines
import { useEffect, useRef, useState } from "react";
import {
CartesianGrid,
Legend,
Line,
LineChart,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import type { BacklinksOverviewData } from "./backlinksPageTypes";
import {
formatCompactDate,
formatMonthLabel,
formatTooltipValue,
} from "./backlinksPageUtils";
export function BacklinksTrendChart({
data,
}: {
data: BacklinksOverviewData["trends"];
}) {
const { containerRef, chartWidth } = useChartWidth();
if (data.length === 0) {
return <EmptyChartState />;
}
return (
<div
ref={containerRef}
className="h-56 min-w-0"
aria-label="Backlink trend chart"
>
{chartWidth > 0 ? (
<LineChart
width={chartWidth}
height={224}
data={data}
margin={{ left: 8, right: 8, top: 8, bottom: 0 }}
>
<CartesianGrid
strokeDasharray="3 3"
stroke="currentColor"
opacity={0.12}
/>
<XAxis
dataKey="date"
tickFormatter={formatChartTick}
minTickGap={24}
/>
<YAxis yAxisId="left" tickFormatter={formatAxisValue} width={60} />
<YAxis
yAxisId="right"
orientation="right"
tickFormatter={formatAxisValue}
width={60}
/>
<Tooltip
formatter={formatTooltipValue}
labelFormatter={formatChartLabel}
/>
<Legend />
<Line
yAxisId="left"
type="monotone"
dataKey="backlinks"
stroke="#2563eb"
strokeWidth={2}
dot={false}
name="Backlinks"
/>
<Line
yAxisId="right"
type="monotone"
dataKey="referringDomains"
stroke="#14b8a6"
strokeWidth={2}
dot={false}
name="Referring domains"
/>
</LineChart>
) : null}
</div>
);
}
export function BacklinksNewLostChart({
data,
}: {
data: BacklinksOverviewData["newLostTrends"];
}) {
const { containerRef, chartWidth } = useChartWidth();
if (data.length === 0) {
return <EmptyChartState />;
}
return (
<div
ref={containerRef}
className="h-56 min-w-0"
aria-label="New and lost backlinks chart"
>
{chartWidth > 0 ? (
<LineChart
width={chartWidth}
height={224}
data={data}
margin={{ left: 8, right: 8, top: 8, bottom: 0 }}
>
<CartesianGrid
strokeDasharray="3 3"
stroke="currentColor"
opacity={0.12}
/>
<XAxis
dataKey="date"
tickFormatter={formatChartTick}
minTickGap={24}
/>
<YAxis tickFormatter={formatAxisValue} width={60} />
<Tooltip
formatter={formatTooltipValue}
labelFormatter={formatChartLabel}
/>
<Legend />
<Line
type="monotone"
dataKey="lostBacklinks"
stroke="#ef4444"
strokeWidth={2}
dot={false}
name="Lost backlinks"
/>
<Line
type="monotone"
dataKey="newBacklinks"
stroke="#16a34a"
strokeWidth={2}
dot={false}
name="New backlinks"
/>
</LineChart>
) : null}
</div>
);
}
function useChartWidth() {
const containerRef = useRef<HTMLDivElement | null>(null);
const [chartWidth, setChartWidth] = useState(0);
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
const updateWidth = () => {
setChartWidth(container.clientWidth);
};
updateWidth();
const observer = new ResizeObserver(updateWidth);
observer.observe(container);
return () => {
observer.disconnect();
};
}, []);
return { containerRef, chartWidth };
}
function EmptyChartState() {
return (
<div className="flex h-56 items-center justify-center rounded-xl border border-dashed border-base-300 text-sm text-base-content/55">
Not enough historical data yet.
</div>
);
}
function formatAxisValue(value: unknown) {
if (typeof value !== "number") return "";
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
if (value >= 1_000) return `${(value / 1_000).toFixed(0)}K`;
return String(value);
}
function formatChartTick(value: unknown) {
return typeof value === "string" ? formatMonthLabel(value) : "";
}
function formatChartLabel(value: unknown) {
return typeof value === "string" ? formatCompactDate(value) : "";
}