Refactor Funnel component for improved layout and metrics display

- Updated Funnel component to enhance the layout and spacing of elements for better visual clarity.
- Replaced bar height calculation with percentage-based width for a more responsive design.
- Added total conversion rate summary and improved user feedback on step metrics.
- Simplified the rendering of funnel steps and drop-off indicators for better readability.
This commit is contained in:
Bill Yang 2025-04-28 23:04:51 -07:00
parent f8b579511b
commit 20ed14e293

View file

@ -3,7 +3,7 @@
import { FunnelResponse } from "@/api/analytics/useGetFunnel";
import { DateSelector } from "@/components/DateSelector/DateSelector";
import { Time } from "@/components/DateSelector/types";
import { ArrowRight } from "lucide-react";
import { ArrowDown, ArrowRight, ChevronRight } from "lucide-react";
export type FunnelChartData = {
stepName: string;
@ -45,11 +45,11 @@ export function Funnel({
const lastStep = chartData[chartData.length - 1];
const totalConversionRate = lastStep?.conversionRate || 0;
const maxBarHeight = 333;
const maxBarWidth = 100; // as percentage
return (
<div>
<div className="flex justify-end items-center gap-2 mb-2">
<div className="flex justify-end items-center gap-2 mb-6">
<DateSelector time={time} setTime={setTime} />
</div>
@ -63,55 +63,28 @@ export function Funnel({
</div>
</div>
) : data && chartData.length > 0 ? (
<div className="space-y-4">
{/* Chart grid and background */}
<div className="relative h-[350px]">
{/* Grid lines */}
<div className="absolute inset-0 flex flex-col justify-between pointer-events-none">
{[100, 80, 60, 40, 20, 0].map((value) => (
<div
key={value}
className="w-full border-b border-neutral-200 dark:border-neutral-800 flex items-center"
>
<span className="text-xs text-neutral-500 pr-2">
{value}%
</span>
</div>
))}
<div className="space-y-1">
{/* Overall conversion rate summary */}
<div className="border border-neutral-800 rounded-md p-4 mb-4 bg-neutral-900/50">
<div className="text-sm text-neutral-400">Total Conversion</div>
<div className="text-2xl font-semibold">
{totalConversionRate.toFixed(2)}%
</div>
{/* Bars container */}
<div className="absolute inset-0 pt-6 flex items-end ml-8">
{chartData.map((step, index) => {
// Calculate pixel height of the bar based on proportion of visitors
const ratio = firstStep?.visitors
? step.visitors / firstStep.visitors
: 0;
const barHeight = Math.max(ratio * maxBarHeight, 0);
return (
<div
key={step.stepNumber}
className="flex-1 flex flex-col items-center px-2"
>
<div
className="w-full bg-accent-400/70 rounded-lg"
style={{ height: `${barHeight}px` }}
></div>
</div>
);
})}
<div className="text-sm text-neutral-400">
{lastStep?.visitors.toLocaleString()} out of{" "}
{firstStep?.visitors.toLocaleString()} users
</div>
</div>
{/* Step details */}
<div
style={{
gridTemplateColumns: `repeat(${chartData.length}, 1fr)`,
}}
className={`grid gap-0 ml-8`}
>
{/* Funnel steps list */}
<div className="space-y-6">
{chartData.map((step, index) => {
// Calculate the percentage width for the bar
const ratio = firstStep?.visitors
? step.visitors / firstStep.visitors
: 0;
const barWidth = Math.max(ratio * maxBarWidth, 0);
// For step 2+, calculate the number of users who dropped off
const prevStep = index > 0 ? chartData[index - 1] : null;
const droppedUsers = prevStep
@ -122,47 +95,69 @@ export function Funnel({
: 0;
return (
<div
key={step.stepNumber}
className="text-sm rounded-md p-1 relative pl-3 border-l border-l-neutral-800 first:border-l-0"
>
<div className="font-medium flex items-center gap-2">
{step.stepName}
<div key={step.stepNumber} className="relative pb-6">
{/* Step number indicator */}
<div className="flex items-center mb-2">
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-neutral-800 flex items-center justify-center text-xs mr-2">
{step.stepNumber}
</div>
<div className="font-medium text-base">{step.stepName}</div>
</div>
{/* Entering users */}
<div className="mt-2">
<div className="flex items-center text-green-600 dark:text-green-500">
<ArrowRight className="w-4 h-4 mr-2" />
<div>
<span className="font-medium">
{/* Bar and metrics */}
<div className="flex items-center pl-8">
{/* Metrics */}
<div className="flex-shrink-0 min-w-[180px] mr-4">
<div className="flex items-baseline">
<span className="text-lg font-semibold">
{step.visitors.toLocaleString()}
</span>{" "}
<span className="text-neutral-500 text-sm">
{index === 0
? ` (100%)`
: ` (${step.conversionRate.toFixed(2)}%)`}
</span>
<span className="text-sm text-neutral-400 ml-2">
users
</span>
</div>
<div className="text-sm">
{index === 0 ? (
<span className="text-neutral-400">Entry point</span>
) : (
<span className="text-green-500">
{step.conversionRate.toFixed(2)}% conversion
</span>
)}
</div>
</div>
{/* Dropped off users - only for steps after the first */}
{index > 0 && (
<div className="flex items-center text-orange-500 mt-1">
<div className="w-4 h-4 mr-2 flex items-center justify-center">
</div>
<div>
<span className="font-medium">
{droppedUsers.toLocaleString()}
</span>{" "}
<span className="text-neutral-500 text-sm">
{/* Bar */}
<div className="flex-grow h-10 bg-neutral-800 rounded-md overflow-hidden">
<div
className="h-full bg-accent-400/70 rounded-md"
style={{ width: `${barWidth}%` }}
></div>
</div>
</div>
{/* Dropoff indicator */}
{index < chartData.length - 1 && (
<div className="absolute left-[11px] -bottom-6 top-6 flex flex-col items-center">
<div className="h-full w-0.5 bg-neutral-800"></div>
</div>
)}
{/* Dropoff metrics */}
{index < chartData.length - 1 && (
<div className="pl-8 mt-2 flex">
<div className="min-w-[180px] mr-4">
<div className="flex items-baseline text-orange-500">
<span className="text-sm font-medium">
{droppedUsers.toLocaleString()} dropped off
</span>
<span className="text-sm text-neutral-400 ml-1">
({dropoffPercent.toFixed(2)}%)
</span>
</div>
</div>
)}
</div>
</div>
)}
</div>
);
})}