conversion wip
This commit is contained in:
@@ -2,9 +2,10 @@ import { pushModal } from '@/modals';
|
||||
import type { RouterOutputs } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import { useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
CartesianGrid,
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
ReferenceLine,
|
||||
@@ -13,16 +14,25 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
|
||||
import { createChartTooltip } from '@/components/charts/chart-tooltip';
|
||||
import {
|
||||
ChartTooltipHeader,
|
||||
ChartTooltipItem,
|
||||
createChartTooltip,
|
||||
} from '@/components/charts/chart-tooltip';
|
||||
import { useConversionRechartDataModel } from '@/hooks/use-conversion-rechart-data-model';
|
||||
import { useFormatDateInterval } from '@/hooks/use-format-date-interval';
|
||||
import { useNumber } from '@/hooks/use-numer-formatter';
|
||||
import { useVisibleConversionSeries } from '@/hooks/use-visible-conversion-series';
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { average, getPreviousMetric, round } from '@openpanel/common';
|
||||
import type { IInterval } from '@openpanel/validation';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useXAxisProps, useYAxisProps } from '../common/axis';
|
||||
import { PreviousDiffIndicatorPure } from '../common/previous-diff-indicator';
|
||||
import { PreviousDiffIndicator } from '../common/previous-diff-indicator';
|
||||
import { SerieIcon } from '../common/serie-icon';
|
||||
import { SerieName } from '../common/serie-name';
|
||||
import { useReportChartContext } from '../context';
|
||||
import { ConversionTable } from './conversion-table';
|
||||
|
||||
interface Props {
|
||||
data: RouterOutputs['chart']['conversion'];
|
||||
@@ -34,7 +44,8 @@ export function Chart({ data }: Props) {
|
||||
isEditMode,
|
||||
options: { hideXAxis, hideYAxis, maxDomain },
|
||||
} = useReportChartContext();
|
||||
const dataLength = data.current.length || 0;
|
||||
const { series, setVisibleSeries } = useVisibleConversionSeries(data, 5);
|
||||
const rechartData = useConversionRechartDataModel(series);
|
||||
const trpc = useTRPC();
|
||||
const references = useQuery(
|
||||
trpc.reference.getChartReferences.queryOptions(
|
||||
@@ -56,18 +67,11 @@ export function Chart({ data }: Props) {
|
||||
});
|
||||
|
||||
const averageConversionRate = average(
|
||||
data.current.map((serie) => {
|
||||
series.map((serie) => {
|
||||
return average(serie.data.map((item) => item.rate));
|
||||
}, 0),
|
||||
);
|
||||
|
||||
const rechartData = data.current[0].data.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
timestamp: new Date(item.date).getTime(),
|
||||
};
|
||||
});
|
||||
|
||||
const handleChartClick = useCallback((e: any) => {
|
||||
if (e?.activePayload?.[0]) {
|
||||
const clickedData = e.activePayload[0].payload;
|
||||
@@ -79,8 +83,36 @@ export function Chart({ data }: Props) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const CustomLegend = useCallback(() => {
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1 text-xs mt-4 -mb-2">
|
||||
{series.map((serie) => (
|
||||
<div
|
||||
className="flex items-center gap-1"
|
||||
key={serie.id}
|
||||
style={{
|
||||
color: getChartColor(serie.index),
|
||||
}}
|
||||
>
|
||||
<SerieIcon name={serie.breakdowns} />
|
||||
<SerieName
|
||||
name={
|
||||
serie.breakdowns.length > 0 ? serie.breakdowns : ['Conversion']
|
||||
}
|
||||
className="font-semibold"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}, [series]);
|
||||
|
||||
return (
|
||||
<TooltipProvider conversion={data} interval={interval}>
|
||||
<TooltipProvider
|
||||
conversion={data}
|
||||
interval={interval}
|
||||
visibleSeries={series}
|
||||
>
|
||||
<div className={cn('h-full w-full', isEditMode && 'card p-4')}>
|
||||
<ResponsiveContainer>
|
||||
<LineChart data={rechartData} onClick={handleChartClick}>
|
||||
@@ -107,35 +139,48 @@ export function Chart({ data }: Props) {
|
||||
))}
|
||||
<YAxis {...yAxisProps} domain={[0, 100]} />
|
||||
<XAxis {...xAxisProps} allowDuplicatedCategory={false} />
|
||||
{series.length > 1 && <Legend content={<CustomLegend />} />}
|
||||
<Tooltip />
|
||||
<Line
|
||||
dot={false}
|
||||
dataKey="previousRate"
|
||||
stroke={getChartColor(0)}
|
||||
type={lineType}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={0.5}
|
||||
/>
|
||||
<Line
|
||||
dataKey="rate"
|
||||
stroke={getChartColor(0)}
|
||||
type={lineType}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
{series.map((serie) => {
|
||||
const color = getChartColor(serie.index);
|
||||
return (
|
||||
<Line
|
||||
key={`${serie.id}:previousRate`}
|
||||
dot={false}
|
||||
dataKey={`${serie.id}:previousRate`}
|
||||
stroke={color}
|
||||
type={lineType}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={0.3}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{series.map((serie) => {
|
||||
const color = getChartColor(serie.index);
|
||||
return (
|
||||
<Line
|
||||
key={`${serie.id}:rate`}
|
||||
dataKey={`${serie.id}:rate`}
|
||||
stroke={color}
|
||||
type={lineType}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{typeof averageConversionRate === 'number' &&
|
||||
averageConversionRate && (
|
||||
<ReferenceLine
|
||||
y={averageConversionRate}
|
||||
stroke={getChartColor(1)}
|
||||
stroke={getChartColor(series.length)}
|
||||
strokeWidth={2}
|
||||
strokeDasharray="3 3"
|
||||
strokeOpacity={0.5}
|
||||
strokeLinecap="round"
|
||||
label={{
|
||||
value: `Average (${round(averageConversionRate, 2)} %)`,
|
||||
fill: getChartColor(1),
|
||||
fill: getChartColor(series.length),
|
||||
position: 'insideBottomRight',
|
||||
fontSize: 12,
|
||||
}}
|
||||
@@ -144,72 +189,92 @@ export function Chart({ data }: Props) {
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<ConversionTable
|
||||
data={data}
|
||||
visibleSeries={series}
|
||||
setVisibleSeries={setVisibleSeries}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const { Tooltip, TooltipProvider } = createChartTooltip<
|
||||
NonNullable<
|
||||
RouterOutputs['chart']['conversion']['current'][number]
|
||||
>['data'][number],
|
||||
Record<string, any>,
|
||||
{
|
||||
conversion: RouterOutputs['chart']['conversion'];
|
||||
interval: IInterval;
|
||||
visibleSeries: RouterOutputs['chart']['conversion']['current'];
|
||||
}
|
||||
>(({ data, context }) => {
|
||||
if (!data[0]) {
|
||||
if (!data || !data[0]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { date } = data[0];
|
||||
const payload = data[0];
|
||||
const { date } = payload;
|
||||
const formatDate = useFormatDateInterval({
|
||||
interval: context.interval,
|
||||
short: false,
|
||||
});
|
||||
const number = useNumber();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between gap-8 text-muted-foreground">
|
||||
<div>{formatDate(date)}</div>
|
||||
</div>
|
||||
{context.conversion.current.map((serie, index) => {
|
||||
const item = data[index];
|
||||
if (!item) {
|
||||
{context.visibleSeries.map((serie, index) => {
|
||||
const rate = payload[`${serie.id}:rate`];
|
||||
const total = payload[`${serie.id}:total`];
|
||||
const previousRate = payload[`${serie.id}:previousRate`];
|
||||
|
||||
if (rate === undefined) {
|
||||
return null;
|
||||
}
|
||||
const prevItem = context.conversion?.previous?.[0]?.data[item.index];
|
||||
|
||||
const title =
|
||||
serie.breakdowns.length > 0
|
||||
? (serie.breakdowns.join(',') ?? 'Not set')
|
||||
: 'Conversion';
|
||||
const prevSerie = context.conversion?.previous?.find(
|
||||
(p) => p.id === serie.id,
|
||||
);
|
||||
const prevItem = prevSerie?.data.find((d) => d.date === date);
|
||||
const previousMetric = getPreviousMetric(rate, previousRate);
|
||||
|
||||
return (
|
||||
<div className="row gap-2" key={serie.id}>
|
||||
<div
|
||||
className="w-[3px] rounded-full"
|
||||
style={{ background: getChartColor(index) }}
|
||||
/>
|
||||
<div className="col flex-1 gap-1">
|
||||
<div className="flex items-center gap-1">{title}</div>
|
||||
<React.Fragment key={serie.id}>
|
||||
{index === 0 && (
|
||||
<ChartTooltipHeader>
|
||||
<div>{formatDate(date)}</div>
|
||||
</ChartTooltipHeader>
|
||||
)}
|
||||
<ChartTooltipItem color={getChartColor(index)}>
|
||||
<div className="flex items-center gap-1">
|
||||
<SerieIcon
|
||||
name={
|
||||
serie.breakdowns.length > 0
|
||||
? serie.breakdowns
|
||||
: ['Conversion']
|
||||
}
|
||||
/>
|
||||
<SerieName
|
||||
name={
|
||||
serie.breakdowns.length > 0
|
||||
? serie.breakdowns
|
||||
: ['Conversion']
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between gap-8 font-mono font-medium">
|
||||
<div className="col gap-1">
|
||||
<span>{number.formatWithUnit(item.rate / 100, '%')}</span>
|
||||
<span>{item.total}</span>
|
||||
</div>
|
||||
|
||||
{!!prevItem && (
|
||||
<div className="col gap-1">
|
||||
<PreviousDiffIndicatorPure
|
||||
{...getPreviousMetric(item.rate, prevItem?.rate)}
|
||||
/>
|
||||
<div className="row gap-1">
|
||||
<span>{number.formatWithUnit(rate / 100, '%')}</span>
|
||||
<span className="text-muted-foreground">({total})</span>
|
||||
{prevItem && previousRate !== undefined && (
|
||||
<span className="text-muted-foreground">
|
||||
({prevItem?.total})
|
||||
({number.formatWithUnit(previousRate / 100, '%')})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{previousRate !== undefined && (
|
||||
<PreviousDiffIndicator {...previousMetric} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ChartTooltipItem>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { useFormatDateInterval } from '@/hooks/use-format-date-interval';
|
||||
import { useNumber } from '@/hooks/use-numer-formatter';
|
||||
import { useSelector } from '@/redux';
|
||||
import type { RouterOutputs } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import { getPreviousMetric } from '@openpanel/common';
|
||||
import { useMemo } from 'react';
|
||||
import { PreviousDiffIndicatorPure } from '../common/previous-diff-indicator';
|
||||
import { ReportTableToolbar } from '../common/report-table-toolbar';
|
||||
import { SerieIcon } from '../common/serie-icon';
|
||||
import { SerieName } from '../common/serie-name';
|
||||
|
||||
interface ConversionTableProps {
|
||||
data: RouterOutputs['chart']['conversion'];
|
||||
visibleSeries: RouterOutputs['chart']['conversion']['current'];
|
||||
setVisibleSeries: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
export function ConversionTable({
|
||||
data,
|
||||
visibleSeries,
|
||||
setVisibleSeries,
|
||||
}: ConversionTableProps) {
|
||||
const number = useNumber();
|
||||
const interval = useSelector((state) => state.report.interval);
|
||||
const formatDate = useFormatDateInterval({
|
||||
interval,
|
||||
short: true,
|
||||
});
|
||||
|
||||
// Get all unique dates from the first series
|
||||
const dates = useMemo(
|
||||
() => data.current[0]?.data.map((item) => item.date) ?? [],
|
||||
[data.current],
|
||||
);
|
||||
|
||||
// Get all series (including non-visible ones for toggle functionality)
|
||||
const allSeries = data.current;
|
||||
|
||||
// Transform data to table rows with memoization
|
||||
const rows = useMemo(() => {
|
||||
return allSeries.map((serie) => {
|
||||
const dateValues: Record<string, number> = {};
|
||||
dates.forEach((date) => {
|
||||
const item = serie.data.find((d) => d.date === date);
|
||||
dateValues[date] = item?.rate ?? 0;
|
||||
});
|
||||
|
||||
const total = serie.data.reduce((sum, item) => sum + item.total, 0);
|
||||
const conversions = serie.data.reduce(
|
||||
(sum, item) => sum + item.conversions,
|
||||
0,
|
||||
);
|
||||
const avgRate =
|
||||
serie.data.length > 0
|
||||
? serie.data.reduce((sum, item) => sum + item.rate, 0) /
|
||||
serie.data.length
|
||||
: 0;
|
||||
|
||||
const prevSerie = data.previous?.find((p) => p.id === serie.id);
|
||||
const prevAvgRate =
|
||||
prevSerie && prevSerie.data.length > 0
|
||||
? prevSerie.data.reduce((sum, item) => sum + item.rate, 0) /
|
||||
prevSerie.data.length
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: serie.id,
|
||||
serieId: serie.id,
|
||||
serieName:
|
||||
serie.breakdowns.length > 0 ? serie.breakdowns : ['Conversion'],
|
||||
breakdownValues: serie.breakdowns,
|
||||
avgRate,
|
||||
prevAvgRate,
|
||||
total,
|
||||
conversions,
|
||||
dateValues,
|
||||
};
|
||||
});
|
||||
}, [allSeries, dates, data.previous]);
|
||||
|
||||
// Calculate ranges for color visualization (memoized)
|
||||
const { metricRanges, dateRanges } = useMemo(() => {
|
||||
const metricRanges: Record<string, { min: number; max: number }> = {
|
||||
avgRate: {
|
||||
min: Number.POSITIVE_INFINITY,
|
||||
max: Number.NEGATIVE_INFINITY,
|
||||
},
|
||||
total: {
|
||||
min: Number.POSITIVE_INFINITY,
|
||||
max: Number.NEGATIVE_INFINITY,
|
||||
},
|
||||
conversions: {
|
||||
min: Number.POSITIVE_INFINITY,
|
||||
max: Number.NEGATIVE_INFINITY,
|
||||
},
|
||||
};
|
||||
|
||||
const dateRanges: Record<string, { min: number; max: number }> = {};
|
||||
dates.forEach((date) => {
|
||||
dateRanges[date] = {
|
||||
min: Number.POSITIVE_INFINITY,
|
||||
max: Number.NEGATIVE_INFINITY,
|
||||
};
|
||||
});
|
||||
|
||||
rows.forEach((row) => {
|
||||
// Metric ranges
|
||||
metricRanges.avgRate.min = Math.min(
|
||||
metricRanges.avgRate.min,
|
||||
row.avgRate,
|
||||
);
|
||||
metricRanges.avgRate.max = Math.max(
|
||||
metricRanges.avgRate.max,
|
||||
row.avgRate,
|
||||
);
|
||||
metricRanges.total.min = Math.min(metricRanges.total.min, row.total);
|
||||
metricRanges.total.max = Math.max(metricRanges.total.max, row.total);
|
||||
metricRanges.conversions.min = Math.min(
|
||||
metricRanges.conversions.min,
|
||||
row.conversions,
|
||||
);
|
||||
metricRanges.conversions.max = Math.max(
|
||||
metricRanges.conversions.max,
|
||||
row.conversions,
|
||||
);
|
||||
|
||||
// Date ranges
|
||||
dates.forEach((date) => {
|
||||
const value = row.dateValues[date] ?? 0;
|
||||
dateRanges[date]!.min = Math.min(dateRanges[date]!.min, value);
|
||||
dateRanges[date]!.max = Math.max(dateRanges[date]!.max, value);
|
||||
});
|
||||
});
|
||||
|
||||
return { metricRanges, dateRanges };
|
||||
}, [rows, dates]);
|
||||
|
||||
// Helper to get background color style
|
||||
const getCellBackgroundStyle = (
|
||||
value: number,
|
||||
min: number,
|
||||
max: number,
|
||||
colorClass: 'purple' | 'emerald' = 'emerald',
|
||||
): React.CSSProperties => {
|
||||
if (value === 0 || max === min) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const percentage = (value - min) / (max - min);
|
||||
const opacity = Math.max(0.05, Math.min(1, percentage));
|
||||
|
||||
const backgroundColor =
|
||||
colorClass === 'purple'
|
||||
? `rgba(168, 85, 247, ${opacity})`
|
||||
: `rgba(16, 185, 129, ${opacity})`;
|
||||
|
||||
return { backgroundColor };
|
||||
};
|
||||
|
||||
const visibleSeriesIds = useMemo(
|
||||
() => visibleSeries.map((s) => s.id),
|
||||
[visibleSeries],
|
||||
);
|
||||
|
||||
const getSerieIndex = (serieId: string): number => {
|
||||
return allSeries.findIndex((s) => s.id === serieId);
|
||||
};
|
||||
|
||||
const toggleSerieVisibility = (serieId: string) => {
|
||||
setVisibleSeries((prev) => {
|
||||
if (prev.includes(serieId)) {
|
||||
return prev.filter((id) => id !== serieId);
|
||||
}
|
||||
return [...prev, serieId];
|
||||
});
|
||||
};
|
||||
|
||||
if (allSeries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col border rounded-lg overflow-hidden bg-card mt-8">
|
||||
<ReportTableToolbar
|
||||
grouped={false}
|
||||
onToggleGrouped={() => {}}
|
||||
search=""
|
||||
onSearchChange={() => {}}
|
||||
onUnselectAll={() => setVisibleSeries([])}
|
||||
/>
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full align-middle">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted/30 border-b sticky top-0 z-10">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 text-[10px] uppercase font-semibold sticky left-0 bg-muted/30 z-20 min-w-[200px] border-r">
|
||||
Serie
|
||||
</th>
|
||||
<th className="text-right px-4 py-3 text-[10px] uppercase font-semibold min-w-[100px]">
|
||||
Avg Rate
|
||||
</th>
|
||||
<th className="text-right px-4 py-3 text-[10px] uppercase font-semibold min-w-[100px]">
|
||||
Total
|
||||
</th>
|
||||
<th className="text-right px-4 py-3 text-[10px] uppercase font-semibold min-w-[100px]">
|
||||
Conversions
|
||||
</th>
|
||||
{dates.map((date) => (
|
||||
<th
|
||||
key={date}
|
||||
className="text-right px-4 py-3 text-[10px] uppercase font-semibold min-w-[100px]"
|
||||
>
|
||||
{formatDate(date)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => {
|
||||
const isVisible = visibleSeriesIds.includes(row.serieId);
|
||||
const serieIndex = getSerieIndex(row.serieId);
|
||||
const color = getChartColor(serieIndex);
|
||||
const previousMetric =
|
||||
row.prevAvgRate !== undefined
|
||||
? getPreviousMetric(row.avgRate, row.prevAvgRate)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={cn(
|
||||
'border-b hover:bg-muted/30 transition-colors',
|
||||
!isVisible && 'opacity-50',
|
||||
)}
|
||||
>
|
||||
<td className="px-4 py-3 sticky left-0 bg-card z-10 border-r">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={isVisible}
|
||||
onCheckedChange={() =>
|
||||
toggleSerieVisibility(row.serieId)
|
||||
}
|
||||
style={{
|
||||
borderColor: color,
|
||||
backgroundColor: isVisible ? color : 'transparent',
|
||||
}}
|
||||
className="h-4 w-4 shrink-0"
|
||||
/>
|
||||
<div
|
||||
className="w-[3px] rounded-full shrink-0"
|
||||
style={{ background: color }}
|
||||
/>
|
||||
<SerieIcon name={row.serieName} />
|
||||
<SerieName name={row.serieName} className="truncate" />
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className="px-4 py-3 text-right font-mono text-sm"
|
||||
style={getCellBackgroundStyle(
|
||||
row.avgRate,
|
||||
metricRanges.avgRate.min,
|
||||
metricRanges.avgRate.max,
|
||||
'purple',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<span>
|
||||
{number.formatWithUnit(row.avgRate / 100, '%')}
|
||||
</span>
|
||||
{previousMetric && (
|
||||
<PreviousDiffIndicatorPure {...previousMetric} />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className="px-4 py-3 text-right font-mono text-sm"
|
||||
style={getCellBackgroundStyle(
|
||||
row.total,
|
||||
metricRanges.total.min,
|
||||
metricRanges.total.max,
|
||||
'purple',
|
||||
)}
|
||||
>
|
||||
{number.format(row.total)}
|
||||
</td>
|
||||
<td
|
||||
className="px-4 py-3 text-right font-mono text-sm"
|
||||
style={getCellBackgroundStyle(
|
||||
row.conversions,
|
||||
metricRanges.conversions.min,
|
||||
metricRanges.conversions.max,
|
||||
'purple',
|
||||
)}
|
||||
>
|
||||
{number.format(row.conversions)}
|
||||
</td>
|
||||
{dates.map((date) => {
|
||||
const value = row.dateValues[date] ?? 0;
|
||||
return (
|
||||
<td
|
||||
key={date}
|
||||
className="px-4 py-3 text-right font-mono text-sm"
|
||||
style={getCellBackgroundStyle(
|
||||
value,
|
||||
dateRanges[date]!.min,
|
||||
dateRanges[date]!.max,
|
||||
'emerald',
|
||||
)}
|
||||
>
|
||||
{number.formatWithUnit(value / 100, '%')}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
apps/start/src/hooks/use-conversion-rechart-data-model.ts
Normal file
44
apps/start/src/hooks/use-conversion-rechart-data-model.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { RouterOutputs } from '@/trpc/client';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useConversionRechartDataModel(
|
||||
series: RouterOutputs['chart']['conversion']['current'],
|
||||
) {
|
||||
return useMemo(() => {
|
||||
if (!series.length || !series[0]?.data.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get all unique dates from the first series (all series should have same dates)
|
||||
const dates = series[0].data.map((item) => item.date);
|
||||
|
||||
return dates.map((date) => {
|
||||
const baseItem = series[0].data.find((item) => item.date === date);
|
||||
if (!baseItem) {
|
||||
return {
|
||||
date,
|
||||
timestamp: new Date(date).getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
// Build data object with all series values
|
||||
const dataPoint: Record<string, any> = {
|
||||
date,
|
||||
timestamp: new Date(date).getTime(),
|
||||
};
|
||||
|
||||
series.forEach((serie) => {
|
||||
const item = serie.data.find((d) => d.date === date);
|
||||
if (item) {
|
||||
dataPoint[`${serie.id}:rate`] = item.rate;
|
||||
dataPoint[`${serie.id}:previousRate`] = item.previousRate;
|
||||
dataPoint[`${serie.id}:total`] = item.total;
|
||||
dataPoint[`${serie.id}:conversions`] = item.conversions;
|
||||
}
|
||||
});
|
||||
|
||||
return dataPoint;
|
||||
});
|
||||
}, [series]);
|
||||
}
|
||||
|
||||
35
apps/start/src/hooks/use-visible-conversion-series.ts
Normal file
35
apps/start/src/hooks/use-visible-conversion-series.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { RouterOutputs } from '@/trpc/client';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export type IVisibleConversionSeries = ReturnType<
|
||||
typeof useVisibleConversionSeries
|
||||
>['series'];
|
||||
|
||||
export function useVisibleConversionSeries(
|
||||
data: RouterOutputs['chart']['conversion'],
|
||||
limit?: number | undefined,
|
||||
) {
|
||||
const max = limit ?? 5;
|
||||
const [visibleSeries, setVisibleSeries] = useState<string[]>(
|
||||
data?.current?.slice(0, max).map((serie) => serie.id) ?? [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleSeries(
|
||||
data?.current?.slice(0, max).map((serie) => serie.id) ?? [],
|
||||
);
|
||||
}, [data, max]);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
series: data.current
|
||||
.map((serie, index) => ({
|
||||
...serie,
|
||||
index,
|
||||
}))
|
||||
.filter((serie) => visibleSeries.includes(serie.id)),
|
||||
setVisibleSeries,
|
||||
} as const;
|
||||
}, [visibleSeries, data.current]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user