From 96326ad19325a43f7cf7f1b1022729a8d8bd512a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Tue, 20 Aug 2024 23:27:04 +0200 Subject: [PATCH] better previous indicator and funnel --- .../report/PreviousDiffIndicator.tsx | 83 +++--- .../components/report/chart/ChartProvider.tsx | 8 +- .../components/report/chart/MetricCard.tsx | 15 +- .../report/chart/ReportBarChart.tsx | 7 +- .../src/components/report/funnel/Funnel.tsx | 243 ++++++++++-------- packages/common/index.ts | 1 + packages/common/src/get-previous-metric.ts | 39 +++ packages/trpc/src/routers/chart.helpers.ts | 48 +--- 8 files changed, 231 insertions(+), 213 deletions(-) create mode 100644 packages/common/src/get-previous-metric.ts diff --git a/apps/dashboard/src/components/report/PreviousDiffIndicator.tsx b/apps/dashboard/src/components/report/PreviousDiffIndicator.tsx index 9c4b0092..b10288b7 100644 --- a/apps/dashboard/src/components/report/PreviousDiffIndicator.tsx +++ b/apps/dashboard/src/components/report/PreviousDiffIndicator.tsx @@ -1,6 +1,11 @@ import { useNumber } from '@/hooks/useNumerFormatter'; 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 { useChartContext } from './chart/ChartProvider'; @@ -29,19 +34,24 @@ interface PreviousDiffIndicatorProps { state?: string | null | undefined; children?: React.ReactNode; inverted?: boolean; + className?: string; + size?: 'sm' | 'lg'; } export function PreviousDiffIndicator({ diff, state, + inverted, + size = 'sm', children, + className, }: PreviousDiffIndicatorProps) { const { previous, previousIndicatorInverted } = useChartContext(); const variant = getDiffIndicator( - previousIndicatorInverted, + inverted ?? previousIndicatorInverted, state, - 'success', - 'destructive', + 'bg-emerald-300', + 'bg-rose-300', undefined ); const number = useNumber(); @@ -52,62 +62,35 @@ export function PreviousDiffIndicator({ const renderIcon = () => { if (state === 'positive') { - return ; + return ; } if (state === 'negative') { - return ; + return ; } return null; }; return ( <> - - {renderIcon()} +
+
+ {renderIcon()} +
{number.format(diff)}% - +
{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 ; - } - if (state === 'negative') { - return ; - } - return null; - }; - - return ( -
- {renderIcon()} - {number.short(diff)}% -
- ); -} diff --git a/apps/dashboard/src/components/report/chart/ChartProvider.tsx b/apps/dashboard/src/components/report/chart/ChartProvider.tsx index 8725740f..9362a24c 100644 --- a/apps/dashboard/src/components/report/chart/ChartProvider.tsx +++ b/apps/dashboard/src/components/report/chart/ChartProvider.tsx @@ -26,7 +26,13 @@ const ChartContext = createContext(null); export function ChartProvider({ children, ...props }: IChartProviderProps) { return ( - {children} + + {children} + ); } diff --git a/apps/dashboard/src/components/report/chart/MetricCard.tsx b/apps/dashboard/src/components/report/chart/MetricCard.tsx index 9238ec6a..a24a9e04 100644 --- a/apps/dashboard/src/components/report/chart/MetricCard.tsx +++ b/apps/dashboard/src/components/report/chart/MetricCard.tsx @@ -10,7 +10,7 @@ import type { IChartMetric } from '@openpanel/validation'; import { getDiffIndicator, - PreviousDiffIndicatorText, + PreviousDiffIndicator, } from '../PreviousDiffIndicator'; import { useChartContext } from './ChartProvider'; import { SerieName } from './SerieName'; @@ -49,9 +49,9 @@ export function MetricCard({ const graphColors = getDiffIndicator( previousIndicatorInverted, previous?.state, - 'green', - 'red', - 'blue' + '#6ee7b7', // green + '#fda4af', // red + '#93c5fd' // blue ); return ( @@ -64,7 +64,7 @@ export function MetricCard({ >
@@ -96,15 +96,14 @@ export function MetricCard({
- {/* */}
{renderValue(serie.metrics[metric], 'ml-1 font-light text-xl')}
-
diff --git a/apps/dashboard/src/components/report/chart/ReportBarChart.tsx b/apps/dashboard/src/components/report/chart/ReportBarChart.tsx index 873af28c..9922e6b7 100644 --- a/apps/dashboard/src/components/report/chart/ReportBarChart.tsx +++ b/apps/dashboard/src/components/report/chart/ReportBarChart.tsx @@ -5,19 +5,17 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuShortcut, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { useNumber } from '@/hooks/useNumerFormatter'; import type { IChartData } from '@/trpc/client'; import { cn } from '@/utils/cn'; import { DropdownMenuPortal } from '@radix-ui/react-dropdown-menu'; -import { ExternalLinkIcon, FilterIcon } from 'lucide-react'; import { round } from '@openpanel/common'; import { NOT_SET_VALUE } from '@openpanel/constants'; -import { PreviousDiffIndicatorText } from '../PreviousDiffIndicator'; +import { PreviousDiffIndicator } from '../PreviousDiffIndicator'; import { useChartContext } from './ChartProvider'; import { SerieIcon } from './SerieIcon'; import { SerieName } from './SerieName'; @@ -73,9 +71,8 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
- {serie.metrics.previous?.[metric]?.value}
diff --git a/apps/dashboard/src/components/report/funnel/Funnel.tsx b/apps/dashboard/src/components/report/funnel/Funnel.tsx index 65f3ed5b..a326ef5a 100644 --- a/apps/dashboard/src/components/report/funnel/Funnel.tsx +++ b/apps/dashboard/src/components/report/funnel/Funnel.tsx @@ -2,20 +2,23 @@ import { ColorSquare } from '@/components/color-square'; import { AutoSizer } from '@/components/react-virtualized-auto-sizer'; +import { TooltipComplete } from '@/components/tooltip-complete'; import { Progress } from '@/components/ui/progress'; import { Widget, WidgetBody } from '@/components/widget'; import type { RouterOutputs } from '@/trpc/client'; import { cn } from '@/utils/cn'; import { round } from '@/utils/math'; import { getChartColor } from '@/utils/theme'; -import { AlertCircleIcon } from 'lucide-react'; +import { AlertCircleIcon, TrendingUp } from 'lucide-react'; import { last } from 'ramda'; import { Cell, Pie, PieChart } from 'recharts'; +import { getPreviousMetric } from '@openpanel/common'; import { alphabetIds } from '@openpanel/constants'; import type { IChartInput } from '@openpanel/validation'; import { useChartContext } from '../chart/ChartProvider'; +import { PreviousDiffIndicator } from '../PreviousDiffIndicator'; const findMostDropoffs = ( steps: RouterOutputs['chart']['funnel']['current']['steps'] @@ -74,80 +77,43 @@ export function FunnelSteps({ return withWidget(
-
- - {({ width }) => { - const height = width; - return ( -
- - - - - - -
-
{round(lastStep.percent, 2)}%
-
-
- ); - }} -
+
+ {steps.map((step) => { + return ( +
+
+
+ ); + })}
-
-
Insights
-
- - {lastStep.count} - of - {totalSessions} - - - {round(lastStep.percent, 2)}% - compared to - - {round(prevLastStep.percent, 2)}% - - - +
+
+
- {mostDropoffs.event.displayName} + {lastStep.count} of {totalSessions} + {' '} + sessions{' '} +
+
+ Last period:{' '} + + {prevLastStep.count} of {previous.totalSessions} - lost - - {mostDropoffs.dropoffCount} sessions - - +
+
@@ -167,46 +133,113 @@ export function FunnelSteps({
{step.event.displayName.replace(/_/g, ' ')}
-
-
- - Total: - - {step.previousCount} -
-
- - Dropoff: - - - {isMostDropoffs && } - {step.dropoffCount} - -
-
- - Current: - -
- {step.count} - {/*
+ } + > +
+ + Total: + +
+ + {step.previousCount} + +
+
+ + + + Last period:{' '} + + {previous.steps[index]?.dropoffCount} + + + +
+ } + > +
+ + Dropoff: + +
+ + {isMostDropoffs && } + {step.dropoffCount} + +
+
+ + + + Last period:{' '} + + {previous.steps[index]?.count} + + + +
+ } + > +
+ + Current: + +
+ {step.count} + {/* */} + } + > + Inspect + */} +
-
+
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, + }; +} diff --git a/packages/trpc/src/routers/chart.helpers.ts b/packages/trpc/src/routers/chart.helpers.ts index 2fe752c3..451d723e 100644 --- a/packages/trpc/src/routers/chart.helpers.ts +++ b/packages/trpc/src/routers/chart.helpers.ts @@ -13,12 +13,13 @@ import { subYears, } from 'date-fns'; 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 { average, completeSerie, + getPreviousMetric, max, min, round, @@ -26,7 +27,6 @@ import { sum, } from '@openpanel/common'; import type { ISerieDataItem } from '@openpanel/common'; -import { alphabetIds } from '@openpanel/constants'; import { chQuery, createSqlBuilder, @@ -44,7 +44,6 @@ import type { IChartRange, IGetChartDataInput, IInterval, - PreviousValue, } from '@openpanel/validation'; 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 [funnelRes, sessionRes] = await Promise.all([ - 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)}')` - ), - ]); + const funnelRes = await chQuery<{ level: number; count: number }>(sql); if (funnelRes[0]?.level !== payload.events.length) { funnelRes.unshift({ @@ -318,7 +312,6 @@ export async function getFunnelData({ }); } - const totalSessions = sessionRes[0]?.count ?? 0; const filledFunnelRes = funnelRes.reduce( (acc, item, index) => { const diff = @@ -346,6 +339,7 @@ export async function getFunnelData({ [] as typeof funnelRes ); + const totalSessions = last(filledFunnelRes)?.count ?? 0; const steps = reverse(filledFunnelRes) .filter((item) => item.level !== 0) .reduce( @@ -663,37 +657,3 @@ export async function getChart(input: IChartInput) { 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, - }; -}