improve graphs and table
This commit is contained in:
@@ -14,6 +14,8 @@
|
||||
"@mixan/types": "^0.0.2-alpha",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.1.1",
|
||||
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
@@ -38,10 +40,12 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-virtualized-auto-sizer": "^1.0.20",
|
||||
"recharts": "^2.8.0",
|
||||
"superjson": "^1.13.1",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"usehooks-ts": "^2.9.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
3
apps/web/src/components/AutoSizer.tsx
Normal file
3
apps/web/src/components/AutoSizer.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import AutoSizer from "react-virtualized-auto-sizer";
|
||||
|
||||
export { AutoSizer }
|
||||
@@ -17,6 +17,17 @@ import {
|
||||
} from "@/types";
|
||||
import { getChartColor } from "@/utils/theme";
|
||||
import { ReportTable } from "./ReportTable";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AutoSizer } from "@/components/AutoSizer";
|
||||
|
||||
type ReportLineChartProps = {
|
||||
interval: IInterval;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
events: IChartEvent[];
|
||||
breakdowns: IChartBreakdown[];
|
||||
showTable?: boolean;
|
||||
};
|
||||
|
||||
export function ReportLineChart({
|
||||
interval,
|
||||
@@ -24,13 +35,9 @@ export function ReportLineChart({
|
||||
endDate,
|
||||
events,
|
||||
breakdowns,
|
||||
}: {
|
||||
interval: IInterval;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
events: IChartEvent[];
|
||||
breakdowns: IChartBreakdown[];
|
||||
}) {
|
||||
showTable,
|
||||
}: ReportLineChartProps) {
|
||||
const [visibleSeries, setVisibleSeries] = useState<string[]>([]);
|
||||
const chart = api.chartMeta.chart.useQuery(
|
||||
{
|
||||
interval,
|
||||
@@ -47,42 +54,71 @@ export function ReportLineChart({
|
||||
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
|
||||
return (
|
||||
<>
|
||||
const ref = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!ref.current && chart.data) {
|
||||
const max = 20;
|
||||
|
||||
setVisibleSeries(
|
||||
chart.data?.series?.slice(0, max).map((serie) => serie.name) ?? [],
|
||||
);
|
||||
// ref.current = true;
|
||||
}
|
||||
}, [chart.data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{chart.isSuccess && chart.data?.series?.[0]?.data && (
|
||||
<>
|
||||
<LineChart width={800} height={400}>
|
||||
<Legend />
|
||||
<YAxis dataKey={"count"}></YAxis>
|
||||
<Tooltip content={<ReportLineChartTooltip />} />
|
||||
{/* <Tooltip /> */}
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={(m: Date) => {
|
||||
return formatDate(m);
|
||||
}}
|
||||
tickLine={false}
|
||||
allowDuplicatedCategory={false}
|
||||
/>
|
||||
{chart.data?.series.slice(0, 5).map((serie, index) => {
|
||||
const key = serie.name;
|
||||
const strokeColor = getChartColor(index)
|
||||
return (
|
||||
<Line
|
||||
type="monotone"
|
||||
key={key}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
dataKey="count"
|
||||
stroke={strokeColor}
|
||||
data={serie.data}
|
||||
name={serie.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</LineChart>
|
||||
<ReportTable data={chart.data} />
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<LineChart width={width} height={width * 0.5}>
|
||||
{/* <Legend /> */}
|
||||
<YAxis dataKey={"count"}></YAxis>
|
||||
<Tooltip content={<ReportLineChartTooltip />} />
|
||||
{/* <Tooltip /> */}
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={(m: Date) => {
|
||||
return formatDate(m);
|
||||
}}
|
||||
tickLine={false}
|
||||
allowDuplicatedCategory={false}
|
||||
/>
|
||||
{chart.data?.series
|
||||
.filter((serie) => {
|
||||
return visibleSeries.includes(serie.name);
|
||||
})
|
||||
.map((serie) => {
|
||||
const realIndex = chart.data?.series.findIndex(
|
||||
(item) => item.name === serie.name,
|
||||
);
|
||||
const key = serie.name;
|
||||
const strokeColor = getChartColor(realIndex);
|
||||
return (
|
||||
<Line
|
||||
type="monotone"
|
||||
key={key}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
dataKey="count"
|
||||
stroke={strokeColor}
|
||||
data={serie.data}
|
||||
name={serie.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</LineChart>
|
||||
)}
|
||||
</AutoSizer>
|
||||
{showTable && (
|
||||
<ReportTable
|
||||
data={chart.data}
|
||||
visibleSeries={visibleSeries}
|
||||
setVisibleSeries={setVisibleSeries}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -4,48 +4,81 @@ import { useFormatDateInterval } from "@/hooks/useFormatDateInterval";
|
||||
import { useSelector } from "@/redux";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { getChartColor } from "@/utils/theme";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
type ReportTableProps = {
|
||||
data: RouterOutputs["chartMeta"]["chart"];
|
||||
visibleSeries: string[];
|
||||
setVisibleSeries: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
};
|
||||
|
||||
export function ReportTable({
|
||||
data,
|
||||
}: {
|
||||
data: RouterOutputs["chartMeta"]["chart"];
|
||||
}) {
|
||||
visibleSeries,
|
||||
setVisibleSeries,
|
||||
}: ReportTableProps) {
|
||||
const interval = useSelector((state) => state.report.interval);
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
|
||||
function handleChange(name: string, checked: boolean) {
|
||||
setVisibleSeries((prev) => {
|
||||
if (checked) {
|
||||
return [...prev, name];
|
||||
} else {
|
||||
return prev.filter((item) => item !== name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const row = "flex border-b border-border last:border-b-0 flex-1";
|
||||
const cell = "p-2 last:pr-8 last:w-[8rem]";
|
||||
const value = "min-w-[6rem] text-right";
|
||||
const header = "text-sm font-medium";
|
||||
const total = 'bg-gray-50 text-emerald-600 font-bold border-r border-border'
|
||||
return (
|
||||
<div className="flex min-w-0">
|
||||
<div className="flex w-fit max-w-full rounded-md border border-border">
|
||||
{/* Labels */}
|
||||
<div>
|
||||
<div className="font-medium">Name</div>
|
||||
<div className="border-r border-border">
|
||||
<div className={cn(header, row, cell)}>Name</div>
|
||||
{data.series.map((serie, index) => {
|
||||
const checked = index < 5;
|
||||
const checked = visibleSeries.includes(serie.name);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={serie.name}
|
||||
className="max-w-[200px] overflow-hidden text-ellipsis whitespace-nowrap flex items-center gap-2"
|
||||
className={cn("flex max-w-[200px] items-center gap-2", row, cell)}
|
||||
>
|
||||
<Checkbox
|
||||
style={checked ? {
|
||||
background: getChartColor(index),
|
||||
borderColor: getChartColor(index),
|
||||
} : undefined}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange(serie.name, !!checked)
|
||||
}
|
||||
style={
|
||||
checked
|
||||
? {
|
||||
background: getChartColor(index),
|
||||
borderColor: getChartColor(index),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
checked={checked}
|
||||
/>
|
||||
{serie.name}
|
||||
<div className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{serie.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* ScrollView for all values */}
|
||||
<div className="min-w-0 overflow-auto">
|
||||
<div className="w-full overflow-auto">
|
||||
{/* Header */}
|
||||
<div className="flex">
|
||||
{data.series[0]?.data.map((serie, index) => (
|
||||
<div className={cn("w-max", row)}>
|
||||
<div className={cn(header, value, cell, total)}>Total</div>
|
||||
{data.series[0]?.data.map((serie) => (
|
||||
<div
|
||||
key={serie.date.toString()}
|
||||
className="min-w-[80px] text-right font-medium"
|
||||
className={cn(header, value, cell)}
|
||||
>
|
||||
{formatDate(serie.date)}
|
||||
</div>
|
||||
@@ -53,12 +86,13 @@ export function ReportTable({
|
||||
</div>
|
||||
|
||||
{/* Values */}
|
||||
{data.series.map((serie, index) => {
|
||||
{data.series.map((serie) => {
|
||||
return (
|
||||
<div className="flex" key={serie.name}>
|
||||
<div className={cn("w-max", row)} key={serie.name}>
|
||||
<div className={cn(header, value, cell, total)}>{serie.totalCount}</div>
|
||||
{serie.data.map((item) => {
|
||||
return (
|
||||
<div key={item.date} className="min-w-[80px] text-right">
|
||||
<div key={item.date} className={cn(value, cell)}>
|
||||
{item.count}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type IChartEvent,
|
||||
type IInterval,
|
||||
} from "@/types";
|
||||
import { getDaysOldDate } from "@/utils/date";
|
||||
import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
type InitialState = {
|
||||
@@ -15,27 +16,20 @@ type InitialState = {
|
||||
|
||||
// First approach: define the initial state using that type
|
||||
const initialState: InitialState = {
|
||||
startDate: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
|
||||
startDate: getDaysOldDate(7),
|
||||
endDate: new Date(),
|
||||
interval: "day",
|
||||
breakdowns: [
|
||||
{
|
||||
id: "A",
|
||||
name: "properties.params.title",
|
||||
},
|
||||
name: 'properties.id'
|
||||
}
|
||||
],
|
||||
events: [
|
||||
{
|
||||
id: "A",
|
||||
displayName: "screen_view (0)",
|
||||
name: "screen_view",
|
||||
filters: [
|
||||
{
|
||||
id: "1",
|
||||
name: "properties.route",
|
||||
value: "RecipeDetails",
|
||||
},
|
||||
],
|
||||
name: "sign_up",
|
||||
filters: []
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -115,6 +109,18 @@ export const reportSlice = createSlice({
|
||||
changeEndDate: (state, action: PayloadAction<Date>) => {
|
||||
state.endDate = action.payload;
|
||||
},
|
||||
|
||||
changeDateRanges: (state, action: PayloadAction<number>) => {
|
||||
if(action.payload === 1) {
|
||||
state.interval = "hour";
|
||||
} else if(action.payload <= 30) {
|
||||
state.interval = "day";
|
||||
} else {
|
||||
state.interval = "month";
|
||||
}
|
||||
state.startDate = getDaysOldDate(action.payload);
|
||||
state.endDate = new Date();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -127,6 +133,7 @@ export const {
|
||||
removeBreakdown,
|
||||
changeBreakdown,
|
||||
changeInterval,
|
||||
changeDateRanges,
|
||||
} = reportSlice.actions;
|
||||
|
||||
export default reportSlice.reducer;
|
||||
|
||||
@@ -141,7 +141,7 @@ function Filter({ filter, event }: FilterProps) {
|
||||
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-emerald-600 text-xs font-medium text-white">
|
||||
<SlidersHorizontal size={10} />
|
||||
</div>
|
||||
<RenderDots className="text-sm">{filter.name}</RenderDots>
|
||||
<RenderDots className="text-sm flex-1">{filter.name}</RenderDots>
|
||||
<Button variant="ghost" size="sm" onClick={removeFilter}>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
|
||||
@@ -36,11 +36,11 @@ export function ReportEvents() {
|
||||
<div>
|
||||
<h3 className="mb-2 font-medium">Events</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
{selectedEvents.map((event, index) => {
|
||||
{selectedEvents.map((event) => {
|
||||
return (
|
||||
<div key={event.name} className="border rounded-lg">
|
||||
<div className="flex gap-2 items-center p-2 px-4">
|
||||
<div className="flex-shrink-0 bg-purple-500 w-5 h-5 rounded text-xs flex items-center justify-center text-white font-medium">{index}</div>
|
||||
<div className="flex-shrink-0 bg-purple-500 w-5 h-5 rounded text-xs flex items-center justify-center text-white font-medium">{event.id}</div>
|
||||
<Combobox
|
||||
value={event.name}
|
||||
onChange={(value) => {
|
||||
@@ -70,7 +70,6 @@ export function ReportEvents() {
|
||||
onChange={(value) => {
|
||||
dispatch(
|
||||
addEvent({
|
||||
displayName: `${value} (${selectedEvents.length})`,
|
||||
name: value,
|
||||
filters: [],
|
||||
}),
|
||||
|
||||
5
apps/web/src/components/ui/aspect-ratio.tsx
Normal file
5
apps/web/src/components/ui/aspect-ratio.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root
|
||||
|
||||
export { AspectRatio }
|
||||
48
apps/web/src/components/ui/avatar.tsx
Normal file
48
apps/web/src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/utils/cn"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
@@ -6,8 +6,15 @@ import { ReportLineChart } from "@/components/report/chart/ReportLineChart";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Combobox } from "@/components/ui/combobox";
|
||||
import { useDispatch, useSelector } from "@/redux";
|
||||
import { changeInterval } from "@/components/report/reportSlice";
|
||||
import {
|
||||
changeDateRanges,
|
||||
changeInterval,
|
||||
} from "@/components/report/reportSlice";
|
||||
import { type IInterval } from "@/types";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuShortcut, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { User } from "lucide-react";
|
||||
import { DropdownMenuSeparator } from "@radix-ui/react-dropdown-menu";
|
||||
|
||||
export default function Home() {
|
||||
const dispatch = useDispatch();
|
||||
@@ -24,6 +31,50 @@ export default function Home() {
|
||||
<meta name="description" content="Generated by create-t3-app" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<nav className="flex h-20 items-center border-b border-border px-8 justify-between bg-black text-white">
|
||||
<div className="flex flex-col [&_*]:leading-tight">
|
||||
<span className="text-2xl font-extrabold">MIXAN</span>
|
||||
<span className="text-xs text-muted">v0.0.1</span>
|
||||
</div>
|
||||
<div className="flex gap-4 uppercase text-sm">
|
||||
<a href="#">Dashboards</a>
|
||||
<a href="#">Reports</a>
|
||||
<a href="#">Events</a>
|
||||
<a href="#">Users</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button>
|
||||
<Avatar className="text-black">
|
||||
<AvatarFallback>CL</AvatarFallback>
|
||||
</Avatar>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Organization
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-red-600">
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Logout
|
||||
<DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="grid min-h-screen grid-cols-[400px_minmax(0,1fr)] divide-x">
|
||||
<div>
|
||||
<ReportSidebar />
|
||||
@@ -32,12 +83,55 @@ export default function Home() {
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="flex gap-4">
|
||||
<RadioGroup>
|
||||
<RadioGroupItem>7 days</RadioGroupItem>
|
||||
<RadioGroupItem>14 days</RadioGroupItem>
|
||||
<RadioGroupItem>1 month</RadioGroupItem>
|
||||
<RadioGroupItem>3 month</RadioGroupItem>
|
||||
<RadioGroupItem>6 month</RadioGroupItem>
|
||||
<RadioGroupItem>1 year</RadioGroupItem>
|
||||
<RadioGroupItem
|
||||
onClick={() => {
|
||||
dispatch(changeDateRanges(1));
|
||||
}}
|
||||
>
|
||||
Today
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem
|
||||
onClick={() => {
|
||||
dispatch(changeDateRanges(1));
|
||||
}}
|
||||
>
|
||||
7 days
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem
|
||||
onClick={() => {
|
||||
dispatch(changeDateRanges(14));
|
||||
}}
|
||||
>
|
||||
14 days
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem
|
||||
onClick={() => {
|
||||
dispatch(changeDateRanges(30));
|
||||
}}
|
||||
>
|
||||
1 month
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem
|
||||
onClick={() => {
|
||||
dispatch(changeDateRanges(90));
|
||||
}}
|
||||
>
|
||||
3 month
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem
|
||||
onClick={() => {
|
||||
dispatch(changeDateRanges(180));
|
||||
}}
|
||||
>
|
||||
6 month
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem
|
||||
onClick={() => {
|
||||
dispatch(changeDateRanges(356));
|
||||
}}
|
||||
>
|
||||
1 year
|
||||
</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<div className="w-full max-w-[200px]">
|
||||
<Combobox
|
||||
@@ -70,6 +164,7 @@ export default function Home() {
|
||||
events={events}
|
||||
breakdowns={breakdowns}
|
||||
interval={interval}
|
||||
showTable
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,14 @@ function propertyNameToSql(name: string) {
|
||||
return name;
|
||||
}
|
||||
|
||||
function getEventLegend(event: IChartEvent) {
|
||||
return `${event.name} (${event.id})`
|
||||
}
|
||||
|
||||
function getTotalCount(arr: ResultItem[]) {
|
||||
return arr.reduce((acc, item) => acc + item.count, 0);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
responseLimit: false,
|
||||
@@ -114,9 +122,14 @@ async function getChartData({
|
||||
console.log(sql);
|
||||
|
||||
const result = await db.$queryRawUnsafe<ResultItem[]>(sql);
|
||||
|
||||
// group by sql label
|
||||
const series = result.reduce(
|
||||
(acc, item) => {
|
||||
const label = item.label?.trim() ?? event.displayName;
|
||||
// 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)
|
||||
if (label) {
|
||||
if (acc[label]) {
|
||||
acc[label]?.push(item);
|
||||
@@ -133,17 +146,20 @@ async function getChartData({
|
||||
);
|
||||
|
||||
return Object.keys(series).map((key) => {
|
||||
const legend = breakdowns.length ? key : getEventLegend(event);
|
||||
const data = series[key] ?? []
|
||||
return {
|
||||
name: breakdowns.length ? key ?? "break a leg" : event.displayName,
|
||||
name: legend,
|
||||
totalCount: getTotalCount(data),
|
||||
data: fillEmptySpotsInTimeline(
|
||||
series[key] ?? [],
|
||||
data,
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
label: breakdowns.length ? key ?? "break a leg" : event.displayName,
|
||||
label: legend,
|
||||
count: item.count,
|
||||
date: new Date(item.date).toISOString(),
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function getDaysOldDate(days: number) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - days);
|
||||
return date;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { z } from "zod";
|
||||
export const zChartEvent = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
displayName: z.string(),
|
||||
filters: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
|
||||
Reference in New Issue
Block a user