182 lines
5.0 KiB
TypeScript
182 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { CartesianGrid, Line, LineChart, XAxis, Bar, BarChart } from "recharts";
|
|
import { CardContent } from "@/components/ui/card";
|
|
import {
|
|
ChartConfig,
|
|
ChartContainer,
|
|
ChartTooltip,
|
|
ChartTooltipContent,
|
|
ChartLegend,
|
|
ChartLegendContent,
|
|
} from "@/components/ui/chart";
|
|
import { formatCurrency, formatNumber } from "@/lib/utils/format";
|
|
import type { SpendingDataPoint, AnalyticsMetric } from "@/lib/types/api";
|
|
|
|
interface AnalyticsChartProps {
|
|
data: SpendingDataPoint[];
|
|
metrics: AnalyticsMetric[];
|
|
chartType: "line" | "bar";
|
|
showDataLabels?: boolean;
|
|
}
|
|
|
|
const METRIC_LABELS: Record<AnalyticsMetric, string> = {
|
|
cost: "Расходы",
|
|
subscriptions: "Подписки",
|
|
views: "Просмотры",
|
|
cpf: "CPF",
|
|
cpm: "CPM",
|
|
};
|
|
|
|
export const AnalyticsChart: React.FC<AnalyticsChartProps> = ({
|
|
data,
|
|
metrics,
|
|
chartType,
|
|
showDataLabels = false,
|
|
}) => {
|
|
// Создаем конфигурацию для chart
|
|
const chartConfig: ChartConfig = metrics.reduce((acc, metric, index) => {
|
|
acc[metric] = {
|
|
label: METRIC_LABELS[metric],
|
|
color: `var(--chart-${index + 1})`,
|
|
};
|
|
return acc;
|
|
}, {} as ChartConfig);
|
|
|
|
const formatValue = (value: number | null, metric: AnalyticsMetric) => {
|
|
if (value === null) return "—";
|
|
if (metric === "cost" || metric === "cpf" || metric === "cpm") {
|
|
return formatCurrency(value);
|
|
}
|
|
return formatNumber(value);
|
|
};
|
|
|
|
if (chartType === "bar") {
|
|
return (
|
|
<CardContent className="px-2 sm:p-6">
|
|
<ChartContainer
|
|
config={chartConfig}
|
|
className="aspect-auto h-[350px] w-full"
|
|
>
|
|
<BarChart
|
|
accessibilityLayer
|
|
data={data}
|
|
margin={{
|
|
left: 12,
|
|
right: 12,
|
|
top: showDataLabels ? 20 : 5,
|
|
}}
|
|
>
|
|
<CartesianGrid vertical={false} />
|
|
<XAxis
|
|
dataKey="period"
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickMargin={8}
|
|
minTickGap={32}
|
|
/>
|
|
<ChartTooltip
|
|
content={
|
|
<ChartTooltipContent
|
|
labelFormatter={(value) => `Период: ${value}`}
|
|
formatter={(value, name) => [
|
|
formatValue(
|
|
value as number,
|
|
name as AnalyticsMetric
|
|
),
|
|
chartConfig[name as AnalyticsMetric]?.label || name,
|
|
]}
|
|
/>
|
|
}
|
|
/>
|
|
<ChartLegend content={<ChartLegendContent />} />
|
|
{metrics.map((metric) => (
|
|
<Bar
|
|
key={metric}
|
|
dataKey={metric}
|
|
fill={chartConfig[metric]?.color}
|
|
radius={4}
|
|
label={
|
|
showDataLabels
|
|
? {
|
|
position: "top",
|
|
formatter: (value: number) =>
|
|
formatValue(value, metric),
|
|
fontSize: 10,
|
|
}
|
|
: undefined
|
|
}
|
|
/>
|
|
))}
|
|
</BarChart>
|
|
</ChartContainer>
|
|
</CardContent>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<CardContent className="px-2 sm:p-6">
|
|
<ChartContainer
|
|
config={chartConfig}
|
|
className="aspect-auto h-[350px] w-full"
|
|
>
|
|
<LineChart
|
|
accessibilityLayer
|
|
data={data}
|
|
margin={{
|
|
left: 12,
|
|
right: 12,
|
|
top: showDataLabels ? 20 : 5,
|
|
}}
|
|
>
|
|
<CartesianGrid vertical={false} />
|
|
<XAxis
|
|
dataKey="period"
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickMargin={8}
|
|
minTickGap={32}
|
|
/>
|
|
<ChartTooltip
|
|
content={
|
|
<ChartTooltipContent
|
|
labelFormatter={(value) => `Период: ${value}`}
|
|
formatter={(value, name) => [
|
|
formatValue(
|
|
value as number,
|
|
name as AnalyticsMetric
|
|
),
|
|
chartConfig[name as AnalyticsMetric]?.label || name,
|
|
]}
|
|
/>
|
|
}
|
|
/>
|
|
<ChartLegend content={<ChartLegendContent />} />
|
|
{metrics.map((metric) => (
|
|
<Line
|
|
key={metric}
|
|
dataKey={metric}
|
|
type="monotone"
|
|
stroke={chartConfig[metric]?.color}
|
|
strokeWidth={2}
|
|
dot={showDataLabels}
|
|
label={
|
|
showDataLabels
|
|
? {
|
|
position: "top",
|
|
formatter: (value: number) =>
|
|
formatValue(value, metric),
|
|
fontSize: 10,
|
|
}
|
|
: undefined
|
|
}
|
|
/>
|
|
))}
|
|
</LineChart>
|
|
</ChartContainer>
|
|
</CardContent>
|
|
);
|
|
};
|
|
|