diff --git a/apps/start/src/components/charts/chart-tooltip.tsx b/apps/start/src/components/charts/chart-tooltip.tsx index 70e0a3c7..b9e12da5 100644 --- a/apps/start/src/components/charts/chart-tooltip.tsx +++ b/apps/start/src/components/charts/chart-tooltip.tsx @@ -2,6 +2,34 @@ import { createContext, useContext as useBaseContext } from 'react'; import { Tooltip as RechartsTooltip, type TooltipProps } from 'recharts'; +export const ChartTooltipContainer = ({ + children, +}: { children: React.ReactNode }) => { + return ( +
+ {children} +
+ ); +}; + +export const ChartTooltipHeader = ({ + children, +}: { children: React.ReactNode }) => { + return
{children}
; +}; + +export const ChartTooltipItem = ({ + children, + color, +}: { children: React.ReactNode; color: string }) => { + return ( +
+
+
{children}
+
+ ); +}; + export function createChartTooltip< PropsFromTooltip, PropsFromContext extends Record, @@ -31,9 +59,9 @@ export function createChartTooltip< } return ( -
+ -
+ ); }; diff --git a/apps/start/src/components/overview/overview-metric-card.tsx b/apps/start/src/components/overview/overview-metric-card.tsx index 99b43a31..e2d31fba 100644 --- a/apps/start/src/components/overview/overview-metric-card.tsx +++ b/apps/start/src/components/overview/overview-metric-card.tsx @@ -1,10 +1,17 @@ import { fancyMinutes, useNumber } from '@/hooks/use-numer-formatter'; import { cn } from '@/utils/cn'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { Area, AreaChart } from 'recharts'; +import { Area, AreaChart, Tooltip } from 'recharts'; import { formatDate, timeAgo } from '@/utils/date'; +import { getChartColor } from '@/utils/theme'; import { getPreviousMetric } from '@openpanel/common'; +import { useState } from 'react'; +import { + ChartTooltipContainer, + ChartTooltipHeader, + ChartTooltipItem, +} from '../charts/chart-tooltip'; import { PreviousDiffIndicatorPure, getDiffIndicator, @@ -41,6 +48,7 @@ export function OverviewMetricCard({ inverted = false, isLoading = false, }: MetricCardProps) { + const [value, setValue] = useState(metric.current); const number = useNumber(); const { current, previous } = metric; @@ -79,7 +87,7 @@ export function OverviewMetricCard({ {label}:{' '} - {renderValue(current, 'ml-1 font-light text-xl', false)} + {renderValue(value, 'ml-1 font-light text-xl', false)} } @@ -97,7 +105,7 @@ export function OverviewMetricCard({
@@ -107,6 +115,11 @@ export function OverviewMetricCard({ height={height / 4} data={data} style={{ marginTop: (height / 4) * 3 }} + onMouseMove={(event) => { + setValue( + event.activePayload?.[0]?.payload?.current ?? current, + ); + }} > + null} /> (isEditMode ? data.series : data.series.slice(0, limit || 10)), [data, isEditMode, limit], ); - const maxCount = Math.max(...series.map((serie) => serie.metrics[metric])); + const maxCount = Math.max( + ...series.map((serie) => serie.metrics[metric] ?? 0), + ); const tableColumns = [ { diff --git a/apps/start/src/components/report-chart/common/report-chart-tooltip.tsx b/apps/start/src/components/report-chart/common/report-chart-tooltip.tsx index 513ecc3f..97f19eaf 100644 --- a/apps/start/src/components/report-chart/common/report-chart-tooltip.tsx +++ b/apps/start/src/components/report-chart/common/report-chart-tooltip.tsx @@ -3,7 +3,11 @@ import { useNumber } from '@/hooks/use-numer-formatter'; import type { IRechartPayloadItem } from '@/hooks/use-rechart-data-model'; import React from 'react'; -import { createChartTooltip } from '@/components/charts/chart-tooltip'; +import { + ChartTooltipHeader, + ChartTooltipItem, + createChartTooltip, +} from '@/components/charts/chart-tooltip'; import type { RouterOutputs } from '@/trpc/client'; import type { IInterval } from '@openpanel/validation'; import { @@ -88,37 +92,31 @@ export const ReportChartTooltip = createChartTooltip( const hidden = sorted.slice(limit); return ( -
+ <> {visible.map((item, index) => ( {index === 0 && item.date && ( -
+
{formatDate(new Date(item.date))}
-
+ )} -
-
-
-
- - -
-
-
- {number.formatWithUnit(item.count, unit)} - {!!item.previous && ( - - ({number.formatWithUnit(item.previous.value, unit)}) - - )} -
- -
+ +
+ +
-
+
+
+ {number.formatWithUnit(item.count, unit)} + {!!item.previous && ( + + ({number.formatWithUnit(item.previous.value, unit)}) + + )} +
+ +
+ ))} {hidden.length > 0 && ( @@ -142,7 +140,7 @@ export const ReportChartTooltip = createChartTooltip( ))} )} -
+ ); }, ); diff --git a/apps/start/src/components/report-chart/map/chart.tsx b/apps/start/src/components/report-chart/map/chart.tsx index 45c0d81f..18d3932f 100644 --- a/apps/start/src/components/report-chart/map/chart.tsx +++ b/apps/start/src/components/report-chart/map/chart.tsx @@ -22,7 +22,7 @@ export function Chart({ data }: Props) { () => series.map((s) => ({ country: s.names[0]?.toLowerCase() ?? '', - value: s.metrics[metric], + value: s.metrics[metric] ?? 0, })), [series, metric], ); diff --git a/apps/start/src/components/report-chart/metric/chart.tsx b/apps/start/src/components/report-chart/metric/chart.tsx index 9079c637..823210c6 100644 --- a/apps/start/src/components/report-chart/metric/chart.tsx +++ b/apps/start/src/components/report-chart/metric/chart.tsx @@ -12,7 +12,7 @@ interface Props { export function Chart({ data }: Props) { const { isEditMode, - report: { metric, unit }, + report: { unit }, } = useReportChartContext(); const { series } = useVisibleSeries(data, isEditMode ? 20 : 4); return ( @@ -27,7 +27,7 @@ export function Chart({ data }: Props) { ); diff --git a/apps/start/src/components/report-chart/metric/metric-card.tsx b/apps/start/src/components/report-chart/metric/metric-card.tsx index 6a6dc94d..0fffd992 100644 --- a/apps/start/src/components/report-chart/metric/metric-card.tsx +++ b/apps/start/src/components/report-chart/metric/metric-card.tsx @@ -2,10 +2,17 @@ import { fancyMinutes, useNumber } from '@/hooks/use-numer-formatter'; import type { IChartData } from '@/trpc/client'; import { cn } from '@/utils/cn'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { Area, AreaChart } from 'recharts'; +import { Area, AreaChart, Tooltip } from 'recharts'; import type { IChartMetric } from '@openpanel/validation'; +import { + ChartTooltipContainer, + ChartTooltipHeader, + ChartTooltipItem, +} from '@/components/charts/chart-tooltip'; +import { formatDate } from '@/utils/date'; +import { getChartColor } from '@/utils/theme'; import { PreviousDiffIndicator, getDiffIndicator, @@ -20,6 +27,27 @@ interface MetricCardProps { unit?: string; } +const TooltipContent = (props: { payload?: any[] }) => { + const number = useNumber(); + return ( + + {props.payload?.map((item) => { + const { date, count } = item.payload; + return ( +
+ +
{formatDate(new Date(date))}
+
+ +
{number.format(count)}
+
+
+ ); + })} +
+ ); +}; + export function MetricCard({ serie, color: _color, @@ -32,7 +60,11 @@ export function MetricCard({ } = useReportChartContext(); const number = useNumber(); - const renderValue = (value: number, unitClassName?: string) => { + const renderValue = (value: number | undefined, unitClassName?: string) => { + if (!value) { + return
N/A
; + } + if (unit === 'min') { return <>{fancyMinutes(value)}; } @@ -62,7 +94,7 @@ export function MetricCard({ >
@@ -89,6 +121,7 @@ export function MetricCard({ /> + { const number = useNumber(); return ( -
+ {props.payload?.map((serie, index) => { const item = serie.payload; return ( {index === 0 && item.date && ( -
+
{formatDate(new Date(item.date))}
-
+ )} -
-
-
-
- - -
-
-
- {number.formatWithUnit(item.count)} - {!!item.previous && ( - - ({number.formatWithUnit(item.previous.sum.value)}) - - )} -
- -
+ +
+ +
-
+
+
+ {number.formatWithUnit(item.count)} + {!!item.previous && ( + + ({number.formatWithUnit(item.previous.sum.value)}) + + )} +
+ +
+ ); })} -
+ ); }; diff --git a/packages/common/src/group-by-labels.ts b/packages/common/src/group-by-labels.ts index 32da93be..d8dadbb1 100644 --- a/packages/common/src/group-by-labels.ts +++ b/packages/common/src/group-by-labels.ts @@ -4,12 +4,14 @@ export interface ISerieDataItem { label_2?: string | null | undefined; label_3?: string | null | undefined; count: number; + total_count?: number; date: string; } interface GroupedDataPoint { date: string; count: number; + total_count?: number; } interface GroupedResult { @@ -45,6 +47,7 @@ export function groupByLabels(data: ISerieDataItem[]): GroupedResult[] { group.data.push({ date: row.date, count: row.count, + total_count: row.total_count, }); }); @@ -63,7 +66,7 @@ export function groupByLabels(data: ISerieDataItem[]): GroupedResult[] { // This will ensure that all dates are present in the data array data: Array.from(timestamps).map((date) => { const dataPoint = group.data.find((dp) => dp.date === date); - return dataPoint || { date, count: 0 }; + return dataPoint || { date, count: 0, total_count: 0 }; }), }; }); diff --git a/packages/constants/index.ts b/packages/constants/index.ts index 998cb3a3..6d331626 100644 --- a/packages/constants/index.ts +++ b/packages/constants/index.ts @@ -180,6 +180,7 @@ export const deprecated_timeRanges = { }; export const metrics = { + count: 'count', sum: 'sum', average: 'average', min: 'min', diff --git a/packages/db/src/services/chart.service.ts b/packages/db/src/services/chart.service.ts index 9deca5d3..39b98dbb 100644 --- a/packages/db/src/services/chart.service.ts +++ b/packages/db/src/services/chart.service.ts @@ -62,6 +62,7 @@ export function getChartSql({ projectId, limit, timezone, + chartType, }: IGetChartDataInput & { timezone: string }) { const { sb, @@ -209,6 +210,17 @@ export function getChartSql({ return sql; } + // Add total unique count for user segment using a scalar subquery + if (event.segment === 'user') { + const totalUniqueSubquery = `( + SELECT ${sb.select.count} + FROM ${sb.from} + ${getJoins()} + ${getWhere()} + )`; + sb.select.total_unique_count = `${totalUniqueSubquery} as total_count`; + } + const sql = `${getSelect()} ${getFrom()} ${getJoins()} ${getWhere()} ${getGroupBy()} ${getOrderBy()} ${getFill()}`; console.log('-- Report --'); console.log(sql.replaceAll(/[\n\r]/g, ' ')); diff --git a/packages/db/src/services/overview.service.ts b/packages/db/src/services/overview.service.ts index 38ef8cc7..ef43378c 100644 --- a/packages/db/src/services/overview.service.ts +++ b/packages/db/src/services/overview.service.ts @@ -83,62 +83,12 @@ export type IGetTopGenericInput = z.infer & { }; export class OverviewService { - private pendingQueries: Map> = new Map(); - constructor(private client: typeof ch) {} isPageFilter(filters: IChartEventFilter[]) { return filters.some((filter) => filter.name === 'path' && filter.value); } - getTotalSessions({ - projectId, - startDate, - endDate, - filters, - timezone, - }: { - projectId: string; - startDate: string; - endDate: string; - filters: IChartEventFilter[]; - timezone: string; - }) { - const where = this.getRawWhereClause('sessions', filters); - const key = `total_sessions_${projectId}_${startDate}_${endDate}_${JSON.stringify(filters)}`; - - // Check if there's already a pending query for this key - const pendingQuery = this.pendingQueries.get(key); - if (pendingQuery) { - return pendingQuery.then((res) => res ?? 0); - } - - // Create new query promise and store it - const queryPromise = getCache(key, 15, async () => { - try { - const result = await clix(this.client, timezone) - .select<{ - total_sessions: number; - }>(['sum(sign) as total_sessions']) - .from(TABLE_NAMES.sessions, true) - .where('project_id', '=', projectId) - .where('created_at', 'BETWEEN', [ - clix.datetime(startDate, 'toDateTime'), - clix.datetime(endDate, 'toDateTime'), - ]) - .rawWhere(where) - .having('sum(sign)', '>', 0) - .execute(); - return result?.[0]?.total_sessions ?? 0; - } catch (error) { - return 0; - } - }); - - this.pendingQueries.set(key, queryPromise); - return queryPromise; - } - getMetrics({ projectId, filters, @@ -483,14 +433,6 @@ export class OverviewService { .orderBy('sessions', 'DESC') .limit(limit); - const totalSessions = await this.getTotalSessions({ - projectId, - startDate, - endDate, - filters, - timezone, - }); - return mainQuery.execute(); } @@ -556,14 +498,6 @@ export class OverviewService { ); } - const totalSessions = await this.getTotalSessions({ - projectId, - startDate, - endDate, - filters, - timezone, - }); - return mainQuery.execute(); } @@ -666,16 +600,7 @@ export class OverviewService { mainQuery.rawWhere(this.getRawWhereClause('sessions', filters)); } - const [res, totalSessions] = await Promise.all([ - mainQuery.execute(), - this.getTotalSessions({ - projectId, - startDate, - endDate, - filters, - timezone, - }), - ]); + const res = await mainQuery.execute(); return res; } diff --git a/packages/trpc/src/routers/chart.helpers.ts b/packages/trpc/src/routers/chart.helpers.ts index 09a40f83..788f6e13 100644 --- a/packages/trpc/src/routers/chart.helpers.ts +++ b/packages/trpc/src/routers/chart.helpers.ts @@ -4,7 +4,6 @@ import sqlstring from 'sqlstring'; import type { ISerieDataItem } from '@openpanel/common'; import { - DateTime, average, getPreviousMetric, groupByLabels, @@ -226,39 +225,32 @@ export async function getChartSerie( payload: IGetChartDataInput, timezone: string, ) { - async function getSeries() { - const result = await chQuery( - getChartSql({ ...payload, timezone }), + let result = await chQuery( + getChartSql({ ...payload, timezone }), + { + session_timezone: timezone, + }, + ); + + if (result.length === 0 && payload.breakdowns.length > 0) { + result = await chQuery( + getChartSql({ + ...payload, + breakdowns: [], + timezone, + }), { session_timezone: timezone, }, ); - - if (result.length === 0 && payload.breakdowns.length > 0) { - return await chQuery( - getChartSql({ - ...payload, - breakdowns: [], - timezone, - }), - { - session_timezone: timezone, - }, - ); - } - return result; } - return getSeries() - .then(groupByLabels) - .then((series) => { - return series.map((serie) => { - return { - ...serie, - event: payload.event, - }; - }); - }); + return groupByLabels(result).map((serie) => { + return { + ...serie, + event: payload.event, + }; + }); } export type IGetChartSerie = Awaited>[number]; @@ -339,6 +331,7 @@ export async function getChart(input: IChartInput) { average: round(average(serie.data.map((item) => item.count)), 2), min: min(serie.data.map((item) => item.count)), max: max(serie.data.map((item) => item.count)), + count: serie.data[0]?.total_count, // We can grab any since all are the same }; const event = { id: serie.event.id, @@ -388,6 +381,10 @@ export async function getChart(input: IChartInput) { ? max(previousSerie?.data.map((item) => item.count)) : null, ), + count: getPreviousMetric( + metrics.count ?? 0, + previousSerie?.data[0]?.total_count ?? null, + ), }, } : {}), @@ -409,6 +406,7 @@ export async function getChart(input: IChartInput) { average: 0, min: 0, max: 0, + count: undefined, }, }; @@ -420,7 +418,7 @@ export async function getChart(input: IChartInput) { const sumB = b.data.reduce((acc, item) => acc + (item.count ?? 0), 0); return sumB - sumA; } - return b.metrics[input.metric] - a.metrics[input.metric]; + return (b.metrics[input.metric] ?? 0) - (a.metrics[input.metric] ?? 0); }) .slice(offset, limit ? offset + limit : series.length); @@ -456,6 +454,7 @@ export async function getChart(input: IChartInput) { final.metrics.max, max(final.series.map((item) => item.metrics.previous?.max?.value ?? 0)), ), + count: undefined, }; } diff --git a/packages/trpc/src/routers/report.ts b/packages/trpc/src/routers/report.ts index 723803ea..d4ae4cef 100644 --- a/packages/trpc/src/routers/report.ts +++ b/packages/trpc/src/routers/report.ts @@ -56,7 +56,7 @@ export const reportRouter = createTRPCRouter({ previous: report.previous ?? false, unit: report.unit, criteria: report.criteria, - metric: report.metric, + metric: report.metric === 'count' ? 'sum' : report.metric, funnelGroup: report.funnelGroup, funnelWindow: report.funnelWindow, }, @@ -101,7 +101,7 @@ export const reportRouter = createTRPCRouter({ previous: report.previous ?? false, unit: report.unit, criteria: report.criteria, - metric: report.metric, + metric: report.metric === 'count' ? 'sum' : report.metric, funnelGroup: report.funnelGroup, funnelWindow: report.funnelWindow, }, diff --git a/packages/validation/src/types.validation.ts b/packages/validation/src/types.validation.ts index 612a0742..2c348dca 100644 --- a/packages/validation/src/types.validation.ts +++ b/packages/validation/src/types.validation.ts @@ -61,11 +61,13 @@ export type Metrics = { average: number; min: number; max: number; + count: number | undefined; previous?: { sum: PreviousValue; average: PreviousValue; min: PreviousValue; max: PreviousValue; + count: PreviousValue; }; };