Updated Funnel report component.

This commit is contained in:
Mike Cao 2024-02-23 20:31:35 -08:00
parent 2832ff9622
commit f81f0839c6
7 changed files with 169 additions and 116 deletions

View File

@ -1,3 +1,84 @@
.loading {
height: 300px;
.chart {
display: grid;
}
.num {
display: flex;
align-items: center;
justify-content: center;
border-radius: 100%;
width: 50px;
height: 50px;
font-size: 16px;
font-weight: 700;
color: var(--base800);
background: var(--base100);
z-index: 1;
}
.step {
display: grid;
grid-template-columns: max-content 1fr;
column-gap: 30px;
position: relative;
padding-bottom: 60px;
}
.step::before {
content: '';
position: absolute;
top: 0;
left: 25px;
bottom: 0;
width: 2px;
background-color: var(--base100);
}
.card {
display: grid;
gap: 20px;
}
.header {
display: flex;
align-items: center;
font-weight: 700;
gap: 20px;
}
.bar {
display: flex;
align-items: center;
justify-content: end;
background: var(--base900);
height: 50px;
border-radius: 5px;
overflow: hidden;
position: relative;
}
.label {
color: var(--base700);
}
.value {
color: var(--base50);
margin-right: 20px;
}
.track {
background-color: var(--base100);
border-radius: 5px;
}
.info {
display: flex;
justify-content: space-between;
text-transform: lowercase;
}
.item {
padding: 6px 10px;
border-radius: 4px;
border: 1px solid var(--base300);
}

View File

@ -1,83 +1,57 @@
import { JSX, useCallback, useContext, useMemo } from 'react';
import { Loading, StatusLight } from 'react-basics';
import { useMessages, useTheme } from 'components/hooks';
import BarChart from 'components/metrics/BarChart';
import { formatLongNumber } from 'lib/format';
import { useContext } from 'react';
import classNames from 'classnames';
import { useMessages } from 'components/hooks';
import { ReportContext } from '../[reportId]/Report';
import styles from './FunnelChart.module.css';
import { formatLongNumber } from 'lib/format';
export interface FunnelChartProps {
className?: string;
isLoading?: boolean;
}
export function FunnelChart({ className, isLoading }: FunnelChartProps) {
export function FunnelChart({ className }: FunnelChartProps) {
const { report } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
const { colors } = useTheme();
const { parameters, data } = report || {};
const renderXLabel = useCallback(
(label: string, index: number) => {
return parameters.urls[index];
},
[parameters],
);
const renderTooltipPopup = useCallback(
(
setTooltipPopup: (arg0: JSX.Element) => void,
model: { tooltip: { opacity: any; labelColors: any; dataPoints: any } },
) => {
const { opacity, labelColors, dataPoints } = model.tooltip;
if (!dataPoints?.length || !opacity) {
setTooltipPopup(null);
return;
}
setTooltipPopup(
<>
<div>
{formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)}
</div>
<div>
<StatusLight color={labelColors?.[0]?.backgroundColor}>
{formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)}
</StatusLight>
</div>
</>,
);
},
[],
);
const datasets = useMemo(() => {
return [
{
label: formatMessage(labels.uniqueVisitors),
data: data,
borderWidth: 1,
...colors.chart.visitors,
},
];
}, [data, colors, formatMessage, labels]);
if (isLoading) {
return <Loading icon="dots" className={styles.loading} />;
}
const { data } = report || {};
return (
<BarChart
className={className}
datasets={datasets}
unit="day"
isLoading={isLoading}
renderXLabel={renderXLabel}
renderTooltipPopup={renderTooltipPopup}
XAxisType="category"
/>
<div className={classNames(styles.chart, className)}>
{data?.map(({ url, visitors, dropped, dropoff, remaining }, index: number) => {
return (
<div key={url} className={styles.step}>
<div className={styles.num}>{index + 1}</div>
<div className={styles.card}>
<div className={styles.header}>
<span className={styles.label}>{formatMessage(labels.viewedPage)}:</span>
<span className={styles.item}>{url}</span>
</div>
<div className={styles.track}>
<div className={styles.bar} style={{ width: `${remaining * 100}%` }}>
<span className={styles.value}>
{remaining > 0.1 && `${(remaining * 100).toFixed(2)}%`}
</span>
</div>
</div>
<div className={styles.info}>
<div>
<b>{formatLongNumber(visitors)}</b>
<span> {formatMessage(labels.visitors)}</span>
<span> ({(remaining * 100).toFixed(2)}%)</span>
</div>
{dropoff > 0 && (
<div>
<b>{formatLongNumber(dropped)}</b> {formatMessage(labels.visitorsDroppedOff)} (
{(dropoff * 100).toFixed(2)}%)
</div>
)}
</div>
</div>
</div>
);
})}
</div>
);
}

