web: add previous/compare values for all charts
This commit is contained in:
43
apps/web/src/components/report/PreviousDiffIndicator.tsx
Normal file
43
apps/web/src/components/report/PreviousDiffIndicator.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
|
||||
import { useChartContext } from './chart/ChartProvider';
|
||||
|
||||
interface PreviousDiffIndicatorProps {
|
||||
diff?: number | null | undefined;
|
||||
state?: string | null | undefined;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PreviousDiffIndicator({
|
||||
diff,
|
||||
state,
|
||||
children,
|
||||
}: PreviousDiffIndicatorProps) {
|
||||
const { previous } = useChartContext();
|
||||
const number = useNumber();
|
||||
if (
|
||||
(children === undefined && (diff === null || diff === undefined)) ||
|
||||
previous === false
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn('flex items-center', [
|
||||
state === 'positive' && 'text-emerald-500',
|
||||
state === 'negative' && 'text-rose-500',
|
||||
state === 'neutral' && 'text-slate-400',
|
||||
])}
|
||||
>
|
||||
{state === 'positive' && <ChevronUp size={20} />}
|
||||
{state === 'negative' && <ChevronDown size={20} />}
|
||||
{number.format(diff)}%
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -16,7 +16,12 @@ export function ReportInterval({ className }: ReportIntervalProps) {
|
||||
const interval = useSelector((state) => state.report.interval);
|
||||
const range = useSelector((state) => state.report.range);
|
||||
const chartType = useSelector((state) => state.report.chartType);
|
||||
if (chartType !== 'linear' && chartType !== 'histogram') {
|
||||
if (
|
||||
chartType !== 'linear' &&
|
||||
chartType !== 'histogram' &&
|
||||
chartType !== 'area' &&
|
||||
chartType !== 'metric'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createContext, memo, useContext, useMemo } from 'react';
|
||||
|
||||
export interface ChartContextType {
|
||||
editMode: boolean;
|
||||
previous?: boolean;
|
||||
}
|
||||
|
||||
type ChartProviderProps = {
|
||||
@@ -12,14 +13,19 @@ const ChartContext = createContext<ChartContextType>({
|
||||
editMode: false,
|
||||
});
|
||||
|
||||
export function ChartProvider({ children, editMode }: ChartProviderProps) {
|
||||
export function ChartProvider({
|
||||
children,
|
||||
editMode,
|
||||
previous,
|
||||
}: ChartProviderProps) {
|
||||
return (
|
||||
<ChartContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
editMode,
|
||||
previous: previous ?? false,
|
||||
}),
|
||||
[editMode]
|
||||
[editMode, previous]
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ import type { SortingState } from '@tanstack/react-table';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useElementSize } from 'usehooks-ts';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
|
||||
const columnHelper =
|
||||
@@ -36,6 +38,7 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
|
||||
const [ref, { width }] = useElementSize();
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const maxCount = Math.max(...data.series.map((serie) => serie.metrics.sum));
|
||||
const number = useNumber();
|
||||
const table = useReactTable({
|
||||
data: useMemo(
|
||||
() => (editMode ? data.series : data.series.slice(0, 20)),
|
||||
@@ -60,7 +63,12 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
|
||||
columnHelper.accessor((row) => row.metrics.sum, {
|
||||
id: 'totalCount',
|
||||
cell: (info) => (
|
||||
<div className="text-right font-medium">{info.getValue()}</div>
|
||||
<div className="text-right font-medium flex gap-2">
|
||||
<div>{number.format(info.getValue())}</div>
|
||||
<PreviousDiffIndicator
|
||||
{...info.row.original.metrics.previous.sum}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
header: () => 'Count',
|
||||
footer: (info) => info.column.id,
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import React from 'react';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useMappings } from '@/hooks/useMappings';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import type { IRechartPayloadItem } from '@/hooks/useRechartDataModel';
|
||||
import { useSelector } from '@/redux';
|
||||
import type { IToolTipProps } from '@/types';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
|
||||
type ReportLineChartTooltipProps = IToolTipProps<{
|
||||
value: number;
|
||||
dataKey: string;
|
||||
payload: {
|
||||
date: Date;
|
||||
count: number;
|
||||
label: string;
|
||||
color: string;
|
||||
} & Record<string, any>;
|
||||
payload: Record<string, unknown>;
|
||||
}>;
|
||||
|
||||
export function ReportChartTooltip({
|
||||
active,
|
||||
payload,
|
||||
}: ReportLineChartTooltipProps) {
|
||||
const { previous } = useChartContext();
|
||||
const getLabel = useMappings();
|
||||
const interval = useSelector((state) => state.report.interval);
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
|
||||
const number = useNumber();
|
||||
if (!active || !payload) {
|
||||
return null;
|
||||
}
|
||||
@@ -31,7 +33,10 @@ export function ReportChartTooltip({
|
||||
}
|
||||
|
||||
const limit = 3;
|
||||
const sorted = payload.slice(0).sort((a, b) => b.value - a.value);
|
||||
const sorted = payload
|
||||
.slice(0)
|
||||
.filter((item) => !item.dataKey.includes(':prev:count'))
|
||||
.sort((a, b) => b.value - a.value);
|
||||
const visible = sorted.slice(0, limit);
|
||||
const hidden = sorted.slice(limit);
|
||||
|
||||
@@ -40,26 +45,46 @@ export function ReportChartTooltip({
|
||||
{visible.map((item, index) => {
|
||||
// If we have a <Cell /> component, payload can be nested
|
||||
const payload = item.payload.payload ?? item.payload;
|
||||
const data = item.dataKey.includes(':')
|
||||
? payload[`${item.dataKey.split(':')[0]}:payload`]
|
||||
: payload;
|
||||
const data = (
|
||||
item.dataKey.includes(':')
|
||||
? // @ts-expect-error
|
||||
payload[`${item.dataKey.split(':')[0]}:payload`]
|
||||
: payload
|
||||
) as IRechartPayloadItem;
|
||||
|
||||
return (
|
||||
<>
|
||||
{index === 0 && data.date ? formatDate(new Date(data.date)) : null}
|
||||
<div key={item.payload.label} className="flex gap-2">
|
||||
<React.Fragment key={data.label}>
|
||||
{index === 0 && data.date && (
|
||||
<div className="flex justify-between gap-8">
|
||||
<div>{formatDate(new Date(data.date))}</div>
|
||||
{previous && data.previous?.date && (
|
||||
<div className="text-slate-400 italic">
|
||||
{formatDate(new Date(data.previous.date))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<div
|
||||
className="w-[3px] rounded-full"
|
||||
style={{ background: data.color }}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="min-w-0 max-w-[200px] overflow-hidden text-ellipsis whitespace-nowrap font-medium">
|
||||
{getLabel(data.label)}
|
||||
</div>
|
||||
<div>{data.count}</div>
|
||||
<div className="flex justify-between gap-8">
|
||||
<div>{number.format(data.count)}</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<PreviousDiffIndicator {...data.previous}>
|
||||
{!!data.previous && `(${data.previous.count})`}
|
||||
</PreviousDiffIndicator>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{hidden.length > 0 && (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import type { IChartData } from '@/app/_trpc/client';
|
||||
import { AutoSizer } from '@/components/AutoSizer';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
@@ -27,7 +28,7 @@ export function ReportHistogramChart({
|
||||
interval,
|
||||
data,
|
||||
}: ReportHistogramChartProps) {
|
||||
const { editMode } = useChartContext();
|
||||
const { editMode, previous } = useChartContext();
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
|
||||
@@ -65,14 +66,25 @@ export function ReportHistogramChart({
|
||||
/>
|
||||
{series.map((serie) => {
|
||||
return (
|
||||
<Bar
|
||||
stackId={serie.index}
|
||||
key={serie.name}
|
||||
name={serie.name}
|
||||
dataKey={`${serie.index}:count`}
|
||||
fill={getChartColor(serie.index)}
|
||||
radius={8}
|
||||
/>
|
||||
<React.Fragment key={serie.name}>
|
||||
{previous && (
|
||||
<Bar
|
||||
key={`${serie.name}:prev`}
|
||||
name={`${serie.name}:prev`}
|
||||
dataKey={`${serie.index}:prev:count`}
|
||||
fill={getChartColor(serie.index)}
|
||||
fillOpacity={0.2}
|
||||
radius={8}
|
||||
/>
|
||||
)}
|
||||
<Bar
|
||||
key={serie.name}
|
||||
name={serie.name}
|
||||
dataKey={`${serie.index}:count`}
|
||||
fill={getChartColor(serie.index)}
|
||||
radius={8}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</BarChart>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import type { IChartData } from '@/app/_trpc/client';
|
||||
import { AutoSizer } from '@/components/AutoSizer';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
@@ -32,7 +32,7 @@ export function ReportLineChart({
|
||||
interval,
|
||||
data,
|
||||
}: ReportLineChartProps) {
|
||||
const { editMode } = useChartContext();
|
||||
const { editMode, previous } = useChartContext();
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
const rechartData = useRechartDataModel(data);
|
||||
@@ -75,15 +75,30 @@ export function ReportLineChart({
|
||||
/>
|
||||
{series.map((serie) => {
|
||||
return (
|
||||
<Line
|
||||
type={lineType}
|
||||
key={serie.name}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
dataKey={`${serie.index}:count`}
|
||||
stroke={getChartColor(serie.index)}
|
||||
name={serie.name}
|
||||
/>
|
||||
<React.Fragment key={serie.name}>
|
||||
<Line
|
||||
type={lineType}
|
||||
key={serie.name}
|
||||
name={serie.name}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
dataKey={`${serie.index}:count`}
|
||||
stroke={getChartColor(serie.index)}
|
||||
/>
|
||||
{previous && (
|
||||
<Line
|
||||
type={lineType}
|
||||
key={`${serie.name}:prev`}
|
||||
name={`${serie.name}:prev`}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
strokeDasharray={'6 6'}
|
||||
dataKey={`${serie.index}:prev:count`}
|
||||
stroke={getChartColor(serie.index)}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</LineChart>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { IChartData } from '@/app/_trpc/client';
|
||||
import { ColorSquare } from '@/components/ColorSquare';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { theme } from '@/utils/theme';
|
||||
import { ChevronDown, ChevronUp, ChevronUpCircle } from 'lucide-react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { Area, AreaChart } from 'recharts';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
|
||||
interface ReportMetricChartProps {
|
||||
@@ -16,6 +19,7 @@ export function ReportMetricChart({ data }: ReportMetricChartProps) {
|
||||
const { editMode } = useChartContext();
|
||||
const { series } = useVisibleSeries(data, editMode ? undefined : 2);
|
||||
const color = theme?.colors['chart-0'];
|
||||
const number = useNumber();
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -29,7 +33,7 @@ export function ReportMetricChart({ data }: ReportMetricChartProps) {
|
||||
className="relative border border-border p-4 rounded-md bg-white overflow-hidden"
|
||||
key={serie.name}
|
||||
>
|
||||
<div className="absolute -top-1 -left-1 -right-1 -bottom-1 z-0 opacity-50">
|
||||
<div className="absolute -top-1 -left-1 -right-1 -bottom-1 z-0 opacity-20">
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<AreaChart
|
||||
@@ -65,10 +69,19 @@ export function ReportMetricChart({ data }: ReportMetricChartProps) {
|
||||
<ColorSquare>{serie.event.id}</ColorSquare>
|
||||
{serie.name ?? serie.event.displayName ?? serie.event.name}
|
||||
</div>
|
||||
<div className="mt-6 font-mono text-4xl font-light">
|
||||
{new Intl.NumberFormat('en', {
|
||||
maximumSignificantDigits: 20,
|
||||
}).format(serie.metrics.sum)}
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="mt-6 font-mono text-3xl font-bold">
|
||||
{number.format(serie.metrics.sum)}
|
||||
</div>
|
||||
{!!serie.metrics.previous.sum && (
|
||||
<div className="flex flex-col items-end">
|
||||
<PreviousDiffIndicator {...serie.metrics.previous.sum}>
|
||||
<div className="font-mono">
|
||||
{number.format(serie.metrics.previous.sum.value)}
|
||||
</div>
|
||||
</PreviousDiffIndicator>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { IChartData } from '@/app/_trpc/client';
|
||||
import { AutoSizer } from '@/components/AutoSizer';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { round } from '@/utils/math';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import { Cell, Pie, PieChart, Tooltip } from 'recharts';
|
||||
|
||||
@@ -14,10 +15,55 @@ interface ReportPieChartProps {
|
||||
data: IChartData;
|
||||
}
|
||||
|
||||
const RADIAN = Math.PI / 180;
|
||||
const renderLabel = ({
|
||||
x,
|
||||
y,
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
payload,
|
||||
...props
|
||||
}: any) => {
|
||||
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
||||
const xx = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
const yy = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||
const label = payload.label;
|
||||
const percent = round(payload.percent * 100, 1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<text
|
||||
x={xx}
|
||||
y={yy}
|
||||
fill="white"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize={12}
|
||||
>
|
||||
{percent}%
|
||||
</text>
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
fill="black"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize={12}
|
||||
>
|
||||
{label}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export function ReportPieChart({ data }: ReportPieChartProps) {
|
||||
const { editMode } = useChartContext();
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
|
||||
const sum = series.reduce((acc, serie) => acc + serie.metrics.sum, 0);
|
||||
// Get max 10 series and than combine others into one
|
||||
const pieData = series.map((serie) => {
|
||||
return {
|
||||
@@ -26,6 +72,7 @@ export function ReportPieChart({ data }: ReportPieChartProps) {
|
||||
index: serie.index,
|
||||
label: serie.name,
|
||||
count: serie.metrics.sum,
|
||||
percent: serie.metrics.sum / sum,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -50,8 +97,9 @@ export function ReportPieChart({ data }: ReportPieChartProps) {
|
||||
dataKey={'count'}
|
||||
data={pieData}
|
||||
innerRadius={height / 4}
|
||||
outerRadius={height / 2 - 20}
|
||||
outerRadius={height / 2.5}
|
||||
isAnimationActive={false}
|
||||
label={renderLabel}
|
||||
>
|
||||
{pieData.map((item) => {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import type { IChartData } from '@/app/_trpc/client';
|
||||
import { Pagination, usePagination } from '@/components/Pagination';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -8,10 +18,12 @@ import {
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useMappings } from '@/hooks/useMappings';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { useSelector } from '@/redux';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
|
||||
interface ReportTableProps {
|
||||
data: IChartData;
|
||||
visibleSeries: IChartData['series'];
|
||||
@@ -23,6 +35,8 @@ export function ReportTable({
|
||||
visibleSeries,
|
||||
setVisibleSeries,
|
||||
}: ReportTableProps) {
|
||||
const pagination = usePagination(50);
|
||||
const number = useNumber();
|
||||
const interval = useSelector((state) => state.report.interval);
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const getLabel = useMappings();
|
||||
@@ -37,117 +51,125 @@ export function ReportTable({
|
||||
});
|
||||
}
|
||||
|
||||
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-medium border-r border-border';
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-fit max-w-full rounded-md border border-border bg-white">
|
||||
{/* Labels */}
|
||||
<div className="border-r border-border">
|
||||
<div className={cn(header, row, cell)}>Name</div>
|
||||
{data.series.map((serie, index) => {
|
||||
const checked = !!visibleSeries.find(
|
||||
(item) => item.name === serie.name
|
||||
);
|
||||
<div className="grid grid-cols-[200px_1fr] border border-border rounded-md overflow-hidden">
|
||||
<Table className="rounded-none border-t-0 border-l-0 border-b-0">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.series
|
||||
.slice(pagination.skip, pagination.skip + pagination.take)
|
||||
.map((serie, index) => {
|
||||
const checked = !!visibleSeries.find(
|
||||
(item) => item.name === serie.name
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={serie.name}
|
||||
className={cn(
|
||||
'flex max-w-[200px] lg:max-w-[400px] xl:max-w-[600px] w-full min-w-full items-center gap-2',
|
||||
row,
|
||||
// avoid using cell since its better on the right side
|
||||
'p-2'
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange(serie.name, !!checked)
|
||||
}
|
||||
style={
|
||||
checked
|
||||
? {
|
||||
background: getChartColor(index),
|
||||
borderColor: getChartColor(index),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
checked={checked}
|
||||
/>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="min-w-0 overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{getLabel(serie.name)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{getLabel(serie.name)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* ScrollView for all values */}
|
||||
<div className="w-full overflow-auto">
|
||||
{/* Header */}
|
||||
<div className={cn('w-max', row)}>
|
||||
<div className={cn(header, value, cell, total)}>Total</div>
|
||||
<div className={cn(header, value, cell, total)}>Average</div>
|
||||
{data.series[0]?.data.map((serie) => (
|
||||
<div
|
||||
key={serie.date.toString()}
|
||||
className={cn(header, value, cell)}
|
||||
>
|
||||
{formatDate(serie.date)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Values */}
|
||||
{data.series.map((serie) => {
|
||||
return (
|
||||
<div className={cn('w-max', row)} key={serie.name}>
|
||||
<div className={cn(header, value, cell, total)}>
|
||||
{serie.metrics.sum}
|
||||
</div>
|
||||
<div className={cn(header, value, cell, total)}>
|
||||
{serie.metrics.average}
|
||||
</div>
|
||||
{serie.data.map((item) => {
|
||||
return (
|
||||
<TableRow key={serie.name}>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange(serie.name, !!checked)
|
||||
}
|
||||
style={
|
||||
checked
|
||||
? {
|
||||
background: getChartColor(index),
|
||||
borderColor: getChartColor(index),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
checked={checked}
|
||||
/>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="min-w-0 overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{getLabel(serie.name)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{getLabel(serie.name)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="overflow-auto">
|
||||
<Table className="rounded-none border-none">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Total</TableHead>
|
||||
<TableHead>Average</TableHead>
|
||||
{data.series[0]?.data.map((serie) => (
|
||||
<TableHead
|
||||
key={serie.date.toString()}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{formatDate(serie.date)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.series
|
||||
.slice(pagination.skip, pagination.skip + pagination.take)
|
||||
.map((serie) => {
|
||||
return (
|
||||
<div key={item.date} className={cn(value, cell)}>
|
||||
{item.count}
|
||||
</div>
|
||||
<TableRow key={serie.name}>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
{number.format(serie.metrics.sum)}
|
||||
<PreviousDiffIndicator
|
||||
{...serie.metrics.previous.sum}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
{number.format(serie.metrics.average)}
|
||||
<PreviousDiffIndicator
|
||||
{...serie.metrics.previous.average}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{serie.data.map((item) => {
|
||||
return (
|
||||
<TableCell
|
||||
className="h-10"
|
||||
key={item.date.toString()}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{number.format(item.count)}
|
||||
<PreviousDiffIndicator {...item.previous} />
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-1">
|
||||
<div>Total</div>
|
||||
<div>{data.metrics.sum}</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<div>Average</div>
|
||||
<div>{data.metrics.averge}</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<div>Min</div>
|
||||
<div>{data.metrics.min}</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<div>Max</div>
|
||||
<div>{data.metrics.max}</div>
|
||||
<div className="flex flex-col-reverse gap-4 md:flex-row md:justify-between md:items-center">
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
<Badge>Total: {number.format(data.metrics.sum)}</Badge>
|
||||
<Badge>Average: {number.format(data.metrics.average)}</Badge>
|
||||
<Badge>Min: {number.format(data.metrics.min)}</Badge>
|
||||
<Badge>Max: {number.format(data.metrics.max)}</Badge>
|
||||
</div>
|
||||
<Pagination {...pagination} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ export const Chart = memo(
|
||||
name,
|
||||
range,
|
||||
lineType,
|
||||
previous,
|
||||
}: ReportChartProps) {
|
||||
const params = useAppParams();
|
||||
const hasEmptyFilters = events.some((event) =>
|
||||
@@ -44,6 +45,7 @@ export const Chart = memo(
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
projectId: params.projectId,
|
||||
previous,
|
||||
},
|
||||
{
|
||||
keepPreviousData: false,
|
||||
|
||||
@@ -36,6 +36,7 @@ const initialState: InitialState = {
|
||||
range: '1m',
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
previous: false,
|
||||
};
|
||||
|
||||
export const reportSlice = createSlice({
|
||||
|
||||
Reference in New Issue
Block a user