add bar chart support and other fixes
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import { pipe, sort, uniq } from "ramda";
|
||||
import { last, pipe, sort, uniq } from "ramda";
|
||||
import { toDots } from "@/utils/object";
|
||||
import { zChartInput } from "@/utils/validation";
|
||||
import { type IChartInput, type IChartEvent } from "@/types";
|
||||
@@ -13,15 +13,14 @@ export const config = {
|
||||
};
|
||||
|
||||
export const chartRouter = createTRPCRouter({
|
||||
events: protectedProcedure
|
||||
.query(async () => {
|
||||
const events = await db.event.findMany({
|
||||
take: 500,
|
||||
distinct: ["name"],
|
||||
});
|
||||
events: protectedProcedure.query(async () => {
|
||||
const events = await db.event.findMany({
|
||||
take: 500,
|
||||
distinct: ["name"],
|
||||
});
|
||||
|
||||
return events;
|
||||
}),
|
||||
return events;
|
||||
}),
|
||||
|
||||
properties: protectedProcedure
|
||||
.input(z.object({ event: z.string() }).optional())
|
||||
@@ -106,12 +105,37 @@ export const chartRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
series: series.sort((a, b) => {
|
||||
const sorted = [...series].sort((a, b) => {
|
||||
if (input.chartType === "linear") {
|
||||
const sumA = a.data.reduce((acc, item) => acc + item.count, 0);
|
||||
const sumB = b.data.reduce((acc, item) => acc + item.count, 0);
|
||||
return sumB - sumA;
|
||||
}),
|
||||
} else {
|
||||
return b.totalCount - a.totalCount;
|
||||
}
|
||||
});
|
||||
|
||||
const meta = {
|
||||
highest: sorted[0]?.totalCount ?? 0,
|
||||
lowest: last(sorted)?.totalCount ?? 0,
|
||||
};
|
||||
|
||||
return {
|
||||
events: Object.entries(series.reduce((acc, item) => {
|
||||
if(acc[item.event.id]) {
|
||||
acc[item.event.id] += item.totalCount;
|
||||
} else {
|
||||
acc[item.event.id] = item.totalCount;
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<typeof series[number]['event']['id'], number>)).map(([id, count]) => ({
|
||||
count,
|
||||
...events.find((event) => event.id === id)!,
|
||||
})),
|
||||
series: sorted.map((item) => ({
|
||||
...item,
|
||||
meta,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -169,7 +193,7 @@ async function getChartData({
|
||||
endDate,
|
||||
}: {
|
||||
event: IChartEvent;
|
||||
} & Omit<IChartInput, 'events'>) {
|
||||
} & Omit<IChartInput, "events">) {
|
||||
const select = [];
|
||||
const where = [];
|
||||
const groupBy = [];
|
||||
@@ -259,30 +283,34 @@ async function getChartData({
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
where.push(`"createdAt" >= '${startDate.toISOString()}'`);
|
||||
where.push(`"createdAt" >= '${startDate}'`);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
where.push(`"createdAt" <= '${endDate.toISOString()}'`);
|
||||
where.push(`"createdAt" <= '${endDate}'`);
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT ${select.join(", ")}
|
||||
FROM events
|
||||
WHERE ${where.join(" AND ")}
|
||||
GROUP BY ${groupBy.join(", ")}
|
||||
ORDER BY ${orderBy.join(", ")}
|
||||
`;
|
||||
const sql = [
|
||||
`SELECT ${select.join(", ")}`,
|
||||
`FROM events`,
|
||||
`WHERE ${where.join(" AND ")}`,
|
||||
];
|
||||
|
||||
const result = await db.$queryRawUnsafe<ResultItem[]>(sql);
|
||||
if (groupBy.length) {
|
||||
sql.push(`GROUP BY ${groupBy.join(", ")}`);
|
||||
}
|
||||
if (orderBy.length) {
|
||||
sql.push(`ORDER BY ${orderBy.join(", ")}`);
|
||||
}
|
||||
|
||||
const result = await db.$queryRawUnsafe<ResultItem[]>(sql.join("\n"));
|
||||
|
||||
// group by sql label
|
||||
const series = result.reduce(
|
||||
(acc, item) => {
|
||||
// item.label can be null when using breakdowns on a property
|
||||
// that doesn't exist on all events
|
||||
// fallback on event legend
|
||||
const label = item.label?.trim() ?? getEventLegend(event);
|
||||
const label = item.label?.trim() ?? event.id;
|
||||
if (label) {
|
||||
if (acc[label]) {
|
||||
acc[label]?.push(item);
|
||||
@@ -301,18 +329,26 @@ async function getChartData({
|
||||
return Object.keys(series).map((key) => {
|
||||
const legend = breakdowns.length ? key : getEventLegend(event);
|
||||
const data = series[key] ?? [];
|
||||
|
||||
return {
|
||||
name: legend,
|
||||
event: {
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
},
|
||||
totalCount: getTotalCount(data),
|
||||
data: fillEmptySpotsInTimeline(data, interval, startDate, endDate).map(
|
||||
(item) => {
|
||||
return {
|
||||
label: legend,
|
||||
count: item.count,
|
||||
date: new Date(item.date).toISOString(),
|
||||
};
|
||||
},
|
||||
),
|
||||
data:
|
||||
chartType === "linear"
|
||||
? fillEmptySpotsInTimeline(data, interval, startDate, endDate).map(
|
||||
(item) => {
|
||||
return {
|
||||
label: legend,
|
||||
count: item.count,
|
||||
date: new Date(item.date).toISOString(),
|
||||
};
|
||||
},
|
||||
)
|
||||
: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -320,8 +356,8 @@ async function getChartData({
|
||||
function fillEmptySpotsInTimeline(
|
||||
items: ResultItem[],
|
||||
interval: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
) {
|
||||
const result = [];
|
||||
const clonedStartDate = new Date(startDate);
|
||||
|
||||
@@ -8,21 +8,42 @@ import {
|
||||
type IChartInput,
|
||||
type IChartBreakdown,
|
||||
type IChartEvent,
|
||||
type IChartEventFilter,
|
||||
} from "@/types";
|
||||
import { type Report as DbReport } from "@prisma/client";
|
||||
import { getProjectBySlug } from "@/server/services/project.service";
|
||||
import { getDashboardBySlug } from "@/server/services/dashboard.service";
|
||||
import { alphabetIds } from "@/utils/constants";
|
||||
|
||||
function transform(report: DbReport): IChartInput & { id: string } {
|
||||
function transformFilter(filter: Partial<IChartEventFilter>, index: number): IChartEventFilter {
|
||||
return {
|
||||
id: filter.id ?? alphabetIds[index]!,
|
||||
name: filter.name ?? 'Unknown Filter',
|
||||
operator: filter.operator ?? 'is',
|
||||
value: typeof filter.value === 'string' ? [filter.value] : filter.value ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
function transformEvent(event: Partial<IChartEvent>, index: number): IChartEvent {
|
||||
return {
|
||||
segment: event.segment ?? 'event',
|
||||
filters: (event.filters ?? []).map(transformFilter),
|
||||
id: event.id ?? alphabetIds[index]!,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
name: event.name || 'Untitled',
|
||||
}
|
||||
}
|
||||
|
||||
function transformReport(report: DbReport): IChartInput & { id: string } {
|
||||
return {
|
||||
id: report.id,
|
||||
events: report.events as IChartEvent[],
|
||||
events: (report.events as IChartEvent[]).map(transformEvent),
|
||||
breakdowns: report.breakdowns as IChartBreakdown[],
|
||||
startDate: getDaysOldDate(report.range),
|
||||
endDate: new Date(),
|
||||
startDate: getDaysOldDate(report.range).toISOString(),
|
||||
endDate: new Date().toISOString(),
|
||||
chartType: report.chart_type,
|
||||
interval: report.interval,
|
||||
name: report.name,
|
||||
name: report.name || 'Untitled',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,7 +61,7 @@ export const reportRouter = createTRPCRouter({
|
||||
id,
|
||||
},
|
||||
})
|
||||
.then(transform);
|
||||
.then(transformReport);
|
||||
}),
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
@@ -60,7 +81,7 @@ export const reportRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
return {
|
||||
reports: reports.map(transform),
|
||||
reports: reports.map(transformReport),
|
||||
dashboard,
|
||||
}
|
||||
}),
|
||||
@@ -82,7 +103,7 @@ export const reportRouter = createTRPCRouter({
|
||||
interval: report.interval,
|
||||
breakdowns: report.breakdowns,
|
||||
chart_type: report.chartType,
|
||||
range: dateDifferanceInDays(report.endDate, report.startDate),
|
||||
range: dateDifferanceInDays(new Date(report.endDate), new Date(report.startDate)),
|
||||
},
|
||||
});
|
||||
}),
|
||||
@@ -108,7 +129,7 @@ export const reportRouter = createTRPCRouter({
|
||||
interval: report.interval,
|
||||
breakdowns: report.breakdowns,
|
||||
chart_type: report.chartType,
|
||||
range: dateDifferanceInDays(report.endDate, report.startDate),
|
||||
range: dateDifferanceInDays(new Date(report.endDate), new Date(report.startDate)),
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user