import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
import { useEventQueryFilters } from '@/hooks/use-event-query-filters';
import { cn } from '@/utils/cn';
import { useDashedStroke } from '@/hooks/use-dashed-stroke';
import { useFormatDateInterval } from '@/hooks/use-format-date-interval';
import { useNumber } from '@/hooks/use-numer-formatter';
import { useTRPC } from '@/integrations/trpc/react';
import type { RouterOutputs } from '@/trpc/client';
import { getChartColor } from '@/utils/theme';
import { getPreviousMetric } from '@openpanel/common';
import type { IInterval } from '@openpanel/validation';
import { useQuery } from '@tanstack/react-query';
import { isSameDay, isSameHour, isSameMonth, isSameWeek } from 'date-fns';
import { last } from 'ramda';
import React, { useState } from 'react';
import {
Area,
Bar,
CartesianGrid,
Cell,
ComposedChart,
Customized,
Line,
ReferenceLine,
ResponsiveContainer,
XAxis,
YAxis,
} from 'recharts';
import { createChartTooltip } from '../charts/chart-tooltip';
import { useXAxisProps, useYAxisProps } from '../report-chart/common/axis';
import { PreviousDiffIndicatorPure } from '../report-chart/common/previous-diff-indicator';
import { Skeleton } from '../skeleton';
import { OverviewLiveHistogram } from './overview-live-histogram';
import { OverviewMetricCard } from './overview-metric-card';
interface OverviewMetricsProps {
projectId: string;
shareId?: string;
}
const TITLES = [
{
title: 'Unique Visitors',
key: 'unique_visitors',
unit: '',
inverted: false,
},
{
title: 'Sessions',
key: 'total_sessions',
unit: '',
inverted: false,
},
{
title: 'Pageviews',
key: 'total_screen_views',
unit: '',
inverted: false,
},
{
title: 'Pages per session',
key: 'views_per_session',
unit: '',
inverted: false,
},
{
title: 'Bounce Rate',
key: 'bounce_rate',
unit: '%',
inverted: true,
},
{
title: 'Session Duration',
key: 'avg_session_duration',
unit: 'min',
inverted: false,
},
{
title: 'Revenue',
key: 'total_revenue',
unit: 'currency',
inverted: false,
},
] as const;
export default function OverviewMetrics({
projectId,
shareId,
}: OverviewMetricsProps) {
const { range, interval, metric, setMetric, startDate, endDate } =
useOverviewOptions();
const [filters] = useEventQueryFilters();
const trpc = useTRPC();
const activeMetric = TITLES[metric]!;
const overviewQuery = useQuery(
trpc.overview.stats.queryOptions({
projectId,
shareId,
range,
interval,
filters,
startDate,
endDate,
}),
);
const data =
overviewQuery.data?.series?.map((item) => ({
...item,
timestamp: new Date(item.date).getTime(),
})) || [];
return (
<>
{TITLES.map((title, index) => (
setMetric(index)}
label={title.title}
metric={{
current: overviewQuery.data?.metrics[title.key] ?? 0,
previous: overviewQuery.data?.metrics[`prev_${title.key}`],
}}
unit={title.unit}
data={data.map((item) => ({
date: item.date,
current: item[title.key],
previous: item[`prev_${title.key}`],
}))}
active={metric === index}
isLoading={overviewQuery.isLoading}
inverted={title.inverted}
/>
))}
{overviewQuery.isLoading && }
>
);
}
const { Tooltip, TooltipProvider } = createChartTooltip<
RouterOutputs['overview']['stats']['series'][number],
{
anyMetric?: boolean;
metric: (typeof TITLES)[number];
interval: IInterval;
}
>(({ context: { metric, interval, anyMetric }, data: dataArray }) => {
const data = dataArray[0];
const formatDate = useFormatDateInterval({
interval,
short: false,
});
const number = useNumber();
if (!data) {
return null;
}
const revenue = data.total_revenue ?? 0;
const prevRevenue = data.prev_total_revenue ?? 0;
return (
<>
{formatDate(new Date(data.date))}
{metric.title}
{metric.unit === 'currency'
? number.currency((data[metric.key] ?? 0) / 100)
: number.formatWithUnit(data[metric.key], metric.unit)}
{!!data[`prev_${metric.key}`] && (
(
{metric.unit === 'currency'
? number.currency((data[`prev_${metric.key}`] ?? 0) / 100)
: number.formatWithUnit(
data[`prev_${metric.key}`],
metric.unit,
)}
)
)}
{anyMetric && revenue > 0 && (
Revenue
{number.currency(revenue / 100)}
{prevRevenue > 0 && (
({number.currency(prevRevenue / 100)})
)}
{prevRevenue > 0 && (
)}
)}
>
);
});
function Chart({
activeMetric,
interval,
data,
projectId,
}: {
activeMetric: (typeof TITLES)[number];
interval: IInterval;
data: RouterOutputs['overview']['stats']['series'];
projectId: string;
}) {
const xAxisProps = useXAxisProps({ interval });
const yAxisProps = useYAxisProps();
const number = useNumber();
const revenueYAxisProps = useYAxisProps({
tickFormatter: (value) => number.short(value / 100),
});
const [activeBar, setActiveBar] = useState(-1);
const { range, startDate, endDate } = useOverviewOptions();
const trpc = useTRPC();
const references = useQuery(
trpc.reference.getChartReferences.queryOptions(
{
projectId,
startDate,
endDate,
range,
},
{
staleTime: 1000 * 60 * 10,
},
),
);
// Line chart specific logic
let dotIndex = undefined;
if (interval === 'hour') {
// Find closest index based on times
dotIndex = data.findIndex((item) => {
return isSameHour(item.date, new Date());
});
}
const { calcStrokeDasharray, handleAnimationEnd, getStrokeDasharray } =
useDashedStroke({
dotIndex,
});
const lastSerieDataItem = last(data)?.date || new Date();
const useDashedLastLine = (() => {
if (range === 'today') {
return true;
}
if (interval === 'hour') {
return isSameHour(lastSerieDataItem, new Date());
}
if (interval === 'day') {
return isSameDay(lastSerieDataItem, new Date());
}
if (interval === 'month') {
return isSameMonth(lastSerieDataItem, new Date());
}
if (interval === 'week') {
return isSameWeek(lastSerieDataItem, new Date());
}
return false;
})();
if (activeMetric.key === 'total_revenue') {
return (
90
? false
: {
stroke: 'oklch(from var(--foreground) l c h / 0.1)',
fill: 'transparent',
strokeWidth: 1.5,
r: 2,
}
}
activeDot={{
stroke: 'oklch(from var(--foreground) l c h / 0.2)',
fill: 'transparent',
strokeWidth: 1.5,
r: 3,
}}
/>
90
? false
: {
stroke: '#3ba974',
fill: '#3ba974',
strokeWidth: 1.5,
r: 3,
}
}
activeDot={{
stroke: '#3ba974',
fill: 'var(--def-100)',
strokeWidth: 2,
r: 4,
}}
filter="url(#rainbow-line-glow)"
/>
{references.data?.map((ref) => (
))}
);
}
return (
{
setActiveBar(e.activeTooltipIndex ?? -1);
}}
>
Math.max(dataMax, 1),
]}
width={25}
/>
Math.max(max, item.total_revenue ?? 0),
0,
) * 1.2,
1,
),
]}
width={30}
allowDataOverflow={false}
/>
{data.map((item, index) => {
return (
|
);
})}
90
? false
: {
stroke: getChartColor(0),
fill: 'var(--def-100)',
fillOpacity: 1,
strokeWidth: 1.5,
r: 3,
}
}
activeDot={{
stroke: getChartColor(0),
fill: 'var(--def-100)',
strokeWidth: 2,
r: 4,
}}
filter="url(#rainbow-line-glow)"
/>
{references.data?.map((ref) => (
))}
);
}