<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TikTok Analytics VChart Demo - Standalone</title>
<!-- VChart CDN -->
<script src="https://unpkg.com/@visactor/vchart@latest/dist/index.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 32px;
margin-bottom: 10px;
}
.header p {
font-size: 16px;
opacity: 0.9;
}
.controls {
padding: 20px 30px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
display: flex;
gap: 20px;
flex-wrap: wrap;
align-items: center;
}
.control-group {
display: flex;
align-items: center;
gap: 10px;
}
.control-group label {
font-weight: 600;
color: #495057;
}
.control-group select,
.control-group input,
.control-group button {
padding: 8px 16px;
border: 1px solid #ced4da;
border-radius: 6px;
font-size: 14px;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.control-group select:hover,
.control-group input:hover {
border-color: #667eea;
}
.control-group button {
background: #667eea;
color: white;
border: none;
font-weight: 600;
}
.control-group button:hover {
background: #5568d3;
transform: translateY(-1px);
}
.control-group button:active {
transform: translateY(0);
}
.stats {
padding: 20px 30px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-card .label {
font-size: 12px;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.stat-card .value {
font-size: 24px;
font-weight: 700;
color: #212529;
}
.chart-container {
padding: 30px;
}
.chart-wrapper {
background: white;
border-radius: 8px;
padding: 20px;
min-height: 500px;
position: relative;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 500px;
flex-direction: column;
gap: 20px;
}
.loading .spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
color: #dc3545;
padding: 20px;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 6px;
margin: 20px;
}
.footer {
padding: 20px 30px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
text-align: center;
color: #6c757d;
font-size: 14px;
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
align-items: stretch;
}
.control-group {
flex-direction: column;
align-items: stretch;
}
.stats {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎵 TikTok Analytics Dashboard</h1>
<p>Powered by VChart - Standalone Demo (No Build Required)</p>
</div>
<div class="controls">
<div class="control-group">
<label for="chartType">Chart Type:</label>
<select id="chartType">
<option value="dashboard">Dashboard</option>
<option value="line">Line Chart</option>
<option value="bar">Bar Chart</option>
<option value="pie">Pie Chart</option>
</select>
</div>
<div class="control-group">
<label for="dataSource">Data Source:</label>
<select id="dataSource">
<option value="sample">Sample Data</option>
<option value="api">Lark API (MCP Proxy)</option>
</select>
</div>
<div class="control-group">
<button id="refreshBtn">🔄 Refresh</button>
<button id="exportBtn">📥 Export PNG</button>
</div>
</div>
<div class="stats" id="stats">
<div class="stat-card">
<div class="label">Total Videos</div>
<div class="value" id="totalVideos">-</div>
</div>
<div class="stat-card">
<div class="label">Total Views</div>
<div class="value" id="totalViews">-</div>
</div>
<div class="stat-card">
<div class="label">Total Likes</div>
<div class="value" id="totalLikes">-</div>
</div>
<div class="stat-card">
<div class="label">Avg Watch %</div>
<div class="value" id="avgWatch">-</div>
</div>
</div>
<div class="chart-container">
<div class="chart-wrapper">
<div id="chart"></div>
</div>
</div>
<div class="footer">
<p>VChart Component Demo • Approach C • Base: C8kmbTsqoa6rBesTKRpl8nV8gHd • Table: tblG4uuUvbwfvI9Z</p>
</div>
</div>
<script>
// Configuration
const CONFIG = {
APP_TOKEN: 'C8kmbTsqoa6rBesTKRpl8nV8gHd',
TABLE_ID: 'tblG4uuUvbwfvI9Z',
MCP_PROXY_URL: 'http://localhost:3000' // Adjust as needed
};
// TikTok colors - Standardized with other approaches
const 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']
};
// Sample data
const SAMPLE_DATA = [
{
videoId: '7409731702890827041',
title: 'Amazing Dance Tutorial #1',
views: 2500000,
likes: 180000,
comments: 12000,
shares: 8500,
watchPercent: 78.5,
datePublished: '2025-01-15',
duration: 45
},
{
videoId: '7409731702890827042',
title: 'Cooking Made Easy',
views: 1800000,
likes: 120000,
comments: 8000,
shares: 5200,
watchPercent: 82.3,
datePublished: '2025-01-16',
duration: 60
},
{
videoId: '7409731702890827043',
title: 'Tech Review: Latest Gadgets',
views: 3200000,
likes: 250000,
comments: 18000,
shares: 12000,
watchPercent: 75.2,
datePublished: '2025-01-17',
duration: 90
},
{
videoId: '7409731702890827044',
title: 'Fitness Motivation',
views: 2100000,
likes: 150000,
comments: 9500,
shares: 6800,
watchPercent: 80.1,
datePublished: '2025-01-18',
duration: 55
},
{
videoId: '7409731702890827045',
title: 'Travel Vlog: Japan',
views: 2800000,
likes: 200000,
comments: 15000,
shares: 10000,
watchPercent: 77.8,
datePublished: '2025-01-19',
duration: 120
}
];
// Global state
let currentChart = null;
let currentData = SAMPLE_DATA;
// Utility functions
function formatNumber(value) {
if (value >= 1000000) return (value / 1000000).toFixed(1) + 'M';
if (value >= 1000) return (value / 1000).toFixed(1) + 'K';
return value.toString();
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
// Chart specs
function createLineChartSpec(data) {
const sortedData = [...data].sort((a, b) =>
new Date(a.datePublished).getTime() - new Date(b.datePublished).getTime()
);
const chartData = sortedData.map(item => ({
date: formatDate(item.datePublished),
views: item.views
}));
return {
type: 'line',
data: { values: chartData },
title: { visible: true, text: 'Views Over Time' },
xField: 'date',
yField: 'views',
point: { visible: true, style: { size: 6, fill: COLORS.primary } },
line: { style: { stroke: COLORS.primary, lineWidth: 3 } },
axes: [
{
orient: 'left',
label: { formatMethod: (val) => formatNumber(val) }
},
{ orient: 'bottom' }
]
};
}
function createBarChartSpec(data) {
const top10 = [...data]
.sort((a, b) => b.views - a.views)
.slice(0, 10)
.reverse();
const chartData = top10.map(item => ({
title: item.title.length > 30 ? item.title.substring(0, 30) + '...' : item.title,
views: item.views
}));
return {
type: 'bar',
data: { values: chartData },
title: { visible: true, text: 'Top 10 Videos by Views' },
xField: 'views',
yField: 'title',
direction: 'horizontal',
bar: {
style: {
fill: {
gradient: 'linear',
x0: 0, y0: 0, x1: 1, y1: 0,
stops: [
{ offset: 0, color: COLORS.primary },
{ offset: 1, color: COLORS.info }
]
}
}
},
axes: [
{
orient: 'bottom',
label: { formatMethod: (val) => formatNumber(val) }
},
{ orient: 'left' }
]
};
}
function createPieChartSpec(data) {
const totalLikes = data.reduce((sum, item) => sum + item.likes, 0);
const totalComments = data.reduce((sum, item) => sum + item.comments, 0);
const totalShares = data.reduce((sum, item) => sum + item.shares, 0);
const chartData = [
{ type: 'Likes', value: totalLikes },
{ type: 'Comments', value: totalComments },
{ type: 'Shares', value: totalShares }
];
return {
type: 'pie',
data: { values: chartData },
title: { visible: true, text: 'Engagement Breakdown' },
categoryField: 'type',
valueField: 'value',
radius: 0.8,
innerRadius: 0.5,
label: { visible: true },
legends: [{ visible: true, orient: 'bottom' }]
};
}
function createDashboardSpec(data) {
const sortedData = [...data].sort((a, b) =>
new Date(a.datePublished).getTime() - new Date(b.datePublished).getTime()
);
return {
type: 'common',
layout: {
type: 'grid',
col: 2,
row: 2,
elements: [
{ modelId: 'line', col: 0, row: 0, colSpan: 2 },
{ modelId: 'bar', col: 0, row: 1 },
{ modelId: 'pie', col: 1, row: 1 }
]
},
region: [
{ id: 'lineRegion' },
{ id: 'barRegion' },
{ id: 'pieRegion' }
],
series: [
{
id: 'line',
regionId: 'lineRegion',
type: 'line',
data: {
values: sortedData.map(d => ({
date: formatDate(d.datePublished),
views: d.views
}))
},
xField: 'date',
yField: 'views',
line: { style: { stroke: COLORS.primary } }
},
{
id: 'bar',
regionId: 'barRegion',
type: 'bar',
data: {
values: [...data].sort((a, b) => b.views - a.views).slice(0, 5).reverse()
.map(d => ({ title: d.title.substring(0, 20) + '...', views: d.views }))
},
xField: 'views',
yField: 'title',
direction: 'horizontal'
},
{
id: 'pie',
regionId: 'pieRegion',
type: 'pie',
data: {
values: [
{ type: 'Likes', value: data.reduce((s, d) => s + d.likes, 0) },
{ type: 'Comments', value: data.reduce((s, d) => s + d.comments, 0) },
{ type: 'Shares', value: data.reduce((s, d) => s + d.shares, 0) }
]
},
categoryField: 'type',
valueField: 'value'
}
],
title: { visible: true, text: 'TikTok Analytics Dashboard' }
};
}
// Update statistics
function updateStats(data) {
document.getElementById('totalVideos').textContent = data.length;
document.getElementById('totalViews').textContent = formatNumber(
data.reduce((sum, d) => sum + d.views, 0)
);
document.getElementById('totalLikes').textContent = formatNumber(
data.reduce((sum, d) => sum + d.likes, 0)
);
const avgWatch = data.reduce((sum, d) => sum + d.watchPercent, 0) / data.length;
document.getElementById('avgWatch').textContent = avgWatch.toFixed(1) + '%';
}
// Render chart
function renderChart(chartType, data) {
// Clear previous chart
if (currentChart) {
currentChart.release();
currentChart = null;
}
// Get spec based on type
let spec;
switch (chartType) {
case 'line':
spec = createLineChartSpec(data);
break;
case 'bar':
spec = createBarChartSpec(data);
break;
case 'pie':
spec = createPieChartSpec(data);
break;
case 'dashboard':
spec = createDashboardSpec(data);
break;
}
// Create chart
const chartContainer = document.getElementById('chart');
chartContainer.innerHTML = '';
currentChart = new VChart.default(spec, {
dom: chartContainer,
mode: 'desktop-browser'
});
currentChart.renderSync();
updateStats(data);
}
// Fetch data from API
async function fetchDataFromAPI() {
try {
const response = await fetch(`${CONFIG.MCP_PROXY_URL}/bitable/records`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
app_token: CONFIG.APP_TOKEN,
table_id: CONFIG.TABLE_ID
})
});
if (!response.ok) {
throw new Error('API request failed');
}
const result = await response.json();
return transformRecords(result.data.items);
} catch (error) {
console.error('Failed to fetch from API:', error);
alert('Failed to fetch data from API. Using sample data instead.');
return SAMPLE_DATA;
}
}
function transformRecords(records) {
return records.map(record => ({
videoId: record.fields['Unique identifier of the video'] || '',
title: record.fields['Video description'] || 'Untitled',
views: parseFloat(record.fields['Total video views']) || 0,
likes: parseFloat(record.fields['Total number of likes the video received']) || 0,
comments: parseFloat(record.fields['Total number of comments the video received']) || 0,
shares: parseFloat(record.fields['Total number of times the video was shared']) || 0,
watchPercent: (parseFloat(record.fields['Percentage of video watched completely']) || 0) * 100,
datePublished: new Date(record.fields['Date and time the video was published']).toISOString(),
duration: parseFloat(record.fields['Video duration in seconds, rounded to three decimal places']) || 0
}));
}
// Event listeners
document.getElementById('chartType').addEventListener('change', (e) => {
renderChart(e.target.value, currentData);
});
document.getElementById('dataSource').addEventListener('change', async (e) => {
if (e.target.value === 'api') {
currentData = await fetchDataFromAPI();
} else {
currentData = SAMPLE_DATA;
}
const chartType = document.getElementById('chartType').value;
renderChart(chartType, currentData);
});
document.getElementById('refreshBtn').addEventListener('click', async () => {
const dataSource = document.getElementById('dataSource').value;
if (dataSource === 'api') {
currentData = await fetchDataFromAPI();
}
const chartType = document.getElementById('chartType').value;
renderChart(chartType, currentData);
});
document.getElementById('exportBtn').addEventListener('click', async () => {
if (currentChart) {
const imageData = await currentChart.exportImg('png');
const link = document.createElement('a');
link.href = imageData;
link.download = 'tiktok-analytics.png';
link.click();
}
});
// Initial render
renderChart('dashboard', currentData);
</script>
</body>
</html>