mirror of
https://github.com/rybbit-io/rybbit.git
synced 2025-05-11 20:35:39 +02:00
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:
parent
f8b579511b
commit
20ed14e293
1 changed files with 72 additions and 77 deletions
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue