Some checks failed
CI / Lint & Typecheck (push) Has been cancelled
CI / Test (routes) (push) Has been cancelled
CI / Test (security) (push) Has been cancelled
CI / Test (services) (push) Has been cancelled
CI / Test (unit) (push) Has been cancelled
CI / Test (integration) (push) Has been cancelled
CI / Test Coverage (push) Has been cancelled
CI / Build (push) Has been cancelled
182 lines
4.9 KiB
TypeScript
182 lines
4.9 KiB
TypeScript
import { useMemo } from 'react';
|
|
import Highcharts from 'highcharts';
|
|
import HighchartsReact from 'highcharts-react-official';
|
|
import type { PlayStats } from '@tracearr/shared';
|
|
import { ChartSkeleton } from '@/components/ui/skeleton';
|
|
|
|
interface PlaysChartProps {
|
|
data: PlayStats[] | undefined;
|
|
isLoading?: boolean;
|
|
height?: number;
|
|
period?: 'day' | 'week' | 'month' | 'year' | 'all' | 'custom';
|
|
}
|
|
|
|
export function PlaysChart({ data, isLoading, height = 200, period = 'month' }: PlaysChartProps) {
|
|
const options = useMemo<Highcharts.Options>(() => {
|
|
if (!data || data.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
return {
|
|
chart: {
|
|
type: 'area',
|
|
height,
|
|
backgroundColor: 'transparent',
|
|
style: {
|
|
fontFamily: 'inherit',
|
|
},
|
|
reflow: true,
|
|
},
|
|
title: {
|
|
text: undefined,
|
|
},
|
|
credits: {
|
|
enabled: false,
|
|
},
|
|
legend: {
|
|
enabled: false,
|
|
},
|
|
xAxis: {
|
|
categories: data.map((d) => d.date),
|
|
labels: {
|
|
style: {
|
|
color: 'hsl(var(--muted-foreground))',
|
|
},
|
|
formatter: function () {
|
|
// this.value could be index (number) or category string depending on Highcharts version
|
|
const categories = this.axis.categories;
|
|
const categoryValue = typeof this.value === 'number'
|
|
? categories[this.value]
|
|
: this.value;
|
|
if (!categoryValue) return '';
|
|
const date = new Date(categoryValue);
|
|
if (isNaN(date.getTime())) return '';
|
|
if (period === 'year') {
|
|
// Short month name for yearly view (Dec, Jan, Feb)
|
|
return date.toLocaleDateString('en-US', { month: 'short' });
|
|
}
|
|
// M/D format for week/month views
|
|
return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
},
|
|
step: Math.ceil(data.length / 12), // Show ~12 labels
|
|
},
|
|
lineColor: 'hsl(var(--border))',
|
|
tickColor: 'hsl(var(--border))',
|
|
},
|
|
yAxis: {
|
|
title: {
|
|
text: undefined,
|
|
},
|
|
labels: {
|
|
style: {
|
|
color: 'hsl(var(--muted-foreground))',
|
|
},
|
|
},
|
|
gridLineColor: 'hsl(var(--border))',
|
|
min: 0,
|
|
},
|
|
plotOptions: {
|
|
area: {
|
|
fillColor: {
|
|
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
stops: [
|
|
[0, 'hsl(var(--primary) / 0.3)'],
|
|
[1, 'hsl(var(--primary) / 0.05)'],
|
|
],
|
|
},
|
|
marker: {
|
|
enabled: false,
|
|
states: {
|
|
hover: {
|
|
enabled: true,
|
|
radius: 4,
|
|
},
|
|
},
|
|
},
|
|
lineWidth: 2,
|
|
lineColor: 'hsl(var(--primary))',
|
|
states: {
|
|
hover: {
|
|
lineWidth: 2,
|
|
},
|
|
},
|
|
threshold: null,
|
|
},
|
|
},
|
|
tooltip: {
|
|
backgroundColor: 'hsl(var(--popover))',
|
|
borderColor: 'hsl(var(--border))',
|
|
style: {
|
|
color: 'hsl(var(--popover-foreground))',
|
|
},
|
|
formatter: function () {
|
|
// With categories, this.x is the index. Use this.point.category for the actual value
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const categoryValue = (this as any).point?.category as string | undefined;
|
|
const date = categoryValue ? new Date(categoryValue) : null;
|
|
const dateStr = date && !isNaN(date.getTime())
|
|
? date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
: 'Unknown';
|
|
return `<b>${dateStr}</b><br/>Plays: ${this.y}`;
|
|
},
|
|
},
|
|
series: [
|
|
{
|
|
type: 'area',
|
|
name: 'Plays',
|
|
data: data.map((d) => d.count),
|
|
},
|
|
],
|
|
responsive: {
|
|
rules: [
|
|
{
|
|
condition: {
|
|
maxWidth: 400,
|
|
},
|
|
chartOptions: {
|
|
xAxis: {
|
|
labels: {
|
|
style: {
|
|
fontSize: '9px',
|
|
},
|
|
step: Math.ceil(data.length / 6),
|
|
},
|
|
},
|
|
yAxis: {
|
|
labels: {
|
|
style: {
|
|
fontSize: '9px',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}, [data, height, period]);
|
|
|
|
if (isLoading) {
|
|
return <ChartSkeleton height={height} />;
|
|
}
|
|
|
|
if (!data || data.length === 0) {
|
|
return (
|
|
<div
|
|
className="flex items-center justify-center rounded-lg border border-dashed text-muted-foreground"
|
|
style={{ height }}
|
|
>
|
|
No play data available
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<HighchartsReact
|
|
highcharts={Highcharts}
|
|
options={options}
|
|
containerProps={{ style: { width: '100%', height: '100%' } }}
|
|
/>
|
|
);
|
|
}
|