diff --git a/apps/api/src/controllers/export.controller.ts b/apps/api/src/controllers/export.controller.ts
index bca5a35d..79fc3048 100644
--- a/apps/api/src/controllers/export.controller.ts
+++ b/apps/api/src/controllers/export.controller.ts
@@ -4,7 +4,7 @@ import { z } from 'zod';
import type { GetEventListOptions } from '@openpanel/db';
import { ClientType, db, getEventList, getEventsCount } from '@openpanel/db';
-import { getChart } from '@openpanel/trpc/src/routers/chart';
+import { getChart } from '@openpanel/trpc/src/routers/chart.helpers';
import { zChartInput } from '@openpanel/validation';
async function getProjectId(
diff --git a/apps/dashboard/src/components/overview/overview-top-devices.tsx b/apps/dashboard/src/components/overview/overview-top-devices.tsx
index 5ac4dee7..b771096b 100644
--- a/apps/dashboard/src/components/overview/overview-top-devices.tsx
+++ b/apps/dashboard/src/components/overview/overview-top-devices.tsx
@@ -30,6 +30,7 @@ export default function OverviewTopDevices({
title: 'Top devices',
btn: 'Devices',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -60,6 +61,7 @@ export default function OverviewTopDevices({
title: 'Top browser',
btn: 'Browser',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -90,6 +92,7 @@ export default function OverviewTopDevices({
title: 'Top Browser Version',
btn: 'Browser Version',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -120,6 +123,7 @@ export default function OverviewTopDevices({
title: 'Top OS',
btn: 'OS',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -150,6 +154,7 @@ export default function OverviewTopDevices({
title: 'Top OS version',
btn: 'OS Version',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
diff --git a/apps/dashboard/src/components/overview/overview-top-events/overview-top-events.tsx b/apps/dashboard/src/components/overview/overview-top-events/overview-top-events.tsx
index f5d1711f..63e7e3d1 100644
--- a/apps/dashboard/src/components/overview/overview-top-events/overview-top-events.tsx
+++ b/apps/dashboard/src/components/overview/overview-top-events/overview-top-events.tsx
@@ -31,6 +31,7 @@ export default function OverviewTopEvents({
title: 'Top events',
btn: 'Your',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -69,6 +70,7 @@ export default function OverviewTopEvents({
title: 'Top events',
btn: 'All',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -100,6 +102,7 @@ export default function OverviewTopEvents({
btn: 'Conversions',
hide: conversions.length === 0,
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
diff --git a/apps/dashboard/src/components/overview/overview-top-geo.tsx b/apps/dashboard/src/components/overview/overview-top-geo.tsx
index e0c82678..a811e5d8 100644
--- a/apps/dashboard/src/components/overview/overview-top-geo.tsx
+++ b/apps/dashboard/src/components/overview/overview-top-geo.tsx
@@ -29,6 +29,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
title: 'Top countries',
btn: 'Countries',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -59,6 +60,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
title: 'Top regions',
btn: 'Regions',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -89,6 +91,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
title: 'Top cities',
btn: 'Cities',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
diff --git a/apps/dashboard/src/components/overview/overview-top-pages.tsx b/apps/dashboard/src/components/overview/overview-top-pages.tsx
index 6e8226ae..532affad 100644
--- a/apps/dashboard/src/components/overview/overview-top-pages.tsx
+++ b/apps/dashboard/src/components/overview/overview-top-pages.tsx
@@ -28,6 +28,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
title: 'Top pages',
btn: 'Top pages',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -58,6 +59,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
title: 'Entry Pages',
btn: 'Entries',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -88,6 +90,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
title: 'Exit Pages',
btn: 'Exits',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
diff --git a/apps/dashboard/src/components/overview/overview-top-sources.tsx b/apps/dashboard/src/components/overview/overview-top-sources.tsx
index 03bc4cf2..ca03a5b4 100644
--- a/apps/dashboard/src/components/overview/overview-top-sources.tsx
+++ b/apps/dashboard/src/components/overview/overview-top-sources.tsx
@@ -32,6 +32,7 @@ export default function OverviewTopSources({
title: 'Top sources',
btn: 'All',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -62,6 +63,7 @@ export default function OverviewTopSources({
title: 'Top urls',
btn: 'URLs',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -92,6 +94,7 @@ export default function OverviewTopSources({
title: 'Top types',
btn: 'Types',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -122,6 +125,7 @@ export default function OverviewTopSources({
title: 'UTM Source',
btn: 'Source',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -152,6 +156,7 @@ export default function OverviewTopSources({
title: 'UTM Medium',
btn: 'Medium',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -182,6 +187,7 @@ export default function OverviewTopSources({
title: 'UTM Campaign',
btn: 'Campaign',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -212,6 +218,7 @@ export default function OverviewTopSources({
title: 'UTM Term',
btn: 'Term',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
@@ -242,6 +249,7 @@ export default function OverviewTopSources({
title: 'UTM Content',
btn: 'Content',
chart: {
+ limit: 10,
projectId,
startDate,
endDate,
diff --git a/apps/dashboard/src/components/report/chart/Chart.tsx b/apps/dashboard/src/components/report/chart/Chart.tsx
index 33af7a40..674dfa9d 100644
--- a/apps/dashboard/src/components/report/chart/Chart.tsx
+++ b/apps/dashboard/src/components/report/chart/Chart.tsx
@@ -20,16 +20,16 @@ export function Chart({
events,
breakdowns,
chartType,
- name,
range,
lineType,
previous,
formula,
- unit,
metric,
projectId,
startDate,
endDate,
+ limit,
+ offset,
}: ReportChartProps) {
const [references] = api.reference.getChartReferences.useSuspenseQuery(
{
@@ -56,6 +56,8 @@ export function Chart({
previous,
formula,
metric,
+ limit,
+ offset,
},
{
keepPreviousData: true,
diff --git a/apps/dashboard/src/components/report/chart/ChartProvider.tsx b/apps/dashboard/src/components/report/chart/ChartProvider.tsx
index 29ae49b8..4a30cca7 100644
--- a/apps/dashboard/src/components/report/chart/ChartProvider.tsx
+++ b/apps/dashboard/src/components/report/chart/ChartProvider.tsx
@@ -10,8 +10,7 @@ import {
useState,
} from 'react';
-import type { IChartSerie } from '@openpanel/trpc/src/routers/chart';
-import type { IChartProps } from '@openpanel/validation';
+import type { IChartProps, IChartSerie } from '@openpanel/validation';
import { ChartLoading } from './ChartLoading';
import { MetricCardLoading } from './MetricCard';
diff --git a/apps/dashboard/src/components/report/chart/MetricCard.tsx b/apps/dashboard/src/components/report/chart/MetricCard.tsx
index aab3e99a..dd05c9ca 100644
--- a/apps/dashboard/src/components/report/chart/MetricCard.tsx
+++ b/apps/dashboard/src/components/report/chart/MetricCard.tsx
@@ -44,7 +44,7 @@ export function MetricCard({
);
};
- const previous = serie.metrics.previous[metric];
+ const previous = serie.metrics.previous?.[metric];
const graphColors = getDiffIndicator(
previousIndicatorInverted,
@@ -93,7 +93,7 @@ export function MetricCard({
{serie.event.id}
- {serie.name || serie.event.displayName || serie.event.name}
+ {serie.name}
{/* */}
@@ -125,9 +125,9 @@ export function MetricCardEmpty() {
export function MetricCardLoading() {
return (
);
}
diff --git a/apps/dashboard/src/components/report/chart/ReportBarChart.tsx b/apps/dashboard/src/components/report/chart/ReportBarChart.tsx
index 306eaaa9..6ab26c90 100644
--- a/apps/dashboard/src/components/report/chart/ReportBarChart.tsx
+++ b/apps/dashboard/src/components/report/chart/ReportBarChart.tsx
@@ -41,7 +41,7 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
{...(isClickable ? { onClick: () => onClick(serie) } : {})}
>
- {serie.metrics.previous[metric]?.value}
+ {serie.metrics.previous?.[metric]?.value}
{number.format(
round((serie.metrics.sum / data.metrics.sum) * 100, 2)
diff --git a/apps/dashboard/src/components/report/chart/ReportChartTooltip.tsx b/apps/dashboard/src/components/report/chart/ReportChartTooltip.tsx
index d89b8fe5..1ccb3413 100644
--- a/apps/dashboard/src/components/report/chart/ReportChartTooltip.tsx
+++ b/apps/dashboard/src/components/report/chart/ReportChartTooltip.tsx
@@ -51,7 +51,7 @@ export function ReportChartTooltip({
) as IRechartPayloadItem;
return (
-
+
{index === 0 && data.date && (
{formatDate(new Date(data.date))}
@@ -64,7 +64,7 @@ export function ReportChartTooltip({
/>
- {getLabel(data.label)}
+ {getLabel(data.name)}
{number.formatWithUnit(data.count, unit)}
diff --git a/apps/dashboard/src/components/report/chart/ReportPieChart.tsx b/apps/dashboard/src/components/report/chart/ReportPieChart.tsx
index 3ba859a4..9487313c 100644
--- a/apps/dashboard/src/components/report/chart/ReportPieChart.tsx
+++ b/apps/dashboard/src/components/report/chart/ReportPieChart.tsx
@@ -24,7 +24,7 @@ export function ReportPieChart({ data }: ReportPieChartProps) {
id: serie.id,
color: getChartColor(serie.index),
index: serie.index,
- label: serie.name,
+ name: serie.name,
count: serie.metrics.sum,
percent: serie.metrics.sum / sum,
}));
@@ -88,7 +88,7 @@ const renderLabel = ({
innerRadius: number;
outerRadius: number;
fill: string;
- payload: { label: string; percent: number };
+ payload: { name: string; percent: number };
}) => {
const RADIAN = Math.PI / 180;
const radius = 25 + innerRadius + (outerRadius - innerRadius);
@@ -97,7 +97,7 @@ const renderLabel = ({
const yProcent = cy + radiusProcent * Math.sin(-midAngle * RADIAN);
const x = cx + radius * Math.cos(-midAngle * RADIAN);
const y = cy + radius * Math.sin(-midAngle * RADIAN);
- const label = payload.label;
+ const name = payload.name;
const percent = round(payload.percent * 100, 1);
return (
@@ -108,7 +108,7 @@ const renderLabel = ({
fill="white"
textAnchor="middle"
dominantBaseline="central"
- fontSize={10}
+ fontSize={12}
fontWeight={700}
pointerEvents={'none'}
>
@@ -120,9 +120,9 @@ const renderLabel = ({
fill={fill}
textAnchor={x > cx ? 'start' : 'end'}
dominantBaseline="central"
- fontSize={10}
+ fontSize={12}
>
- {truncate(label, 20)}
+ {truncate(name, 20)}
>
);
diff --git a/apps/dashboard/src/components/report/chart/ReportTable.tsx b/apps/dashboard/src/components/report/chart/ReportTable.tsx
index 64dca2dd..12dd8bd5 100644
--- a/apps/dashboard/src/components/report/chart/ReportTable.tsx
+++ b/apps/dashboard/src/components/report/chart/ReportTable.tsx
@@ -127,7 +127,7 @@ export function ReportTable({
{number.format(serie.metrics.sum)}
@@ -135,7 +135,7 @@ export function ReportTable({
{number.format(serie.metrics.average)}
diff --git a/apps/dashboard/src/components/report/reportSlice.ts b/apps/dashboard/src/components/report/reportSlice.ts
index 9eb6a1ed..979668ee 100644
--- a/apps/dashboard/src/components/report/reportSlice.ts
+++ b/apps/dashboard/src/components/report/reportSlice.ts
@@ -51,6 +51,7 @@ const initialState: InitialState = {
formula: undefined,
unit: undefined,
metric: 'sum',
+ limit: 500,
};
export const reportSlice = createSlice({
diff --git a/apps/dashboard/src/components/report/sidebar/ReportBreakdowns.tsx b/apps/dashboard/src/components/report/sidebar/ReportBreakdowns.tsx
index ac0a589e..3be40b1e 100644
--- a/apps/dashboard/src/components/report/sidebar/ReportBreakdowns.tsx
+++ b/apps/dashboard/src/components/report/sidebar/ReportBreakdowns.tsx
@@ -43,7 +43,7 @@ export function ReportBreakdowns() {
{selectedBreakdowns.map((item, index) => {
return (
-
+
{index}
{
- dispatch(
- addBreakdown({
- name: value,
- })
- );
- }}
- items={propertiesCombobox}
- placeholder="Select breakdown"
- />
- )}
+ {
+ dispatch(
+ addBreakdown({
+ name: value,
+ })
+ );
+ }}
+ items={propertiesCombobox}
+ placeholder="Select breakdown"
+ />
);
diff --git a/apps/dashboard/src/hooks/useRechartDataModel.ts b/apps/dashboard/src/hooks/useRechartDataModel.ts
index 567a4598..a8dabedc 100644
--- a/apps/dashboard/src/hooks/useRechartDataModel.ts
+++ b/apps/dashboard/src/hooks/useRechartDataModel.ts
@@ -1,10 +1,22 @@
'use client';
import { useMemo } from 'react';
-import type { IChartData, IChartSerieDataItem } from '@/trpc/client';
+import type { IChartData } from '@/trpc/client';
import { getChartColor } from '@/utils/theme';
-export type IRechartPayloadItem = IChartSerieDataItem & { color: string };
+export type IRechartPayloadItem = {
+ id: string;
+ name: string;
+ color: string;
+ event: { id: string; name: string };
+ count: number;
+ date: string;
+ previous?: {
+ value: number;
+ diff: number | null;
+ state: 'positive' | 'negative' | 'neutral';
+ };
+};
export function useRechartDataModel(series: IChartData['series']) {
return useMemo(() => {
@@ -25,6 +37,9 @@ export function useRechartDataModel(series: IChartData['series']) {
acc2[`${serie.id}:count`] = item.count;
acc2[`${serie.id}:payload`] = {
...item,
+ id: serie.id,
+ event: serie.event,
+ name: serie.name,
color: getChartColor(idx),
} satisfies IRechartPayloadItem;
}
diff --git a/packages/common/package.json b/packages/common/package.json
index d682c25a..2e11fe2c 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -8,6 +8,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
+ "@openpanel/constants": "workspace:*",
"date-fns": "^3.3.1",
"mathjs": "^12.3.2",
"ramda": "^0.29.1",
@@ -34,4 +35,4 @@
]
},
"prettier": "@openpanel/prettier-config"
-}
+}
\ No newline at end of file
diff --git a/packages/common/src/fill-series.ts b/packages/common/src/fill-series.ts
index 0a7c6f19..2de6e7cb 100644
--- a/packages/common/src/fill-series.ts
+++ b/packages/common/src/fill-series.ts
@@ -11,12 +11,13 @@ import {
startOfMonth,
} from 'date-fns';
+import { NOT_SET_VALUE } from '@openpanel/constants';
import type { IInterval } from '@openpanel/validation';
// Define the data structure
-interface DataEntry {
- label: string;
- count: number | null;
+export interface ISerieDataItem {
+ label: string | null | undefined;
+ count: number;
date: string;
}
@@ -37,8 +38,8 @@ function roundDate(date: Date, interval: IInterval): Date {
}
// Function to complete the timeline for each label
-export function completeTimeline(
- data: DataEntry[],
+export function completeSerie(
+ data: ISerieDataItem[],
_startDate: string,
_endDate: string,
interval: IInterval
@@ -50,16 +51,16 @@ export function completeTimeline(
data.forEach((entry) => {
const roundedDate = roundDate(parseISO(entry.date), interval);
const dateKey = format(roundedDate, 'yyyy-MM-dd HH:mm:ss');
-
- if (!labelsMap.has(entry.label)) {
- labelsMap.set(entry.label, new Map());
+ const label = entry.label || NOT_SET_VALUE;
+ if (!labelsMap.has(label)) {
+ labelsMap.set(label, new Map());
}
- const labelData = labelsMap.get(entry.label);
+ const labelData = labelsMap.get(label);
labelData?.set(dateKey, (labelData.get(dateKey) || 0) + (entry.count || 0));
});
// Complete the timeline for each label
- const result: Record
= {};
+ const result: Record = {};
labelsMap.forEach((counts, label) => {
let currentDate = roundDate(startDate, interval);
result[label] = [];
diff --git a/packages/common/src/math.ts b/packages/common/src/math.ts
index c24eb461..25d6545e 100644
--- a/packages/common/src/math.ts
+++ b/packages/common/src/math.ts
@@ -14,7 +14,7 @@ export const average = (arr: (number | null)[]) => {
return Number.isNaN(avg) ? 0 : avg;
};
-export const sum = (arr: (number | null)[]): number =>
+export const sum = (arr: (number | null | undefined)[]): number =>
round(arr.filter(isNumber).reduce((acc, item) => acc + item, 0));
export const min = (arr: (number | null)[]): number =>
diff --git a/packages/db/src/services/chart.service.ts b/packages/db/src/services/chart.service.ts
index 4f0635d7..cbfdec9f 100644
--- a/packages/db/src/services/chart.service.ts
+++ b/packages/db/src/services/chart.service.ts
@@ -59,18 +59,18 @@ export function getChartSql({
sb.where.endDate = `created_at <= '${formatClickhouseDate(endDate)}'`;
}
- const breakdown = breakdowns[0]!;
- if (breakdown) {
+ breakdowns.forEach((breakdown, index) => {
+ const key = index === 0 ? 'label' : `label_${index}`;
const value = breakdown.name.startsWith('properties.')
? `mapValues(mapExtractKeyLike(properties, ${escape(
breakdown.name.replace(/^properties\./, '').replace('.*.', '.%.')
)}))`
: escape(breakdown.name);
- sb.select.label = breakdown.name.startsWith('properties.')
- ? `arrayElement(${value}, 1) as label`
- : `${breakdown.name} as label`;
- sb.groupBy.label = `label`;
- }
+ sb.select[key] = breakdown.name.startsWith('properties.')
+ ? `arrayElement(${value}, 1) as ${key}`
+ : `${breakdown.name} as ${key}`;
+ sb.groupBy[key] = `${key}`;
+ });
if (event.segment === 'user') {
sb.select.count = `countDistinct(profile_id) as count`;
diff --git a/packages/db/src/services/organization.service.ts b/packages/db/src/services/organization.service.ts
index bff28ca0..85c5519a 100644
--- a/packages/db/src/services/organization.service.ts
+++ b/packages/db/src/services/organization.service.ts
@@ -24,6 +24,7 @@ export function transformOrganization(org: Organization) {
export async function getCurrentOrganizations() {
const session = auth();
if (!session.userId) return [];
+
const organizations = await db.organization.findMany({
where: {
members: {
diff --git a/packages/trpc/src/routers/chart.helpers.ts b/packages/trpc/src/routers/chart.helpers.ts
index 43bb0b62..d5e202a1 100644
--- a/packages/trpc/src/routers/chart.helpers.ts
+++ b/packages/trpc/src/routers/chart.helpers.ts
@@ -3,6 +3,7 @@ import {
endOfMonth,
endOfYear,
formatISO,
+ startOfDay,
startOfMonth,
startOfYear,
subDays,
@@ -15,8 +16,17 @@ import * as mathjs from 'mathjs';
import { repeat, reverse } from 'ramda';
import { escape } from 'sqlstring';
-import { completeTimeline, round } from '@openpanel/common';
-import { alphabetIds, NOT_SET_VALUE } from '@openpanel/constants';
+import {
+ average,
+ completeSerie,
+ max,
+ min,
+ round,
+ slug,
+ sum,
+} from '@openpanel/common';
+import type { ISerieDataItem } from '@openpanel/common';
+import { alphabetIds } from '@openpanel/constants';
import {
chQuery,
createSqlBuilder,
@@ -26,27 +36,23 @@ import {
getProfiles,
} from '@openpanel/db';
import type {
+ FinalChart,
IChartEvent,
IChartInput,
+ IChartInputWithDates,
IChartRange,
IGetChartDataInput,
IInterval,
+ PreviousValue,
} from '@openpanel/validation';
-export type GetChartDataResult = Awaited>;
-export interface ResultItem {
- label: string | null;
- count: number | null;
- date: string;
-}
-
function getEventLegend(event: IChartEvent) {
return event.displayName ?? event.name;
}
export function withFormula(
{ formula, events }: IChartInput,
- series: GetChartDataResult
+ series: Awaited>
) {
if (!formula) {
return series;
@@ -145,58 +151,6 @@ const toDynamicISODateWithTZ = (
return `${date}T00:00:00Z`;
};
-export async function getChartData(payload: IGetChartDataInput) {
- async function getSeries() {
- const result = await chQuery(getChartSql(payload));
- if (result.length === 0 && payload.breakdowns.length > 0) {
- return await chQuery(
- getChartSql({
- ...payload,
- breakdowns: [],
- })
- );
- }
- return result;
- }
-
- return getSeries()
- .then((data) =>
- completeTimeline(
- data.map((item) => {
- const label = item.label?.trim() || NOT_SET_VALUE;
-
- return {
- ...item,
- count: item.count ? round(item.count) : null,
- label,
- };
- }),
- payload.startDate,
- payload.endDate,
- payload.interval
- )
- )
- .then((series) => {
- return Object.keys(series).map((label) => {
- const isBreakdown =
- payload.breakdowns.length && !alphabetIds.includes(label as 'A');
- const serieLabel = isBreakdown ? label : getEventLegend(payload.event);
- return {
- name: serieLabel,
- event: payload.event,
- data: series[label]!.map((item) => ({
- ...item,
- date: toDynamicISODateWithTZ(
- item.date,
- payload.startDate,
- payload.interval
- ),
- })),
- };
- });
- });
-}
-
export function getDatesFromRange(range: IChartRange) {
if (range === '30min' || range === 'lastHour') {
const minutes = range === '30min' ? 30 : 60;
@@ -224,17 +178,7 @@ export function getDatesFromRange(range: IChartRange) {
}
if (range === '7d') {
- const startDate = formatISO(subDays(new Date(), 7));
- const endDate = formatISO(new Date());
-
- return {
- startDate,
- endDate,
- };
- }
-
- if (range === '30d') {
- const startDate = formatISO(subDays(new Date(), 30));
+ const startDate = formatISO(startOfDay(subDays(new Date(), 7)));
const endDate = formatISO(new Date());
return {
@@ -285,9 +229,13 @@ export function getDatesFromRange(range: IChartRange) {
};
}
+ // range === '30d'
+ const startDate = formatISO(startOfDay(subDays(new Date(), 30)));
+ const endDate = formatISO(new Date());
+
return {
- startDate: formatISO(subDays(new Date(), 30)),
- endDate: formatISO(new Date()),
+ startDate,
+ endDate,
};
}
@@ -491,22 +439,51 @@ export async function getFunnelStep({
return getProfiles(res.map((r) => r.id));
}
-export async function getSeriesFromEvents(input: IChartInput) {
- const { startDate, endDate } =
- input.startDate && input.endDate
- ? {
- startDate: input.startDate,
- endDate: input.endDate,
- }
- : getDatesFromRange(input.range);
+export async function getChartSerie(payload: IGetChartDataInput) {
+ async function getSeries() {
+ const result = await chQuery(getChartSql(payload));
+ if (result.length === 0 && payload.breakdowns.length > 0) {
+ return await chQuery(
+ getChartSql({
+ ...payload,
+ breakdowns: [],
+ })
+ );
+ }
+ return result;
+ }
+ return getSeries()
+ .then((data) =>
+ completeSerie(data, payload.startDate, payload.endDate, payload.interval)
+ )
+ .then((series) => {
+ return Object.keys(series).map((label) => {
+ const isBreakdown =
+ payload.breakdowns.length && !alphabetIds.includes(label as 'A');
+ const serieLabel = isBreakdown ? label : getEventLegend(payload.event);
+ return {
+ name: serieLabel,
+ event: payload.event,
+ data: series[label]!.map((item) => ({
+ ...item,
+ date: toDynamicISODateWithTZ(
+ item.date,
+ payload.startDate,
+ payload.interval
+ ),
+ })),
+ };
+ });
+ });
+}
+
+export async function getChartSeries(input: IChartInputWithDates) {
const series = (
await Promise.all(
input.events.map(async (event) =>
- getChartData({
+ getChartSerie({
...input,
- startDate,
- endDate,
event,
})
)
@@ -519,3 +496,188 @@ export async function getSeriesFromEvents(input: IChartInput) {
return series;
}
}
+
+export async function getChart(input: IChartInput) {
+ const currentPeriod = getChartStartEndDate(input);
+ const previousPeriod = getChartPrevStartEndDate({
+ range: input.range,
+ ...currentPeriod,
+ });
+
+ const promises = [getChartSeries({ ...input, ...currentPeriod })];
+
+ if (input.previous) {
+ promises.push(
+ getChartSeries({
+ ...input,
+ ...previousPeriod,
+ })
+ );
+ }
+
+ const result = await Promise.all(promises);
+ const series = result[0]!;
+ const previousSeries = result[1];
+ const limit = input.limit || 300;
+ const offset = input.offset || 0;
+ const final: FinalChart = {
+ series: series
+ .slice(offset, limit ? offset + limit : series.length)
+ .map((serie) => {
+ const previousSerie = previousSeries?.find(
+ (item) => item.name === serie.name
+ );
+ const metrics = {
+ sum: sum(serie.data.map((item) => item.count)),
+ 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)),
+ };
+
+ return {
+ id: slug(serie.name),
+ name: serie.name,
+ event: {
+ id: serie.event.id!,
+ name: serie.event.displayName ?? serie.event.name,
+ },
+ metrics: {
+ ...metrics,
+ ...(input.previous
+ ? {
+ previous: {
+ sum: getPreviousMetric(
+ metrics.sum,
+ previousSerie
+ ? sum(previousSerie?.data.map((item) => item.count))
+ : null
+ ),
+ average: getPreviousMetric(
+ metrics.average,
+ previousSerie
+ ? round(
+ average(
+ previousSerie?.data.map((item) => item.count)
+ ),
+ 2
+ )
+ : null
+ ),
+ min: getPreviousMetric(
+ metrics.sum,
+ previousSerie
+ ? min(previousSerie?.data.map((item) => item.count))
+ : null
+ ),
+ max: getPreviousMetric(
+ metrics.sum,
+ previousSerie
+ ? max(previousSerie?.data.map((item) => item.count))
+ : null
+ ),
+ },
+ }
+ : {}),
+ },
+ data: serie.data.map((item, index) => ({
+ date: item.date,
+ count: item.count ?? 0,
+ previous: previousSerie?.data[index]
+ ? getPreviousMetric(
+ item.count ?? 0,
+ previousSerie?.data[index]?.count ?? null
+ )
+ : undefined,
+ })),
+ };
+ }),
+ metrics: {
+ sum: 0,
+ average: 0,
+ min: 0,
+ max: 0,
+ },
+ };
+
+ final.metrics.sum = sum(final.series.map((item) => item.metrics.sum));
+ final.metrics.average = round(
+ average(final.series.map((item) => item.metrics.average)),
+ 2
+ );
+ final.metrics.min = min(final.series.map((item) => item.metrics.min));
+ final.metrics.max = max(final.series.map((item) => item.metrics.max));
+ if (input.previous) {
+ final.metrics.previous = {
+ sum: getPreviousMetric(
+ final.metrics.sum,
+ sum(final.series.map((item) => item.metrics.previous?.sum?.value ?? 0))
+ ),
+ average: getPreviousMetric(
+ final.metrics.average,
+ round(
+ average(
+ final.series.map(
+ (item) => item.metrics.previous?.average?.value ?? 0
+ )
+ ),
+ 2
+ )
+ ),
+ min: getPreviousMetric(
+ final.metrics.min,
+ min(final.series.map((item) => item.metrics.previous?.min?.value ?? 0))
+ ),
+ max: getPreviousMetric(
+ final.metrics.max,
+ max(final.series.map((item) => item.metrics.previous?.max?.value ?? 0))
+ ),
+ };
+ }
+
+ // Sort by sum
+ final.series = final.series.sort((a, b) => {
+ if (input.chartType === 'linear') {
+ const sumA = a.data.reduce((acc, item) => acc + (item.count ?? 0), 0);
+ const sumB = b.data.reduce((acc, item) => acc + (item.count ?? 0), 0);
+ return sumB - sumA;
+ } else {
+ return b.metrics[input.metric] - a.metrics[input.metric];
+ }
+ });
+
+ return final;
+}
+
+export function getPreviousMetric(
+ current: number,
+ previous: number | null
+): PreviousValue {
+ if (previous === null) {
+ return undefined;
+ }
+
+ const diff = round(
+ ((current > previous
+ ? current / previous
+ : current < previous
+ ? previous / current
+ : 0) -
+ 1) *
+ 100,
+ 1
+ );
+
+ return {
+ diff:
+ Number.isNaN(diff) || !Number.isFinite(diff) || current === previous
+ ? null
+ : diff,
+ state:
+ current > previous
+ ? 'positive'
+ : current < previous
+ ? 'negative'
+ : 'neutral',
+ value: previous,
+ };
+}
diff --git a/packages/trpc/src/routers/chart.ts b/packages/trpc/src/routers/chart.ts
index b950bdad..006f4262 100644
--- a/packages/trpc/src/routers/chart.ts
+++ b/packages/trpc/src/routers/chart.ts
@@ -5,57 +5,24 @@ import { z } from 'zod';
import { average, max, min, round, slug, sum } from '@openpanel/common';
import { chQuery, createSqlBuilder, db } from '@openpanel/db';
import { zChartInput } from '@openpanel/validation';
-import type { IChartEvent, IChartInput } from '@openpanel/validation';
+import type {
+ FinalChart,
+ IChartInput,
+ PreviousValue,
+} from '@openpanel/validation';
import { getProjectAccessCached } from '../access';
import { TRPCAccessError } from '../errors';
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
import {
+ getChart,
getChartPrevStartEndDate,
+ getChartSeries,
getChartStartEndDate,
getFunnelData,
getFunnelStep,
- getSeriesFromEvents,
} from './chart.helpers';
-type PreviousValue = {
- value: number;
- diff: number | null;
- state: 'positive' | 'negative' | 'neutral';
-} | null;
-
-interface Metrics {
- sum: number;
- average: number;
- min: number;
- max: number;
- previous: {
- sum: PreviousValue;
- average: PreviousValue;
- min: PreviousValue;
- max: PreviousValue;
- };
-}
-
-export interface IChartSerie {
- id: string;
- name: string;
- event: IChartEvent;
- metrics: Metrics;
- data: {
- date: string;
- count: number;
- label: string | null;
- previous: PreviousValue;
- }[];
-}
-
-export interface FinalChart {
- events: IChartInput['events'];
- series: IChartSerie[];
- metrics: Metrics;
-}
-
export const chartRouter = createTRPCRouter({
events: protectedProcedure
.input(z.object({ projectId: z.string() }))
@@ -91,6 +58,7 @@ export const chartRouter = createTRPCRouter({
'has_profile',
'name',
'path',
+ 'origin',
'referrer',
'referrer_name',
'duration',
@@ -208,183 +176,3 @@ export const chartRouter = createTRPCRouter({
return getChart(input);
}),
});
-
-export async function getChart(input: IChartInput) {
- const currentPeriod = getChartStartEndDate(input);
- const previousPeriod = getChartPrevStartEndDate({
- range: input.range,
- ...currentPeriod,
- });
-
- const promises = [getSeriesFromEvents({ ...input, ...currentPeriod })];
-
- if (input.previous) {
- promises.push(
- getSeriesFromEvents({
- ...input,
- ...previousPeriod,
- })
- );
- }
-
- const result = await Promise.all(promises);
- const series = result[0]!;
- const previousSeries = result[1];
-
- const final: FinalChart = {
- events: input.events,
- series: series.map((serie) => {
- const previousSerie = previousSeries?.find(
- (item) => item.name === serie.name
- );
- const metrics = {
- sum: sum(serie.data.map((item) => item.count)),
- 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)),
- };
-
- return {
- id: slug(serie.name), // TODO: Remove this (temporary fix for the frontend
- name: serie.name,
- event: {
- ...serie.event,
- displayName: serie.event.displayName ?? serie.event.name,
- },
- metrics: {
- ...metrics,
- previous: {
- sum: getPreviousMetric(
- metrics.sum,
- previousSerie
- ? sum(previousSerie?.data.map((item) => item.count))
- : null
- ),
- average: getPreviousMetric(
- metrics.average,
- previousSerie
- ? round(
- average(previousSerie?.data.map((item) => item.count)),
- 2
- )
- : null
- ),
- min: getPreviousMetric(
- metrics.sum,
- previousSerie
- ? min(previousSerie?.data.map((item) => item.count))
- : null
- ),
- max: getPreviousMetric(
- metrics.sum,
- previousSerie
- ? max(previousSerie?.data.map((item) => item.count))
- : null
- ),
- },
- },
- data: serie.data.map((item, index) => ({
- date: item.date,
- count: item.count ?? 0,
- label: item.label,
- previous: previousSerie?.data[index]
- ? getPreviousMetric(
- item.count ?? 0,
- previousSerie?.data[index]?.count ?? null
- )
- : null,
- })),
- };
- }),
- metrics: {
- sum: 0,
- average: 0,
- min: 0,
- max: 0,
- previous: {
- sum: null,
- average: null,
- min: null,
- max: null,
- },
- },
- };
-
- final.metrics.sum = sum(final.series.map((item) => item.metrics.sum));
- final.metrics.average = round(
- average(final.series.map((item) => item.metrics.average)),
- 2
- );
- final.metrics.min = min(final.series.map((item) => item.metrics.min));
- final.metrics.max = max(final.series.map((item) => item.metrics.max));
- final.metrics.previous = {
- sum: getPreviousMetric(
- final.metrics.sum,
- sum(final.series.map((item) => item.metrics.previous.sum?.value ?? 0))
- ),
- average: getPreviousMetric(
- final.metrics.average,
- round(
- average(
- final.series.map((item) => item.metrics.previous.average?.value ?? 0)
- ),
- 2
- )
- ),
- min: getPreviousMetric(
- final.metrics.min,
- min(final.series.map((item) => item.metrics.previous.min?.value ?? 0))
- ),
- max: getPreviousMetric(
- final.metrics.max,
- max(final.series.map((item) => item.metrics.previous.max?.value ?? 0))
- ),
- };
-
- // Sort by sum
- final.series = final.series.sort((a, b) => {
- if (input.chartType === 'linear') {
- const sumA = a.data.reduce((acc, item) => acc + (item.count ?? 0), 0);
- const sumB = b.data.reduce((acc, item) => acc + (item.count ?? 0), 0);
- return sumB - sumA;
- } else {
- return b.metrics[input.metric] - a.metrics[input.metric];
- }
- });
-
- return final;
-}
-
-export function getPreviousMetric(
- current: number,
- previous: number | null
-): PreviousValue {
- if (previous === null) {
- return null;
- }
-
- const diff = round(
- ((current > previous
- ? current / previous
- : current < previous
- ? previous / current
- : 0) -
- 1) *
- 100,
- 1
- );
-
- return {
- diff:
- Number.isNaN(diff) || !Number.isFinite(diff) || current === previous
- ? null
- : diff,
- state:
- current > previous
- ? 'positive'
- : current < previous
- ? 'negative'
- : 'neutral',
- value: previous,
- };
-}
diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts
index ce419fc6..7d06a15f 100644
--- a/packages/validation/src/index.ts
+++ b/packages/validation/src/index.ts
@@ -71,6 +71,8 @@ export const zChartInput = z.object({
projectId: z.string(),
startDate: z.string().nullish(),
endDate: z.string().nullish(),
+ limit: z.number().optional(),
+ offset: z.number().optional(),
});
export const zReportInput = zChartInput.extend({
diff --git a/packages/validation/src/types.validation.ts b/packages/validation/src/types.validation.ts
index 00d8296f..0270c778 100644
--- a/packages/validation/src/types.validation.ts
+++ b/packages/validation/src/types.validation.ts
@@ -31,9 +31,54 @@ export type IChartType = z.infer;
export type IChartMetric = z.infer;
export type IChartLineType = z.infer;
export type IChartRange = z.infer;
+export interface IChartInputWithDates extends IChartInput {
+ startDate: string;
+ endDate: string;
+}
export type IGetChartDataInput = {
event: IChartEvent;
projectId: string;
startDate: string;
endDate: string;
} & Omit;
+
+export type PreviousValue =
+ | {
+ value: number;
+ diff: number | null;
+ state: 'positive' | 'negative' | 'neutral';
+ }
+ | undefined;
+
+export type Metrics = {
+ sum: number;
+ average: number;
+ min: number;
+ max: number;
+ previous?: {
+ sum: PreviousValue;
+ average: PreviousValue;
+ min: PreviousValue;
+ max: PreviousValue;
+ };
+};
+
+export type IChartSerie = {
+ id: string;
+ name: string;
+ event: {
+ id: string;
+ name: string;
+ };
+ metrics: Metrics;
+ data: {
+ date: string;
+ count: number;
+ previous: PreviousValue;
+ }[];
+};
+
+export type FinalChart = {
+ series: IChartSerie[];
+ metrics: Metrics;
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 601f6e68..53a5a936 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -799,6 +799,9 @@ importers:
packages/common:
dependencies:
+ '@openpanel/constants':
+ specifier: workspace:*
+ version: link:../constants
date-fns:
specifier: ^3.3.1
version: 3.3.1