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; } 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 }: 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, 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} /> ))}
{activeMetric.title}
{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(max, item.total_revenue ?? 0), 0, ) * 2, ]} width={30} /> 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, }} /> {data.map((item, index) => { return ( ); })} 90 ? false : { stroke: getChartColor(0), fill: 'transparent', 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) => ( ))} ); }