View File

@ -1,5 +1,4 @@
import FunnelChart from './FunnelChart';
import FunnelTable from './FunnelTable';
import FunnelParameters from './FunnelParameters';
import Report from '../[reportId]/Report';
import ReportHeader from '../[reportId]/ReportHeader';
@ -22,7 +21,6 @@ export default function FunnelReport({ reportId }: { reportId?: string }) {
</ReportMenu>
<ReportBody>
<FunnelChart />
<FunnelTable />
</ReportBody>
</Report>
);

View File

@ -1,19 +0,0 @@
import { useContext } from 'react';
import ListTable from 'components/metrics/ListTable';
import { useMessages } from 'components/hooks';
import { ReportContext } from '../[reportId]/Report';
export function FunnelTable() {
const { report } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
return (
<ListTable
data={report?.data}
title={formatMessage(labels.url)}
metric={formatMessage(labels.visitors)}
showPercentage={true}
/>
);
}
export default FunnelTable;

View File

@ -209,6 +209,18 @@ export const labels = defineMessages({
select: { id: 'label.select', defaultMessage: 'Select' },
myAccount: { id: 'label.my-account', defaultMessage: 'My account' },
transfer: { id: 'label.transfer', defaultMessage: 'Transfer' },
viewedPage: {
id: 'message.viewed-page',
defaultMessage: 'Viewed page',
},
triggeredEvent: {
id: 'message.triggered-event',
defaultMessage: 'Triggered event',
},
visitorsDroppedOff: {
id: 'message.visitors-dropped-off',
defaultMessage: 'Visitors droppped off',
},
});
export const messages = defineMessages({

View File

@ -2,6 +2,25 @@ import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma';
const formatResults = (urls: string[]) => (results: unknown) => {
return urls.map((url: string, i: number) => {
const visitors = Number(results[i]?.count) || 0;
const previous = Number(results[i - 1]?.count) || 0;
const dropped = previous > 0 ? previous - visitors : 0;
const dropoff = 1 - visitors / previous;
const remaining = visitors / Number(results[0].count);
return {
url,
visitors,
previous,
dropped,
dropoff,
remaining,
};
});
};
export async function getFunnel(
...args: [
websiteId: string,
@ -29,9 +48,9 @@ async function relationalQuery(
},
): Promise<
{
x: string;
y: number;
z: number;
url: string;
visitors: number;
dropoff: number;
}[]
> {
const { windowMinutes, startDate, endDate, urls } = criteria;
@ -98,13 +117,7 @@ async function relationalQuery(
endDate,
...urls,
},
).then(results => {
return urls.map((a, i) => ({
x: a,
y: results[i]?.count || 0,
z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off
}));
});
).then(formatResults(urls));
}
async function clickhouseQuery(
@ -117,9 +130,9 @@ async function clickhouseQuery(
},
): Promise<
{
x: string;
y: number;
z: number;
url: string;
visitors: number;
dropoff: number;
}[]
> {
const { windowMinutes, startDate, endDate, urls } = criteria;
@ -198,11 +211,5 @@ async function clickhouseQuery(
endDate,
...urlParams,
},
).then(results => {
return urls.map((a, i) => ({
x: a,
y: Number(results[i]?.count) || 0,
z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off
}));
});
).then(formatResults(urls));
}

View File

@ -20,27 +20,27 @@ const initialState = {
const store = create(() => ({ ...initialState }));
export function setTheme(theme) {
export function setTheme(theme: string) {
store.setState({ theme });
}
export function setLocale(locale) {
export function setLocale(locale: string) {
store.setState({ locale });
}
export function setShareToken(shareToken) {
export function setShareToken(shareToken: string) {
store.setState({ shareToken });
}
export function setUser(user) {
export function setUser(user: object) {
store.setState({ user });
}
export function setConfig(config) {
export function setConfig(config: object) {
store.setState({ config });
}
export function setDateRange(dateRange) {
export function setDateRange(dateRange: object) {
store.setState({ dateRange });
}