import React, { useEffect, useRef, useState } from 'react';
import VChart from '@visactor/vchart';
import type { IVChartOption } from '@visactor/vchart';
// TikTok brand colors - Standardized with other approaches
export const TIKTOK_COLORS = {
primary: '#3370FF', // Lark Blue (was #1890ff)
secondary: '#FF3B69', // TikTok Pink (was #ff4d4f)
success: '#52c41a',
warning: '#faad14',
info: '#13c2c2',
gradient: ['#3370FF', '#36cfc9', '#73d13d', '#ffec3d', '#FF3B69']
};
export interface TikTokVideoData {
videoId: string;
title: string;
views: number;
likes: number;
comments: number;
shares: number;
watchPercent: number;
datePublished: string;
duration: number;
}
export interface VChartComponentProps {
data: TikTokVideoData[];
chartType: 'line' | 'bar' | 'pie' | 'dashboard';
width?: number | string;
height?: number | string;
onError?: (error: Error) => void;
className?: string;
}
export interface ChartContainerProps extends Omit<VChartComponentProps, 'chartType'> {
spec: IVChartOption;
title?: string;
}
/**
* Base chart container component
*/
export const ChartContainer: React.FC<ChartContainerProps> = ({
data,
spec,
width = '100%',
height = 400,
onError,
className = '',
title
}) => {
const chartRef = useRef<HTMLDivElement>(null);
const chartInstanceRef = useRef<VChart | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!chartRef.current || data.length === 0) {
setLoading(false);
return;
}
try {
// Clean up previous chart instance
if (chartInstanceRef.current) {
chartInstanceRef.current.release();
chartInstanceRef.current = null;
}
// Create new chart instance
const vchart = new VChart(spec, {
dom: chartRef.current,
mode: 'desktop-browser'
});
vchart.renderSync();
chartInstanceRef.current = vchart;
setLoading(false);
setError(null);
} catch (err) {
const error = err instanceof Error ? err : new Error('Chart rendering failed');
setError(error);
setLoading(false);
onError?.(error);
}
return () => {
if (chartInstanceRef.current) {
chartInstanceRef.current.release();
chartInstanceRef.current = null;
}
};
}, [data, spec, onError]);
// Handle responsive resize
useEffect(() => {
if (!chartInstanceRef.current) return;
const handleResize = () => {
if (chartInstanceRef.current && chartRef.current) {
const { offsetWidth, offsetHeight } = chartRef.current;
chartInstanceRef.current.resize(offsetWidth, offsetHeight);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
if (error) {
return (
<div className={`vchart-error ${className}`} style={{ padding: '20px', textAlign: 'center' }}>
<p style={{ color: TIKTOK_COLORS.secondary }}>Failed to render chart: {error.message}</p>
</div>
);
}
if (data.length === 0) {
return (
<div className={`vchart-empty ${className}`} style={{ padding: '20px', textAlign: 'center' }}>
<p style={{ color: '#999' }}>No data available</p>
</div>
);
}
return (
<div className={`vchart-container ${className}`} style={{ width, height: typeof height === 'number' ? `${height}px` : height }}>
{title && <h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: 600 }}>{title}</h3>}
{loading && (
<div style={{ padding: '20px', textAlign: 'center' }}>
<p>Loading chart...</p>
</div>
)}
<div ref={chartRef} style={{ width: '100%', height: '100%' }} />
</div>
);
};
/**
* Main VChart component for TikTok analytics
*/
export const VChartComponent: React.FC<VChartComponentProps> = ({
data,
chartType,
width = '100%',
height = 400,
onError,
className = ''
}) => {
// Import specs dynamically based on chart type
const [spec, setSpec] = useState<IVChartOption | null>(null);
useEffect(() => {
const loadSpec = async () => {
try {
let specModule;
switch (chartType) {
case 'line':
specModule = await import('./specs/line-chart');
setSpec(specModule.createLineChartSpec(data));
break;
case 'bar':
specModule = await import('./specs/bar-chart');
setSpec(specModule.createBarChartSpec(data));
break;
case 'pie':
specModule = await import('./specs/pie-chart');
setSpec(specModule.createPieChartSpec(data));
break;
case 'dashboard':
specModule = await import('./specs/dashboard');
setSpec(specModule.createDashboardSpec(data));
break;
}
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to load chart spec');
onError?.(error);
}
};
loadSpec();
}, [data, chartType, onError]);
if (!spec) {
return (
<div className={`vchart-loading ${className}`} style={{ padding: '20px', textAlign: 'center' }}>
<p>Loading chart specification...</p>
</div>
);
}
return (
<ChartContainer
data={data}
spec={spec}
width={width}
height={height}
onError={onError}
className={className}
/>
);
};
export default VChartComponent;