better previous indicator and funnel
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { TrendingDownIcon, TrendingUpIcon } from 'lucide-react';
|
import {
|
||||||
|
ArrowDownIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
|
TrendingDownIcon,
|
||||||
|
TrendingUpIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { Badge } from '../ui/badge';
|
import { Badge } from '../ui/badge';
|
||||||
import { useChartContext } from './chart/ChartProvider';
|
import { useChartContext } from './chart/ChartProvider';
|
||||||
@@ -29,19 +34,24 @@ interface PreviousDiffIndicatorProps {
|
|||||||
state?: string | null | undefined;
|
state?: string | null | undefined;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
inverted?: boolean;
|
inverted?: boolean;
|
||||||
|
className?: string;
|
||||||
|
size?: 'sm' | 'lg';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PreviousDiffIndicator({
|
export function PreviousDiffIndicator({
|
||||||
diff,
|
diff,
|
||||||
state,
|
state,
|
||||||
|
inverted,
|
||||||
|
size = 'sm',
|
||||||
children,
|
children,
|
||||||
|
className,
|
||||||
}: PreviousDiffIndicatorProps) {
|
}: PreviousDiffIndicatorProps) {
|
||||||
const { previous, previousIndicatorInverted } = useChartContext();
|
const { previous, previousIndicatorInverted } = useChartContext();
|
||||||
const variant = getDiffIndicator(
|
const variant = getDiffIndicator(
|
||||||
previousIndicatorInverted,
|
inverted ?? previousIndicatorInverted,
|
||||||
state,
|
state,
|
||||||
'success',
|
'bg-emerald-300',
|
||||||
'destructive',
|
'bg-rose-300',
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
const number = useNumber();
|
const number = useNumber();
|
||||||
@@ -52,62 +62,35 @@ export function PreviousDiffIndicator({
|
|||||||
|
|
||||||
const renderIcon = () => {
|
const renderIcon = () => {
|
||||||
if (state === 'positive') {
|
if (state === 'positive') {
|
||||||
return <TrendingUpIcon size={15} />;
|
return <ArrowUpIcon strokeWidth={3} size={12} color="#000" />;
|
||||||
}
|
}
|
||||||
if (state === 'negative') {
|
if (state === 'negative') {
|
||||||
return <TrendingDownIcon size={15} />;
|
return <ArrowDownIcon strokeWidth={3} size={12} color="#000" />;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Badge className="flex gap-1" variant={variant}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-1 font-medium',
|
||||||
|
size === 'lg' && 'gap-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
`flex size-4 items-center justify-center rounded-full`,
|
||||||
|
variant,
|
||||||
|
size === 'lg' && 'size-8'
|
||||||
|
)}
|
||||||
|
>
|
||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
|
</div>
|
||||||
{number.format(diff)}%
|
{number.format(diff)}%
|
||||||
</Badge>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PreviousDiffIndicatorText({
|
|
||||||
diff,
|
|
||||||
state,
|
|
||||||
className,
|
|
||||||
}: PreviousDiffIndicatorProps & { className?: string }) {
|
|
||||||
const { previous, previousIndicatorInverted } = useChartContext();
|
|
||||||
const number = useNumber();
|
|
||||||
if (diff === null || diff === undefined || previous === false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderIcon = () => {
|
|
||||||
if (state === 'positive') {
|
|
||||||
return <TrendingUpIcon size={15} />;
|
|
||||||
}
|
|
||||||
if (state === 'negative') {
|
|
||||||
return <TrendingDownIcon size={15} />;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn([
|
|
||||||
'flex items-center gap-0.5',
|
|
||||||
getDiffIndicator(
|
|
||||||
previousIndicatorInverted,
|
|
||||||
state,
|
|
||||||
'text-emerald-600',
|
|
||||||
'text-red-600',
|
|
||||||
undefined
|
|
||||||
),
|
|
||||||
className,
|
|
||||||
])}
|
|
||||||
>
|
|
||||||
{renderIcon()}
|
|
||||||
{number.short(diff)}%
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,7 +26,13 @@ const ChartContext = createContext<IChartContextType | null>(null);
|
|||||||
|
|
||||||
export function ChartProvider({ children, ...props }: IChartProviderProps) {
|
export function ChartProvider({ children, ...props }: IChartProviderProps) {
|
||||||
return (
|
return (
|
||||||
<ChartContext.Provider value={props}>{children}</ChartContext.Provider>
|
<ChartContext.Provider
|
||||||
|
value={
|
||||||
|
props.chartType === 'funnel' ? { ...props, previous: true } : props
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ChartContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { IChartMetric } from '@openpanel/validation';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getDiffIndicator,
|
getDiffIndicator,
|
||||||
PreviousDiffIndicatorText,
|
PreviousDiffIndicator,
|
||||||
} from '../PreviousDiffIndicator';
|
} from '../PreviousDiffIndicator';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { SerieName } from './SerieName';
|
import { SerieName } from './SerieName';
|
||||||
@@ -49,9 +49,9 @@ export function MetricCard({
|
|||||||
const graphColors = getDiffIndicator(
|
const graphColors = getDiffIndicator(
|
||||||
previousIndicatorInverted,
|
previousIndicatorInverted,
|
||||||
previous?.state,
|
previous?.state,
|
||||||
'green',
|
'#6ee7b7', // green
|
||||||
'red',
|
'#fda4af', // red
|
||||||
'blue'
|
'#93c5fd' // blue
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -64,7 +64,7 @@ export function MetricCard({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'pointer-events-none absolute -bottom-1 -left-1 -right-1 top-0 z-0 opacity-20 transition-opacity duration-300 group-hover:opacity-50',
|
'pointer-events-none absolute -bottom-1 -left-1 -right-1 top-0 z-0 opacity-50 transition-opacity duration-300 group-hover:opacity-100',
|
||||||
editMode && 'bottom-1'
|
editMode && 'bottom-1'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -96,15 +96,14 @@ export function MetricCard({
|
|||||||
<SerieName name={serie.names} />
|
<SerieName name={serie.names} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* <PreviousDiffIndicator {...serie.metrics.previous[metric]} /> */}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end justify-between">
|
<div className="flex items-end justify-between">
|
||||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-2xl font-bold">
|
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-2xl font-bold">
|
||||||
{renderValue(serie.metrics[metric], 'ml-1 font-light text-xl')}
|
{renderValue(serie.metrics[metric], 'ml-1 font-light text-xl')}
|
||||||
</div>
|
</div>
|
||||||
<PreviousDiffIndicatorText
|
<PreviousDiffIndicator
|
||||||
{...previous}
|
{...previous}
|
||||||
className="mb-0.5 text-xs font-medium"
|
className="text-xs text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,19 +5,17 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import type { IChartData } from '@/trpc/client';
|
import type { IChartData } from '@/trpc/client';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { DropdownMenuPortal } from '@radix-ui/react-dropdown-menu';
|
import { DropdownMenuPortal } from '@radix-ui/react-dropdown-menu';
|
||||||
import { ExternalLinkIcon, FilterIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
import { round } from '@openpanel/common';
|
import { round } from '@openpanel/common';
|
||||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||||
|
|
||||||
import { PreviousDiffIndicatorText } from '../PreviousDiffIndicator';
|
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { SerieIcon } from './SerieIcon';
|
import { SerieIcon } from './SerieIcon';
|
||||||
import { SerieName } from './SerieName';
|
import { SerieName } from './SerieName';
|
||||||
@@ -73,9 +71,8 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
|
|||||||
<SerieName name={serie.names} />
|
<SerieName name={serie.names} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-shrink-0 items-center justify-end gap-4">
|
<div className="flex flex-shrink-0 items-center justify-end gap-4">
|
||||||
<PreviousDiffIndicatorText
|
<PreviousDiffIndicator
|
||||||
{...serie.metrics.previous?.[metric]}
|
{...serie.metrics.previous?.[metric]}
|
||||||
className="text-xs font-medium"
|
|
||||||
/>
|
/>
|
||||||
{serie.metrics.previous?.[metric]?.value}
|
{serie.metrics.previous?.[metric]?.value}
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
|
|||||||
@@ -2,20 +2,23 @@
|
|||||||
|
|
||||||
import { ColorSquare } from '@/components/color-square';
|
import { ColorSquare } from '@/components/color-square';
|
||||||
import { AutoSizer } from '@/components/react-virtualized-auto-sizer';
|
import { AutoSizer } from '@/components/react-virtualized-auto-sizer';
|
||||||
|
import { TooltipComplete } from '@/components/tooltip-complete';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { Widget, WidgetBody } from '@/components/widget';
|
import { Widget, WidgetBody } from '@/components/widget';
|
||||||
import type { RouterOutputs } from '@/trpc/client';
|
import type { RouterOutputs } from '@/trpc/client';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { round } from '@/utils/math';
|
import { round } from '@/utils/math';
|
||||||
import { getChartColor } from '@/utils/theme';
|
import { getChartColor } from '@/utils/theme';
|
||||||
import { AlertCircleIcon } from 'lucide-react';
|
import { AlertCircleIcon, TrendingUp } from 'lucide-react';
|
||||||
import { last } from 'ramda';
|
import { last } from 'ramda';
|
||||||
import { Cell, Pie, PieChart } from 'recharts';
|
import { Cell, Pie, PieChart } from 'recharts';
|
||||||
|
|
||||||
|
import { getPreviousMetric } from '@openpanel/common';
|
||||||
import { alphabetIds } from '@openpanel/constants';
|
import { alphabetIds } from '@openpanel/constants';
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartInput } from '@openpanel/validation';
|
||||||
|
|
||||||
import { useChartContext } from '../chart/ChartProvider';
|
import { useChartContext } from '../chart/ChartProvider';
|
||||||
|
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||||
|
|
||||||
const findMostDropoffs = (
|
const findMostDropoffs = (
|
||||||
steps: RouterOutputs['chart']['funnel']['current']['steps']
|
steps: RouterOutputs['chart']['funnel']['current']['steps']
|
||||||
@@ -74,80 +77,43 @@ export function FunnelSteps({
|
|||||||
return withWidget(
|
return withWidget(
|
||||||
<div className="flex flex-col gap-4 @container">
|
<div className="flex flex-col gap-4 @container">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn('border border-border', !editMode && 'border-0 border-b')}
|
||||||
'rounded-lg border border-border',
|
|
||||||
!editMode && 'border-0 p-0'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-8 p-4">
|
<div className="flex items-center gap-8 p-4">
|
||||||
<div className="hidden shrink-0 @xl:block @xl:w-36">
|
<div className="hidden shrink-0 gap-2 @xl:flex">
|
||||||
<AutoSizer disableHeight>
|
{steps.map((step) => {
|
||||||
{({ width }) => {
|
|
||||||
const height = width;
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" style={{ width, height }}>
|
|
||||||
<PieChart width={width} height={height}>
|
|
||||||
<Pie
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
value: lastStep.percent,
|
|
||||||
label: 'Conversion',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 100 - lastStep.percent,
|
|
||||||
label: 'Dropoff',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
innerRadius={height / 3}
|
|
||||||
outerRadius={height / 2 - 10}
|
|
||||||
isAnimationActive={false}
|
|
||||||
nameKey="label"
|
|
||||||
dataKey="value"
|
|
||||||
>
|
|
||||||
<Cell strokeWidth={0} className="fill-highlight" />
|
|
||||||
<Cell strokeWidth={0} className="fill-def-200" />
|
|
||||||
</Pie>
|
|
||||||
</PieChart>
|
|
||||||
<div
|
<div
|
||||||
className="font-mono absolute inset-0 flex items-center justify-center font-bold"
|
className="flex h-20 w-8 items-end overflow-hidden rounded bg-def-200"
|
||||||
style={{
|
key={step.event.id}
|
||||||
fontSize: width / 6,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div>{round(lastStep.percent, 2)}%</div>
|
<div
|
||||||
</div>
|
className="w-full bg-def-400"
|
||||||
|
style={{ height: `${step.percent}%` }}
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
})}
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-1 items-center gap-4">
|
||||||
<div className="mb-1 text-xl font-semibold">Insights</div>
|
<div className="flex flex-1 flex-col">
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="text-2xl">
|
||||||
<InsightCard title="Converted">
|
|
||||||
<span className="font-bold">{lastStep.count}</span>
|
|
||||||
<span className="mx-2 text-muted-foreground">of</span>
|
|
||||||
<span className="text-muted-foreground">{totalSessions}</span>
|
|
||||||
</InsightCard>
|
|
||||||
<InsightCard
|
|
||||||
title={hasIncreased ? 'Trending up' : 'Trending down'}
|
|
||||||
>
|
|
||||||
<span className="font-bold">{round(lastStep.percent, 2)}%</span>
|
|
||||||
<span className="mx-2 text-muted-foreground">compared to</span>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{round(prevLastStep.percent, 2)}%
|
|
||||||
</span>
|
|
||||||
</InsightCard>
|
|
||||||
<InsightCard title={'Most dropoffs'}>
|
|
||||||
<span className="font-bold">
|
<span className="font-bold">
|
||||||
{mostDropoffs.event.displayName}
|
{lastStep.count} of {totalSessions}
|
||||||
</span>
|
</span>{' '}
|
||||||
<span className="mx-2 text-muted-foreground">lost</span>
|
sessions{' '}
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{mostDropoffs.dropoffCount} sessions
|
|
||||||
</span>
|
|
||||||
</InsightCard>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-xl text-muted-foreground">
|
||||||
|
Last period:{' '}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{prevLastStep.count} of {previous.totalSessions}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PreviousDiffIndicator
|
||||||
|
size="lg"
|
||||||
|
{...getPreviousMetric(lastStep.count, prevLastStep.count)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,33 +133,99 @@ export function FunnelSteps({
|
|||||||
<div className="font-semibold capitalize">
|
<div className="font-semibold capitalize">
|
||||||
{step.event.displayName.replace(/_/g, ' ')}
|
{step.event.displayName.replace(/_/g, ' ')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 text-sm">
|
<div className="flex items-center gap-8 text-sm">
|
||||||
|
<TooltipComplete
|
||||||
|
disabled={!previous.steps[index]}
|
||||||
|
content={
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<span>
|
||||||
|
Last period:{' '}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{previous.steps[index]?.previousCount}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<PreviousDiffIndicator
|
||||||
|
{...getPreviousMetric(
|
||||||
|
step.previousCount,
|
||||||
|
previous.steps[index]?.previousCount
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
Total:
|
Total:
|
||||||
</span>
|
</span>
|
||||||
<span className="font-semibold">{step.previousCount}</span>
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-lg font-bold">
|
||||||
|
{step.previousCount}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipComplete>
|
||||||
|
<TooltipComplete
|
||||||
|
disabled={!previous.steps[index]}
|
||||||
|
content={
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<span>
|
||||||
|
Last period:{' '}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{previous.steps[index]?.dropoffCount}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<PreviousDiffIndicator
|
||||||
|
inverted
|
||||||
|
{...getPreviousMetric(
|
||||||
|
step.dropoffCount,
|
||||||
|
previous.steps[index]?.dropoffCount
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
Dropoff:
|
Dropoff:
|
||||||
</span>
|
</span>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-1 font-semibold',
|
'flex items-center gap-1 text-lg font-bold',
|
||||||
isMostDropoffs && 'text-red-600'
|
isMostDropoffs && 'text-rose-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isMostDropoffs && <AlertCircleIcon size={14} />}
|
{isMostDropoffs && <AlertCircleIcon size={14} />}
|
||||||
{step.dropoffCount}
|
{step.dropoffCount}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipComplete>
|
||||||
|
<TooltipComplete
|
||||||
|
disabled={!previous.steps[index]}
|
||||||
|
content={
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<span>
|
||||||
|
Last period:{' '}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{previous.steps[index]?.count}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<PreviousDiffIndicator
|
||||||
|
{...getPreviousMetric(
|
||||||
|
step.count,
|
||||||
|
previous.steps[index]?.count
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
Current:
|
Current:
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div className="flex items-center gap-4">
|
||||||
<span className="font-semibold">{step.count}</span>
|
<span className="text-lg font-bold">{step.count}</span>
|
||||||
{/* <button
|
{/* <button
|
||||||
className="ml-2 underline"
|
className="ml-2 underline"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -207,6 +239,7 @@ export function FunnelSteps({
|
|||||||
</button> */}
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipComplete>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ export * from './src/slug';
|
|||||||
export * from './src/fill-series';
|
export * from './src/fill-series';
|
||||||
export * from './src/url';
|
export * from './src/url';
|
||||||
export * from './src/id';
|
export * from './src/id';
|
||||||
|
export * from './src/get-previous-metric';
|
||||||
|
|||||||
39
packages/common/src/get-previous-metric.ts
Normal file
39
packages/common/src/get-previous-metric.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { isNil } from 'ramda';
|
||||||
|
|
||||||
|
import type { PreviousValue } from '@openpanel/validation';
|
||||||
|
|
||||||
|
import { round } from './math';
|
||||||
|
|
||||||
|
export function getPreviousMetric(
|
||||||
|
current: number,
|
||||||
|
previous: number | null | undefined
|
||||||
|
): PreviousValue {
|
||||||
|
if (isNil(previous)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = round(
|
||||||
|
((current > previous
|
||||||
|
? current / previous
|
||||||
|
: current < previous
|
||||||
|
? previous / current
|
||||||
|
: 0) -
|
||||||
|
1) *
|
||||||
|
100,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
diff:
|
||||||
|
Number.isNaN(diff) || !Number.isFinite(diff) || current === previous
|
||||||
|
? null
|
||||||
|
: diff,
|
||||||
|
state:
|
||||||
|
current > previous
|
||||||
|
? 'positive'
|
||||||
|
: current < previous
|
||||||
|
? 'negative'
|
||||||
|
: 'neutral',
|
||||||
|
value: previous,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -13,12 +13,13 @@ import {
|
|||||||
subYears,
|
subYears,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import * as mathjs from 'mathjs';
|
import * as mathjs from 'mathjs';
|
||||||
import { pluck, repeat, reverse, uniq } from 'ramda';
|
import { last, pluck, repeat, reverse, uniq } from 'ramda';
|
||||||
import { escape } from 'sqlstring';
|
import { escape } from 'sqlstring';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
average,
|
average,
|
||||||
completeSerie,
|
completeSerie,
|
||||||
|
getPreviousMetric,
|
||||||
max,
|
max,
|
||||||
min,
|
min,
|
||||||
round,
|
round,
|
||||||
@@ -26,7 +27,6 @@ import {
|
|||||||
sum,
|
sum,
|
||||||
} from '@openpanel/common';
|
} from '@openpanel/common';
|
||||||
import type { ISerieDataItem } from '@openpanel/common';
|
import type { ISerieDataItem } from '@openpanel/common';
|
||||||
import { alphabetIds } from '@openpanel/constants';
|
|
||||||
import {
|
import {
|
||||||
chQuery,
|
chQuery,
|
||||||
createSqlBuilder,
|
createSqlBuilder,
|
||||||
@@ -44,7 +44,6 @@ import type {
|
|||||||
IChartRange,
|
IChartRange,
|
||||||
IGetChartDataInput,
|
IGetChartDataInput,
|
||||||
IInterval,
|
IInterval,
|
||||||
PreviousValue,
|
|
||||||
} from '@openpanel/validation';
|
} from '@openpanel/validation';
|
||||||
|
|
||||||
function getEventLegend(event: IChartEvent) {
|
function getEventLegend(event: IChartEvent) {
|
||||||
@@ -304,12 +303,7 @@ export async function getFunnelData({
|
|||||||
|
|
||||||
const sql = `SELECT level, count() AS count FROM (${innerSql}) GROUP BY level ORDER BY level DESC`;
|
const sql = `SELECT level, count() AS count FROM (${innerSql}) GROUP BY level ORDER BY level DESC`;
|
||||||
|
|
||||||
const [funnelRes, sessionRes] = await Promise.all([
|
const funnelRes = await chQuery<{ level: number; count: number }>(sql);
|
||||||
chQuery<{ level: number; count: number }>(sql),
|
|
||||||
chQuery<{ count: number }>(
|
|
||||||
`SELECT count(name) as count FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND name = 'session_start' AND (created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')`
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (funnelRes[0]?.level !== payload.events.length) {
|
if (funnelRes[0]?.level !== payload.events.length) {
|
||||||
funnelRes.unshift({
|
funnelRes.unshift({
|
||||||
@@ -318,7 +312,6 @@ export async function getFunnelData({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalSessions = sessionRes[0]?.count ?? 0;
|
|
||||||
const filledFunnelRes = funnelRes.reduce(
|
const filledFunnelRes = funnelRes.reduce(
|
||||||
(acc, item, index) => {
|
(acc, item, index) => {
|
||||||
const diff =
|
const diff =
|
||||||
@@ -346,6 +339,7 @@ export async function getFunnelData({
|
|||||||
[] as typeof funnelRes
|
[] as typeof funnelRes
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const totalSessions = last(filledFunnelRes)?.count ?? 0;
|
||||||
const steps = reverse(filledFunnelRes)
|
const steps = reverse(filledFunnelRes)
|
||||||
.filter((item) => item.level !== 0)
|
.filter((item) => item.level !== 0)
|
||||||
.reduce(
|
.reduce(
|
||||||
@@ -663,37 +657,3 @@ export async function getChart(input: IChartInput) {
|
|||||||
|
|
||||||
return final;
|
return final;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPreviousMetric(
|
|
||||||
current: number,
|
|
||||||
previous: number | null
|
|
||||||
): PreviousValue {
|
|
||||||
if (previous === null) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const diff = round(
|
|
||||||
((current > previous
|
|
||||||
? current / previous
|
|
||||||
: current < previous
|
|
||||||
? previous / current
|
|
||||||
: 0) -
|
|
||||||
1) *
|
|
||||||
100,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
diff:
|
|
||||||
Number.isNaN(diff) || !Number.isFinite(diff) || current === previous
|
|
||||||
? null
|
|
||||||
: diff,
|
|
||||||
state:
|
|
||||||
current > previous
|
|
||||||
? 'positive'
|
|
||||||
: current < previous
|
|
||||||
? 'negative'
|
|
||||||
: 'neutral',
|
|
||||||
value: previous,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user