import { ColorSquare } from '@/components/color-square'; import { Button } from '@/components/ui/button'; import { pushModal } from '@/modals'; import type { RouterOutputs } from '@/trpc/client'; import { cn } from '@/utils/cn'; import { ChevronRightIcon, InfoIcon, UsersIcon } from 'lucide-react'; import { alphabetIds } from '@openpanel/constants'; import { createChartTooltip } from '@/components/charts/chart-tooltip'; import { BarShapeBlue, BarShapeProps } from '@/components/charts/common-bar'; import { Tooltiper } from '@/components/ui/tooltip'; import { WidgetTable } from '@/components/widget-table'; import { useNumber } from '@/hooks/use-numer-formatter'; import { getChartColor, getChartTranslucentColor } from '@/utils/theme'; import { getPreviousMetric } from '@openpanel/common'; import { Bar, BarChart, CartesianGrid, Cell, ResponsiveContainer, XAxis, YAxis, } from 'recharts'; import { useXAxisProps, useYAxisProps } from '../common/axis'; import { PreviousDiffIndicatorPure } from '../common/previous-diff-indicator'; import { useReportChartContext } from '../context'; type Props = { data: { current: RouterOutputs['chart']['funnel']['current'][number]; previous: RouterOutputs['chart']['funnel']['current'][number] | null; }; }; export const Metric = ({ label, value, enhancer, className, }: { label: string; value: React.ReactNode; enhancer?: React.ReactNode; className?: string; }) => (
{label}
{value}
{enhancer &&
{enhancer}
}
); export function Summary({ data }: { data: RouterOutputs['chart']['funnel'] }) { const number = useNumber(); const highestConversion = data.current .slice(0) .sort((a, b) => b.lastStep.percent - a.lastStep.percent)[0]; const highestCount = data.current .slice(0) .sort((a, b) => b.lastStep.count - a.lastStep.count)[0]; return (
{highestConversion && (
} /> {number.formatWithUnit( highestConversion.lastStep.percent / 100, '%', )}
)} {highestCount && (
} /> {number.format(highestCount.lastStep.count)}
)}
); } function ChartName({ breakdowns, className, }: { breakdowns: string[]; className?: string }) { return (
{breakdowns.map((name, index) => { return ( <> {index !== 0 && } {name} ); })}
); } export function Tables({ data: { current: { steps, mostDropoffsStep, lastStep, breakdowns }, previous: previousData, }, }: Props) { const number = useNumber(); const hasHeader = breakdowns.length > 0; const { report: { projectId, startDate, endDate, range, interval, series: reportSeries, breakdowns: reportBreakdowns, previous, funnelWindow, funnelGroup, }, } = useReportChartContext(); const handleInspectStep = (step: (typeof steps)[0], stepIndex: number) => { if (!projectId || !step.event.id) return; // For funnels, we need to pass the step index so the modal can query // users who completed at least that step in the funnel sequence pushModal('ViewChartUsers', { type: 'funnel', report: { projectId, series: reportSeries, breakdowns: reportBreakdowns || [], interval: interval || 'day', startDate, endDate, range, previous, chartType: 'funnel', metric: 'sum', funnelWindow, funnelGroup, }, stepIndex, // Pass the step index for funnel queries }); }; return (
{hasHeader && }
) } /> ) } /> {!!mostDropoffsStep && ( {mostDropoffsStep?.dropoffCount} {' '} dropped after this event. Improve this step and your conversion rate will likely increase. } > } /> )}
item.event.id!} className={'text-sm @container'} columnClassName="px-2 group/row items-center" eachRow={(item, index) => { return (
); }} columns={[ { name: 'Event', render: (item, index) => (
{alphabetIds[index]} {item.event.displayName}
), width: 'w-full', className: 'text-left font-mono font-semibold', }, { name: 'Completed', render: (item) => number.format(item.count), className: 'text-right font-mono hidden @xl:block', width: '82px', }, { name: 'Dropped after', render: (item) => item.dropoffCount !== null && item.dropoffPercent !== null ? number.format(item.dropoffCount) : null, className: 'text-right font-mono hidden @xl:block', width: '110px', }, { name: 'Conversion', render: (item) => number.formatWithUnit(item.percent / 100, '%'), className: 'text-right font-mono font-semibold', width: '90px', }, { name: '', render: (item) => ( ), className: 'text-right', width: '48px', }, ]} />
); } type RechartData = { name: string; [key: `step:percent:${number}`]: number | null; [key: `step:data:${number}`]: | (RouterOutputs['chart']['funnel']['current'][number] & { step: RouterOutputs['chart']['funnel']['current'][number]['steps'][number]; }) | null; [key: `prev_step:percent:${number}`]: number | null; [key: `prev_step:data:${number}`]: | (RouterOutputs['chart']['funnel']['current'][number] & { step: RouterOutputs['chart']['funnel']['current'][number]['steps'][number]; }) | null; }; const useRechartData = ({ current, previous, }: RouterOutputs['chart']['funnel']): RechartData[] => { const firstFunnel = current[0]; return ( firstFunnel?.steps.map((step, stepIndex) => { return { id: step?.event.id ?? '', name: step?.event.displayName ?? '', ...current.reduce((acc, item, index) => { const diff = previous?.[index]; return { ...acc, [`step:percent:${index}`]: item.steps[stepIndex]?.percent ?? null, [`step:data:${index}`]: { ...item, step: item.steps[stepIndex], }, [`prev_step:percent:${index}`]: diff?.steps[stepIndex]?.percent ?? null, [`prev_step:data:${index}`]: diff ? { ...diff, step: diff?.steps?.[stepIndex], } : null, }; }, {}), }; }) ?? [] ); }; export function Chart({ data }: { data: RouterOutputs['chart']['funnel'] }) { const rechartData = useRechartData(data); const xAxisProps = useXAxisProps(); const yAxisProps = useYAxisProps(); const hasBreakdowns = data.current.length > 1; return (
data.current[0].steps.find((step) => step.event.id === id) ?.event.displayName ?? '' } /> {hasBreakdowns ? ( data.current.map((item, breakdownIndex) => ( } > {rechartData.map((item, stepIndex) => ( ))} )) ) : ( } > {rechartData.map((item, index) => ( ))} )}
); } const { Tooltip, TooltipProvider } = createChartTooltip< RechartData, { data: RouterOutputs['chart']['funnel']['current']; } >(({ data: dataArray, context, ...props }) => { const data = dataArray[0]!; const number = useNumber(); const variants = Object.keys(data).filter((key) => key.startsWith('step:data:'), ) as `step:data:${number}`[]; const index = context.data[0].steps.findIndex( (step) => step.event.id === (data as any).id, ); return ( <>
{data.name}
{variants.map((key, breakdownIndex) => { const variant = data[key]; const prevVariant = data[`prev_${key}`]; if (!variant?.step) { return null; } return (
1 ? breakdownIndex : index, ), }} />
{number.formatWithUnit(variant.step.percent / 100, '%')} ({number.format(variant.step.count)})
); })} ); });