diff --git a/apps/dashboard/src/components/color-square.tsx b/apps/dashboard/src/components/color-square.tsx index f98b7f44..f707dc45 100644 --- a/apps/dashboard/src/components/color-square.tsx +++ b/apps/dashboard/src/components/color-square.tsx @@ -7,7 +7,7 @@ export function ColorSquare({ children, className }: ColorSquareProps) { return (
diff --git a/apps/dashboard/src/components/report-chart/funnel/chart.tsx b/apps/dashboard/src/components/report-chart/funnel/chart.tsx index ef647b76..c77ed13b 100644 --- a/apps/dashboard/src/components/report-chart/funnel/chart.tsx +++ b/apps/dashboard/src/components/report-chart/funnel/chart.tsx @@ -10,11 +10,12 @@ import { getChartColor } from '@/utils/theme'; import { AlertCircleIcon } from 'lucide-react'; import { last } from 'ramda'; -import { getPreviousMetric } from '@openpanel/common'; +import { getPreviousMetric, round } from '@openpanel/common'; import { alphabetIds } from '@openpanel/constants'; import { PreviousDiffIndicator } from '../common/previous-diff-indicator'; import { useReportChartContext } from '../context'; +import { MetricCardNumber } from '../metric/metric-card'; const findMostDropoffs = ( steps: RouterOutputs['chart']['funnel']['current']['steps'], @@ -41,65 +42,74 @@ export function Chart({ const mostDropoffs = findMostDropoffs(steps); const lastStep = last(steps)!; const prevLastStep = last(previous.steps); - const withWidget = (children: React.ReactNode) => { - if (isEditMode) { - return ( - - {children} - - ); - } - return children; - }; - - return withWidget( -
+ return ( +
-
+
+
+ + } + /> + + } + /> + + } + /> +
{steps.map((step) => { return (
); })}
-
-
-
- - {lastStep.count} of {totalSessions} - {' '} - sessions{' '} -
-
- Last period:{' '} - - {prevLastStep?.count} of {previous.totalSessions} - -
-
- -
-
+
{steps.map((step, index) => { const percent = (step.count / totalSessions) * 100; const isMostDropoffs = mostDropoffs.event.id === step.event.id; @@ -112,8 +122,8 @@ export function Chart({ {alphabetIds[index]} -
- {step.event.displayName.replace(/_/g, ' ')} +
+ {step.event.displayName}
Last period:{' '} - + {previous.steps[index]?.previousCount} @@ -140,7 +150,7 @@ export function Chart({ Total:
- + {step.previousCount}
@@ -152,7 +162,7 @@ export function Chart({
Last period:{' '} - + {previous.steps[index]?.dropoffCount} @@ -173,7 +183,7 @@ export function Chart({
@@ -189,7 +199,7 @@ export function Chart({
Last period:{' '} - + {previous.steps[index]?.count} @@ -207,7 +217,7 @@ export function Chart({ Current:
- {step.count} + {step.count} {/*
); })}
-
, +
); } diff --git a/apps/dashboard/src/components/report-chart/metric/metric-card.tsx b/apps/dashboard/src/components/report-chart/metric/metric-card.tsx index 5a29ed20..77b2128e 100644 --- a/apps/dashboard/src/components/report-chart/metric/metric-card.tsx +++ b/apps/dashboard/src/components/report-chart/metric/metric-card.tsx @@ -104,24 +104,40 @@ export function MetricCard({ )}
-
-
-
- - - -
-
-
-
- {renderValue(serie.metrics[metric], 'ml-1 font-light text-xl')} -
+ } + value={renderValue(serie.metrics[metric], 'ml-1 font-light text-xl')} + enhancer={ + } + /> +
+ ); +} + +export function MetricCardNumber({ + label, + value, + enhancer, +}: { + label: React.ReactNode; + value: React.ReactNode; + enhancer?: React.ReactNode; +}) { + return ( +
+
+
+ {label}
+
+
{value}
+ {enhancer} +
); } diff --git a/apps/dashboard/src/components/ui/progress.tsx b/apps/dashboard/src/components/ui/progress.tsx index 66b2f5e3..40dbba18 100644 --- a/apps/dashboard/src/components/ui/progress.tsx +++ b/apps/dashboard/src/components/ui/progress.tsx @@ -6,16 +6,15 @@ import * as React from 'react'; const Progress = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - color: string; size?: 'sm' | 'default' | 'lg'; } ->(({ className, value, color, size = 'default', ...props }, ref) => ( +>(({ className, value, size = 'default', ...props }, ref) => ( {value && size !== 'sm' && ( -
+
{round(value, 2)}%
)} diff --git a/packages/trpc/src/routers/chart.helpers.ts b/packages/trpc/src/routers/chart.helpers.ts index d9470fe5..2e82f329 100644 --- a/packages/trpc/src/routers/chart.helpers.ts +++ b/packages/trpc/src/routers/chart.helpers.ts @@ -240,6 +240,28 @@ export function getDatesFromRange(range: IChartRange) { }; } +function fillFunnel(funnel: { level: number; count: number }[], steps: number) { + const filled = Array.from({ length: steps }, (_, index) => { + const level = index + 1; + const matchingResult = funnel.find((res) => res.level === level); + return { + level, + count: matchingResult ? matchingResult.count : 0, + }; + }); + + // Accumulate counts from top to bottom of the funnel + for (let i = filled.length - 1; i >= 0; i--) { + const step = filled[i]; + const prevStep = filled[i + 1]; + // If there's a previous step, add the count to the current step + if (step && prevStep) { + step.count += prevStep.count; + } + } + return filled.reverse(); +} + export function getChartStartEndDate({ startDate, endDate, @@ -304,41 +326,9 @@ export async function getFunnelData({ const sql = `SELECT level, count() AS count FROM (${innerSql}) WHERE level != 0 GROUP BY level ORDER BY level DESC`; - const funnelRes = await chQuery<{ level: number; count: number }>(sql); - - if (funnelRes[0]?.level !== payload.events.length) { - funnelRes.unshift({ - level: payload.events.length, - count: 0, - }); - } - - const filledFunnelRes = funnelRes.reduce( - (acc, item, index) => { - const diff = - index !== 0 ? (acc[acc.length - 1]?.level ?? 0) - item.level : 1; - - if (diff > 1) { - acc.push( - ...reverse( - repeat({}, diff - 1).map((_, index) => ({ - count: acc[acc.length - 1]?.count ?? 0, - level: item.level + index + 1, - })), - ), - ); - } - - return [ - ...acc, - { - count: item.count + (acc[acc.length - 1]?.count ?? 0), - level: item.level, - }, - ]; - }, - [] as typeof funnelRes, - ); + const funnel = await chQuery<{ level: number; count: number }>(sql); + const maxLevel = payload.events.length; + const filledFunnelRes = fillFunnel(funnel, maxLevel); const totalSessions = last(filledFunnelRes)?.count ?? 0; const steps = reverse(filledFunnelRes).reduce(