fix: metric chart total count
This commit is contained in:
@@ -2,6 +2,34 @@ import { createContext, useContext as useBaseContext } from 'react';
|
|||||||
|
|
||||||
import { Tooltip as RechartsTooltip, type TooltipProps } from 'recharts';
|
import { Tooltip as RechartsTooltip, type TooltipProps } from 'recharts';
|
||||||
|
|
||||||
|
export const ChartTooltipContainer = ({
|
||||||
|
children,
|
||||||
|
}: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="min-w-[180px] col gap-2 rounded-xl border bg-background/80 p-3 shadow-xl backdrop-blur-sm">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChartTooltipHeader = ({
|
||||||
|
children,
|
||||||
|
}: { children: React.ReactNode }) => {
|
||||||
|
return <div className="flex justify-between gap-8">{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChartTooltipItem = ({
|
||||||
|
children,
|
||||||
|
color,
|
||||||
|
}: { children: React.ReactNode; color: string }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="w-[3px] rounded-full" style={{ background: color }} />
|
||||||
|
<div className="col flex-1 gap-1">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function createChartTooltip<
|
export function createChartTooltip<
|
||||||
PropsFromTooltip,
|
PropsFromTooltip,
|
||||||
PropsFromContext extends Record<string, unknown>,
|
PropsFromContext extends Record<string, unknown>,
|
||||||
@@ -31,9 +59,9 @@ export function createChartTooltip<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-[180px] flex-col gap-2 rounded-xl border bg-background/80 p-3 shadow-xl backdrop-blur-sm">
|
<ChartTooltipContainer>
|
||||||
<Tooltip data={data} context={context} {...tooltip} />
|
<Tooltip data={data} context={context} {...tooltip} />
|
||||||
</div>
|
</ChartTooltipContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { fancyMinutes, useNumber } from '@/hooks/use-numer-formatter';
|
import { fancyMinutes, useNumber } from '@/hooks/use-numer-formatter';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
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 { formatDate, timeAgo } from '@/utils/date';
|
||||||
|
import { getChartColor } from '@/utils/theme';
|
||||||
import { getPreviousMetric } from '@openpanel/common';
|
import { getPreviousMetric } from '@openpanel/common';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
ChartTooltipContainer,
|
||||||
|
ChartTooltipHeader,
|
||||||
|
ChartTooltipItem,
|
||||||
|
} from '../charts/chart-tooltip';
|
||||||
import {
|
import {
|
||||||
PreviousDiffIndicatorPure,
|
PreviousDiffIndicatorPure,
|
||||||
getDiffIndicator,
|
getDiffIndicator,
|
||||||
@@ -41,6 +48,7 @@ export function OverviewMetricCard({
|
|||||||
inverted = false,
|
inverted = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
}: MetricCardProps) {
|
}: MetricCardProps) {
|
||||||
|
const [value, setValue] = useState(metric.current);
|
||||||
const number = useNumber();
|
const number = useNumber();
|
||||||
const { current, previous } = metric;
|
const { current, previous } = metric;
|
||||||
|
|
||||||
@@ -79,7 +87,7 @@ export function OverviewMetricCard({
|
|||||||
<span>
|
<span>
|
||||||
{label}:{' '}
|
{label}:{' '}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{renderValue(current, 'ml-1 font-light text-xl', false)}
|
{renderValue(value, 'ml-1 font-light text-xl', false)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -97,7 +105,7 @@ export function OverviewMetricCard({
|
|||||||
<div className={cn('group relative p-4')}>
|
<div className={cn('group relative p-4')}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'pointer-events-none absolute -left-1 -right-1 bottom-0 top-0 z-0 opacity-50 transition-opacity duration-300 group-hover:opacity-100',
|
'absolute -left-1 -right-1 bottom-0 top-0 z-0 opacity-50 transition-opacity duration-300 group-hover:opacity-100',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
@@ -107,6 +115,11 @@ export function OverviewMetricCard({
|
|||||||
height={height / 4}
|
height={height / 4}
|
||||||
data={data}
|
data={data}
|
||||||
style={{ marginTop: (height / 4) * 3 }}
|
style={{ marginTop: (height / 4) * 3 }}
|
||||||
|
onMouseMove={(event) => {
|
||||||
|
setValue(
|
||||||
|
event.activePayload?.[0]?.payload?.current ?? current,
|
||||||
|
);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
@@ -128,6 +141,7 @@ export function OverviewMetricCard({
|
|||||||
/>
|
/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
<Tooltip content={() => null} />
|
||||||
<Area
|
<Area
|
||||||
dataKey={'current'}
|
dataKey={'current'}
|
||||||
type="step"
|
type="step"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { getChartColor } from '@/utils/theme';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { isSameDay, isSameHour, isSameMonth, isSameWeek } from 'date-fns';
|
import { isSameDay, isSameHour, isSameMonth, isSameWeek } from 'date-fns';
|
||||||
import { last } from 'ramda';
|
import { last } from 'ramda';
|
||||||
import React, { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
|
|
||||||
import { useDashedStroke } from '@/hooks/use-dashed-stroke';
|
import { useDashedStroke } from '@/hooks/use-dashed-stroke';
|
||||||
import { useXAxisProps, useYAxisProps } from '../common/axis';
|
import { useXAxisProps, useYAxisProps } from '../common/axis';
|
||||||
import { SolidToDashedGradient } from '../common/linear-gradient';
|
|
||||||
import { ReportChartTooltip } from '../common/report-chart-tooltip';
|
import { ReportChartTooltip } from '../common/report-chart-tooltip';
|
||||||
import { ReportTable } from '../common/report-table';
|
import { ReportTable } from '../common/report-table';
|
||||||
import { SerieIcon } from '../common/serie-icon';
|
import { SerieIcon } from '../common/serie-icon';
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ export function Chart({ data }: Props) {
|
|||||||
() => (isEditMode ? data.series : data.series.slice(0, limit || 10)),
|
() => (isEditMode ? data.series : data.series.slice(0, limit || 10)),
|
||||||
[data, isEditMode, limit],
|
[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 = [
|
const tableColumns = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { useNumber } from '@/hooks/use-numer-formatter';
|
|||||||
import type { IRechartPayloadItem } from '@/hooks/use-rechart-data-model';
|
import type { IRechartPayloadItem } from '@/hooks/use-rechart-data-model';
|
||||||
import React from 'react';
|
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 { RouterOutputs } from '@/trpc/client';
|
||||||
import type { IInterval } from '@openpanel/validation';
|
import type { IInterval } from '@openpanel/validation';
|
||||||
import {
|
import {
|
||||||
@@ -88,37 +92,31 @@ export const ReportChartTooltip = createChartTooltip<Data, Context>(
|
|||||||
const hidden = sorted.slice(limit);
|
const hidden = sorted.slice(limit);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-[180px] flex-col gap-2">
|
<>
|
||||||
{visible.map((item, index) => (
|
{visible.map((item, index) => (
|
||||||
<React.Fragment key={item.id}>
|
<React.Fragment key={item.id}>
|
||||||
{index === 0 && item.date && (
|
{index === 0 && item.date && (
|
||||||
<div className="flex justify-between gap-8">
|
<ChartTooltipHeader>
|
||||||
<div>{formatDate(new Date(item.date))}</div>
|
<div>{formatDate(new Date(item.date))}</div>
|
||||||
</div>
|
</ChartTooltipHeader>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-2">
|
<ChartTooltipItem color={item.color}>
|
||||||
<div
|
<div className="flex items-center gap-1">
|
||||||
className="w-[3px] rounded-full"
|
<SerieIcon name={item.names} />
|
||||||
style={{ background: item.color }}
|
<SerieName name={item.names} />
|
||||||
/>
|
|
||||||
<div className="col flex-1 gap-1">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<SerieIcon name={item.names} />
|
|
||||||
<SerieName name={item.names} />
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between gap-8 font-mono font-medium">
|
|
||||||
<div className="row gap-1">
|
|
||||||
{number.formatWithUnit(item.count, unit)}
|
|
||||||
{!!item.previous && (
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
({number.formatWithUnit(item.previous.value, unit)})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<PreviousDiffIndicator {...item.previous} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex justify-between gap-8 font-mono font-medium">
|
||||||
|
<div className="row gap-1">
|
||||||
|
{number.formatWithUnit(item.count, unit)}
|
||||||
|
{!!item.previous && (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
({number.formatWithUnit(item.previous.value, unit)})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<PreviousDiffIndicator {...item.previous} />
|
||||||
|
</div>
|
||||||
|
</ChartTooltipItem>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
{hidden.length > 0 && (
|
{hidden.length > 0 && (
|
||||||
@@ -142,7 +140,7 @@ export const ReportChartTooltip = createChartTooltip<Data, Context>(
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function Chart({ data }: Props) {
|
|||||||
() =>
|
() =>
|
||||||
series.map((s) => ({
|
series.map((s) => ({
|
||||||
country: s.names[0]?.toLowerCase() ?? '',
|
country: s.names[0]?.toLowerCase() ?? '',
|
||||||
value: s.metrics[metric],
|
value: s.metrics[metric] ?? 0,
|
||||||
})),
|
})),
|
||||||
[series, metric],
|
[series, metric],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface Props {
|
|||||||
export function Chart({ data }: Props) {
|
export function Chart({ data }: Props) {
|
||||||
const {
|
const {
|
||||||
isEditMode,
|
isEditMode,
|
||||||
report: { metric, unit },
|
report: { unit },
|
||||||
} = useReportChartContext();
|
} = useReportChartContext();
|
||||||
const { series } = useVisibleSeries(data, isEditMode ? 20 : 4);
|
const { series } = useVisibleSeries(data, isEditMode ? 20 : 4);
|
||||||
return (
|
return (
|
||||||
@@ -27,7 +27,7 @@ export function Chart({ data }: Props) {
|
|||||||
<MetricCard
|
<MetricCard
|
||||||
key={serie.id}
|
key={serie.id}
|
||||||
serie={serie}
|
serie={serie}
|
||||||
metric={metric}
|
metric={'count'}
|
||||||
unit={unit}
|
unit={unit}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,10 +2,17 @@ import { fancyMinutes, useNumber } from '@/hooks/use-numer-formatter';
|
|||||||
import type { IChartData } from '@/trpc/client';
|
import type { IChartData } from '@/trpc/client';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
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 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 {
|
import {
|
||||||
PreviousDiffIndicator,
|
PreviousDiffIndicator,
|
||||||
getDiffIndicator,
|
getDiffIndicator,
|
||||||
@@ -20,6 +27,27 @@ interface MetricCardProps {
|
|||||||
unit?: string;
|
unit?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TooltipContent = (props: { payload?: any[] }) => {
|
||||||
|
const number = useNumber();
|
||||||
|
return (
|
||||||
|
<ChartTooltipContainer>
|
||||||
|
{props.payload?.map((item) => {
|
||||||
|
const { date, count } = item.payload;
|
||||||
|
return (
|
||||||
|
<div key={item.id} className="col gap-2">
|
||||||
|
<ChartTooltipHeader>
|
||||||
|
<div>{formatDate(new Date(date))}</div>
|
||||||
|
</ChartTooltipHeader>
|
||||||
|
<ChartTooltipItem color={getChartColor(0)}>
|
||||||
|
<div>{number.format(count)}</div>
|
||||||
|
</ChartTooltipItem>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ChartTooltipContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function MetricCard({
|
export function MetricCard({
|
||||||
serie,
|
serie,
|
||||||
color: _color,
|
color: _color,
|
||||||
@@ -32,7 +60,11 @@ export function MetricCard({
|
|||||||
} = useReportChartContext();
|
} = useReportChartContext();
|
||||||
const number = useNumber();
|
const number = useNumber();
|
||||||
|
|
||||||
const renderValue = (value: number, unitClassName?: string) => {
|
const renderValue = (value: number | undefined, unitClassName?: string) => {
|
||||||
|
if (!value) {
|
||||||
|
return <div className="text-muted-foreground">N/A</div>;
|
||||||
|
}
|
||||||
|
|
||||||
if (unit === 'min') {
|
if (unit === 'min') {
|
||||||
return <>{fancyMinutes(value)}</>;
|
return <>{fancyMinutes(value)}</>;
|
||||||
}
|
}
|
||||||
@@ -62,7 +94,7 @@ export function MetricCard({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'pointer-events-none absolute -left-1 -right-1 bottom-0 top-0 z-0 opacity-50 transition-opacity duration-300 group-hover:opacity-100',
|
'absolute -left-1 -right-1 bottom-0 top-0 z-0 opacity-100 transition-opacity duration-300 group-hover:opacity-100',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
@@ -89,6 +121,7 @@ export function MetricCard({
|
|||||||
/>
|
/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
<Tooltip content={TooltipContent} />
|
||||||
<Area
|
<Area
|
||||||
dataKey="count"
|
dataKey="count"
|
||||||
type="step"
|
type="step"
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import { truncate } from '@/utils/truncate';
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
|
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChartTooltipContainer,
|
||||||
|
ChartTooltipHeader,
|
||||||
|
ChartTooltipItem,
|
||||||
|
} from '@/components/charts/chart-tooltip';
|
||||||
import { useNumber } from '@/hooks/use-numer-formatter';
|
import { useNumber } from '@/hooks/use-numer-formatter';
|
||||||
import { formatDate } from '@/utils/date';
|
import { formatDate } from '@/utils/date';
|
||||||
import { AXIS_FONT_PROPS } from '../common/axis';
|
import { AXIS_FONT_PROPS } from '../common/axis';
|
||||||
@@ -24,43 +29,37 @@ interface Props {
|
|||||||
const PieTooltip = (props: { payload?: any[] }) => {
|
const PieTooltip = (props: { payload?: any[] }) => {
|
||||||
const number = useNumber();
|
const number = useNumber();
|
||||||
return (
|
return (
|
||||||
<div className="bg-background/80 p-2 rounded-md backdrop-blur-md border min-w-[180px]">
|
<ChartTooltipContainer>
|
||||||
{props.payload?.map((serie, index) => {
|
{props.payload?.map((serie, index) => {
|
||||||
const item = serie.payload;
|
const item = serie.payload;
|
||||||
return (
|
return (
|
||||||
<Fragment key={item.id}>
|
<Fragment key={item.id}>
|
||||||
{index === 0 && item.date && (
|
{index === 0 && item.date && (
|
||||||
<div className="flex justify-between gap-8">
|
<ChartTooltipHeader>
|
||||||
<div>{formatDate(new Date(item.date))}</div>
|
<div>{formatDate(new Date(item.date))}</div>
|
||||||
</div>
|
</ChartTooltipHeader>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-2">
|
<ChartTooltipItem color={item.color}>
|
||||||
<div
|
<div className="flex items-center gap-1">
|
||||||
className="w-[3px] rounded-full"
|
<SerieIcon name={item.name} />
|
||||||
style={{ background: item.color }}
|
<SerieName name={item.names} className="font-medium" />
|
||||||
/>
|
|
||||||
<div className="col flex-1 gap-1">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<SerieIcon name={item.name} />
|
|
||||||
<SerieName name={item.names} className="font-medium" />
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between gap-8 font-mono font-medium">
|
|
||||||
<div className="row gap-1">
|
|
||||||
{number.formatWithUnit(item.count)}
|
|
||||||
{!!item.previous && (
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
({number.formatWithUnit(item.previous.sum.value)})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<PreviousDiffIndicator {...item.previous?.sum} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex justify-between gap-8 font-mono font-medium">
|
||||||
|
<div className="row gap-1">
|
||||||
|
{number.formatWithUnit(item.count)}
|
||||||
|
{!!item.previous && (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
({number.formatWithUnit(item.previous.sum.value)})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<PreviousDiffIndicator {...item.previous?.sum} />
|
||||||
|
</div>
|
||||||
|
</ChartTooltipItem>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</ChartTooltipContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ export interface ISerieDataItem {
|
|||||||
label_2?: string | null | undefined;
|
label_2?: string | null | undefined;
|
||||||
label_3?: string | null | undefined;
|
label_3?: string | null | undefined;
|
||||||
count: number;
|
count: number;
|
||||||
|
total_count?: number;
|
||||||
date: string;
|
date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GroupedDataPoint {
|
interface GroupedDataPoint {
|
||||||
date: string;
|
date: string;
|
||||||
count: number;
|
count: number;
|
||||||
|
total_count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GroupedResult {
|
interface GroupedResult {
|
||||||
@@ -45,6 +47,7 @@ export function groupByLabels(data: ISerieDataItem[]): GroupedResult[] {
|
|||||||
group.data.push({
|
group.data.push({
|
||||||
date: row.date,
|
date: row.date,
|
||||||
count: row.count,
|
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
|
// This will ensure that all dates are present in the data array
|
||||||
data: Array.from(timestamps).map((date) => {
|
data: Array.from(timestamps).map((date) => {
|
||||||
const dataPoint = group.data.find((dp) => dp.date === date);
|
const dataPoint = group.data.find((dp) => dp.date === date);
|
||||||
return dataPoint || { date, count: 0 };
|
return dataPoint || { date, count: 0, total_count: 0 };
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ export const deprecated_timeRanges = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const metrics = {
|
export const metrics = {
|
||||||
|
count: 'count',
|
||||||
sum: 'sum',
|
sum: 'sum',
|
||||||
average: 'average',
|
average: 'average',
|
||||||
min: 'min',
|
min: 'min',
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export function getChartSql({
|
|||||||
projectId,
|
projectId,
|
||||||
limit,
|
limit,
|
||||||
timezone,
|
timezone,
|
||||||
|
chartType,
|
||||||
}: IGetChartDataInput & { timezone: string }) {
|
}: IGetChartDataInput & { timezone: string }) {
|
||||||
const {
|
const {
|
||||||
sb,
|
sb,
|
||||||
@@ -209,6 +210,17 @@ export function getChartSql({
|
|||||||
return sql;
|
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()}`;
|
const sql = `${getSelect()} ${getFrom()} ${getJoins()} ${getWhere()} ${getGroupBy()} ${getOrderBy()} ${getFill()}`;
|
||||||
console.log('-- Report --');
|
console.log('-- Report --');
|
||||||
console.log(sql.replaceAll(/[\n\r]/g, ' '));
|
console.log(sql.replaceAll(/[\n\r]/g, ' '));
|
||||||
|
|||||||
@@ -83,62 +83,12 @@ export type IGetTopGenericInput = z.infer<typeof zGetTopGenericInput> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class OverviewService {
|
export class OverviewService {
|
||||||
private pendingQueries: Map<string, Promise<number | null>> = new Map();
|
|
||||||
|
|
||||||
constructor(private client: typeof ch) {}
|
constructor(private client: typeof ch) {}
|
||||||
|
|
||||||
isPageFilter(filters: IChartEventFilter[]) {
|
isPageFilter(filters: IChartEventFilter[]) {
|
||||||
return filters.some((filter) => filter.name === 'path' && filter.value);
|
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({
|
getMetrics({
|
||||||
projectId,
|
projectId,
|
||||||
filters,
|
filters,
|
||||||
@@ -483,14 +433,6 @@ export class OverviewService {
|
|||||||
.orderBy('sessions', 'DESC')
|
.orderBy('sessions', 'DESC')
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
const totalSessions = await this.getTotalSessions({
|
|
||||||
projectId,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
filters,
|
|
||||||
timezone,
|
|
||||||
});
|
|
||||||
|
|
||||||
return mainQuery.execute();
|
return mainQuery.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,14 +498,6 @@ export class OverviewService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalSessions = await this.getTotalSessions({
|
|
||||||
projectId,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
filters,
|
|
||||||
timezone,
|
|
||||||
});
|
|
||||||
|
|
||||||
return mainQuery.execute();
|
return mainQuery.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,16 +600,7 @@ export class OverviewService {
|
|||||||
mainQuery.rawWhere(this.getRawWhereClause('sessions', filters));
|
mainQuery.rawWhere(this.getRawWhereClause('sessions', filters));
|
||||||
}
|
}
|
||||||
|
|
||||||
const [res, totalSessions] = await Promise.all([
|
const res = await mainQuery.execute();
|
||||||
mainQuery.execute(),
|
|
||||||
this.getTotalSessions({
|
|
||||||
projectId,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
filters,
|
|
||||||
timezone,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import sqlstring from 'sqlstring';
|
|||||||
|
|
||||||
import type { ISerieDataItem } from '@openpanel/common';
|
import type { ISerieDataItem } from '@openpanel/common';
|
||||||
import {
|
import {
|
||||||
DateTime,
|
|
||||||
average,
|
average,
|
||||||
getPreviousMetric,
|
getPreviousMetric,
|
||||||
groupByLabels,
|
groupByLabels,
|
||||||
@@ -226,39 +225,32 @@ export async function getChartSerie(
|
|||||||
payload: IGetChartDataInput,
|
payload: IGetChartDataInput,
|
||||||
timezone: string,
|
timezone: string,
|
||||||
) {
|
) {
|
||||||
async function getSeries() {
|
let result = await chQuery<ISerieDataItem>(
|
||||||
const result = await chQuery<ISerieDataItem>(
|
getChartSql({ ...payload, timezone }),
|
||||||
getChartSql({ ...payload, timezone }),
|
{
|
||||||
|
session_timezone: timezone,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.length === 0 && payload.breakdowns.length > 0) {
|
||||||
|
result = await chQuery<ISerieDataItem>(
|
||||||
|
getChartSql({
|
||||||
|
...payload,
|
||||||
|
breakdowns: [],
|
||||||
|
timezone,
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
session_timezone: timezone,
|
session_timezone: timezone,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.length === 0 && payload.breakdowns.length > 0) {
|
|
||||||
return await chQuery<ISerieDataItem>(
|
|
||||||
getChartSql({
|
|
||||||
...payload,
|
|
||||||
breakdowns: [],
|
|
||||||
timezone,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
session_timezone: timezone,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return getSeries()
|
return groupByLabels(result).map((serie) => {
|
||||||
.then(groupByLabels)
|
return {
|
||||||
.then((series) => {
|
...serie,
|
||||||
return series.map((serie) => {
|
event: payload.event,
|
||||||
return {
|
};
|
||||||
...serie,
|
});
|
||||||
event: payload.event,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IGetChartSerie = Awaited<ReturnType<typeof getChartSeries>>[number];
|
export type IGetChartSerie = Awaited<ReturnType<typeof getChartSeries>>[number];
|
||||||
@@ -339,6 +331,7 @@ export async function getChart(input: IChartInput) {
|
|||||||
average: round(average(serie.data.map((item) => item.count)), 2),
|
average: round(average(serie.data.map((item) => item.count)), 2),
|
||||||
min: min(serie.data.map((item) => item.count)),
|
min: min(serie.data.map((item) => item.count)),
|
||||||
max: max(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 = {
|
const event = {
|
||||||
id: serie.event.id,
|
id: serie.event.id,
|
||||||
@@ -388,6 +381,10 @@ export async function getChart(input: IChartInput) {
|
|||||||
? max(previousSerie?.data.map((item) => item.count))
|
? max(previousSerie?.data.map((item) => item.count))
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
count: getPreviousMetric(
|
||||||
|
metrics.count ?? 0,
|
||||||
|
previousSerie?.data[0]?.total_count ?? null,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
@@ -409,6 +406,7 @@ export async function getChart(input: IChartInput) {
|
|||||||
average: 0,
|
average: 0,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 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);
|
const sumB = b.data.reduce((acc, item) => acc + (item.count ?? 0), 0);
|
||||||
return sumB - sumA;
|
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);
|
.slice(offset, limit ? offset + limit : series.length);
|
||||||
|
|
||||||
@@ -456,6 +454,7 @@ export async function getChart(input: IChartInput) {
|
|||||||
final.metrics.max,
|
final.metrics.max,
|
||||||
max(final.series.map((item) => item.metrics.previous?.max?.value ?? 0)),
|
max(final.series.map((item) => item.metrics.previous?.max?.value ?? 0)),
|
||||||
),
|
),
|
||||||
|
count: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const reportRouter = createTRPCRouter({
|
|||||||
previous: report.previous ?? false,
|
previous: report.previous ?? false,
|
||||||
unit: report.unit,
|
unit: report.unit,
|
||||||
criteria: report.criteria,
|
criteria: report.criteria,
|
||||||
metric: report.metric,
|
metric: report.metric === 'count' ? 'sum' : report.metric,
|
||||||
funnelGroup: report.funnelGroup,
|
funnelGroup: report.funnelGroup,
|
||||||
funnelWindow: report.funnelWindow,
|
funnelWindow: report.funnelWindow,
|
||||||
},
|
},
|
||||||
@@ -101,7 +101,7 @@ export const reportRouter = createTRPCRouter({
|
|||||||
previous: report.previous ?? false,
|
previous: report.previous ?? false,
|
||||||
unit: report.unit,
|
unit: report.unit,
|
||||||
criteria: report.criteria,
|
criteria: report.criteria,
|
||||||
metric: report.metric,
|
metric: report.metric === 'count' ? 'sum' : report.metric,
|
||||||
funnelGroup: report.funnelGroup,
|
funnelGroup: report.funnelGroup,
|
||||||
funnelWindow: report.funnelWindow,
|
funnelWindow: report.funnelWindow,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,11 +61,13 @@ export type Metrics = {
|
|||||||
average: number;
|
average: number;
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
|
count: number | undefined;
|
||||||
previous?: {
|
previous?: {
|
||||||
sum: PreviousValue;
|
sum: PreviousValue;
|
||||||
average: PreviousValue;
|
average: PreviousValue;
|
||||||
min: PreviousValue;
|
min: PreviousValue;
|
||||||
max: PreviousValue;
|
max: PreviousValue;
|
||||||
|
count: PreviousValue;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user