feat: 添加直播统计可视化页面
This commit is contained in:
+471
@@ -0,0 +1,471 @@
|
||||
let allData = [];
|
||||
let charts = {};
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
let response = await fetch('../data.json');
|
||||
let data = await response.json();
|
||||
allData = data.timeline || [];
|
||||
return allData;
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function parseDate(dateStr) {
|
||||
return new Date(dateStr.replace(' ', 'T'));
|
||||
}
|
||||
|
||||
function getDurationMinutes(startTime, endTime) {
|
||||
let start = parseDate(startTime);
|
||||
let end = parseDate(endTime);
|
||||
return (end - start) / (1000 * 60);
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
let year = date.getFullYear();
|
||||
let month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
let day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function filterDataByDateRange(data, startDate, endDate) {
|
||||
|
||||
if (!startDate && !endDate) return data;
|
||||
|
||||
return data.filter(item => {
|
||||
let itemDate = parseDate(item.startTime);
|
||||
if (startDate && itemDate < new Date(startDate)) return false;
|
||||
if (endDate && itemDate > new Date(endDate + 'T23:59:59')) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function extractContentItems(content) {
|
||||
if (!content) return [];
|
||||
return content.split('/').map(item => item.trim()).filter(item => item);
|
||||
}
|
||||
|
||||
function analyzeContent(data) {
|
||||
|
||||
let contentStats = {};
|
||||
let dailyStats = {};
|
||||
let hourlyStats = {};
|
||||
|
||||
data.forEach(item => {
|
||||
|
||||
let duration = getDurationMinutes(item.startTime, item.endTime);
|
||||
let startDate = parseDate(item.startTime);
|
||||
let endDate = parseDate(item.endTime);
|
||||
let startHour = startDate.getHours();
|
||||
|
||||
let startDay = formatDate(startDate);
|
||||
let endDay = formatDate(endDate);
|
||||
|
||||
if (!dailyStats[startDay]) {
|
||||
dailyStats[startDay] = { duration: 0, count: 0 };
|
||||
}
|
||||
|
||||
dailyStats[startDay].count += 1;
|
||||
|
||||
if (startDay === endDay) {
|
||||
dailyStats[startDay].duration += duration;
|
||||
} else {
|
||||
|
||||
let endOfDay = new Date(startDate);
|
||||
|
||||
endOfDay.setHours(23, 59, 59, 999);
|
||||
|
||||
let firstDayDuration = (endOfDay - startDate) / (1000 * 60);
|
||||
dailyStats[startDay].duration += firstDayDuration;
|
||||
|
||||
if (!dailyStats[endDay]) {
|
||||
dailyStats[endDay] = { duration: 0, count: 0 };
|
||||
}
|
||||
|
||||
dailyStats[endDay].count += 1;
|
||||
|
||||
let startOfNextDay = new Date(startDate);
|
||||
|
||||
startOfNextDay.setDate(startOfNextDay.getDate() + 1);
|
||||
startOfNextDay.setHours(0, 0, 0, 0);
|
||||
|
||||
let secondDayDuration = (endDate - startOfNextDay) / (1000 * 60);
|
||||
|
||||
dailyStats[endDay].duration += secondDayDuration;
|
||||
|
||||
}
|
||||
|
||||
if (!hourlyStats[startHour]) {
|
||||
hourlyStats[startHour] = { duration: 0, count: 0 };
|
||||
}
|
||||
|
||||
hourlyStats[startHour].duration += duration;
|
||||
hourlyStats[startHour].count += 1;
|
||||
|
||||
let contentItems = extractContentItems(item.content);
|
||||
|
||||
contentItems.forEach(content => {
|
||||
if (!contentStats[content]) {
|
||||
contentStats[content] = { duration: 0, count: 0 };
|
||||
}
|
||||
contentStats[content].duration += duration;
|
||||
contentStats[content].count += 1;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return { contentStats, dailyStats, hourlyStats };
|
||||
}
|
||||
|
||||
function initCharts() {
|
||||
|
||||
charts.contentChart = echarts.init(document.getElementById('contentChart'));
|
||||
charts.dailyChart = echarts.init(document.getElementById('dailyChart'));
|
||||
charts.hourlyChart = echarts.init(document.getElementById('hourlyChart'));
|
||||
charts.trendChart = echarts.init(document.getElementById('trendChart'));
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
Object.values(charts).forEach(chart => chart.resize());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function renderContentChart(contentStats) {
|
||||
|
||||
let sortedData = Object.entries(contentStats)
|
||||
.sort((a, b) => b[1].count - a[1].count)
|
||||
.slice(0, 15);
|
||||
|
||||
let option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
formatter: function (params) {
|
||||
let item = params[0];
|
||||
return `${item.name}<br/>次数: ${item.value}`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '次数',
|
||||
axisLabel: {
|
||||
formatter: '{value} 次'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: sortedData.map(d => d[0]),
|
||||
inverse: true,
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
width: 200,
|
||||
overflow: 'truncate'
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: '次数',
|
||||
type: 'bar',
|
||||
data: sortedData.map(d => d[1].count),
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#66BB6A' },
|
||||
{ offset: 1, color: '#43A047' }
|
||||
])
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
charts.contentChart.setOption(option);
|
||||
|
||||
}
|
||||
|
||||
function renderDailyChart(dailyStats) {
|
||||
|
||||
let sortedDates = Object.keys(dailyStats).sort();
|
||||
let counts = sortedDates.map(date => dailyStats[date].count);
|
||||
|
||||
let option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
formatter: function (params) {
|
||||
let date = params[0].name;
|
||||
let count = params[0].value;
|
||||
return `${date}<br/>次数: ${count}`;
|
||||
}
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'slider',
|
||||
show: true,
|
||||
xAxisIndex: [0],
|
||||
start: 0,
|
||||
end: 100
|
||||
},
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0]
|
||||
}
|
||||
],
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: sortedDates,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '次数',
|
||||
axisLabel: {
|
||||
formatter: '{value} 次'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '次数',
|
||||
type: 'bar',
|
||||
data: counts,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#66BB6A' },
|
||||
{ offset: 1, color: '#43A047' }
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
charts.dailyChart.setOption(option);
|
||||
|
||||
}
|
||||
|
||||
function renderHourlyChart(hourlyStats) {
|
||||
|
||||
let hours = Array.from({ length: 24 }, (_, i) => i);
|
||||
let durations = hours.map(h => hourlyStats[h] ? hourlyStats[h].duration : 0);
|
||||
let counts = hours.map(h => hourlyStats[h] ? hourlyStats[h].count : 0);
|
||||
|
||||
let option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross' },
|
||||
formatter: function (params) {
|
||||
|
||||
let item0 = params[0];
|
||||
let item1 = params[1];
|
||||
let hour = item0 ? item0.name : '';
|
||||
let duration = item0 ? item0.value : 0;
|
||||
let count = item1 ? item1.value : 0;
|
||||
let returns = `${hour}:00 - ${hour}:59<br/>时长: ${duration} 小时`;
|
||||
|
||||
if (count) {
|
||||
returns += `<br/>次数: ${count}`;
|
||||
}
|
||||
|
||||
return returns;
|
||||
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['时长(小时)', '次数'],
|
||||
top: '8%'
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: hours.map(h => `${h}:00`),
|
||||
boundaryGap: false
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '时长(小时)',
|
||||
position: 'left',
|
||||
axisLabel: {
|
||||
formatter: '{value}h'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '次数',
|
||||
position: 'right'
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '时长(小时)',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: durations.map(d => (d / 60).toFixed(2)),
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(102, 187, 106, 0.5)' },
|
||||
{ offset: 1, color: 'rgba(102, 187, 106, 0.1)' }
|
||||
])
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#4CAF50'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '次数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
data: counts,
|
||||
itemStyle: {
|
||||
color: '#81C784'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
charts.hourlyChart.setOption(option);
|
||||
|
||||
}
|
||||
|
||||
function renderTrendChart(dailyStats) {
|
||||
|
||||
let sortedDates = Object.keys(dailyStats).sort();
|
||||
let durations = sortedDates.map(date => dailyStats[date].duration);
|
||||
|
||||
let option = {
|
||||
title: {
|
||||
text: '直播时长趋势',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function (params) {
|
||||
let date = params[0].name;
|
||||
let duration = params[0].value;
|
||||
return `${date}<br/>时长: ${duration} 小时`;
|
||||
}
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'slider',
|
||||
show: true,
|
||||
xAxisIndex: [0],
|
||||
start: 0,
|
||||
end: 100
|
||||
},
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0]
|
||||
}
|
||||
],
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: sortedDates,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '时长(小时)',
|
||||
axisLabel: {
|
||||
formatter: '{value}h'
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: '时长',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: durations.map(d => (d / 60).toFixed(2)),
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(102, 187, 106, 0.5)' },
|
||||
{ offset: 1, color: 'rgba(102, 187, 106, 0.1)' }
|
||||
])
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#4CAF50'
|
||||
},
|
||||
}]
|
||||
};
|
||||
|
||||
charts.trendChart.setOption(option);
|
||||
|
||||
}
|
||||
|
||||
function updateStats(data) {
|
||||
|
||||
let totalDuration = data.reduce((sum, item) => sum + getDurationMinutes(item.startTime, item.endTime), 0);
|
||||
let totalHours = (totalDuration / 60).toFixed(1);
|
||||
let totalCount = data.length;
|
||||
|
||||
let uniqueContents = new Set();
|
||||
|
||||
data.forEach(item => {
|
||||
extractContentItems(item.content).forEach(content => uniqueContents.add(content));
|
||||
});
|
||||
|
||||
let avgDuration = totalCount > 0 ? (totalDuration / totalCount / 60).toFixed(1) : 0;
|
||||
|
||||
document.getElementById('totalHours').textContent = totalHours;
|
||||
document.getElementById('totalCount').textContent = totalCount;
|
||||
document.getElementById('uniqueContent').textContent = uniqueContents.size;
|
||||
document.getElementById('avgDuration').textContent = avgDuration;
|
||||
|
||||
}
|
||||
|
||||
function updateCharts(data) {
|
||||
|
||||
let { contentStats, dailyStats, hourlyStats } = analyzeContent(data);
|
||||
|
||||
renderContentChart(contentStats);
|
||||
renderDailyChart(dailyStats);
|
||||
renderHourlyChart(hourlyStats);
|
||||
renderTrendChart(dailyStats);
|
||||
updateStats(data);
|
||||
|
||||
}
|
||||
|
||||
async function init() {
|
||||
|
||||
await loadData();
|
||||
initCharts();
|
||||
updateCharts(allData);
|
||||
|
||||
let startDateInput = document.getElementById('startDate');
|
||||
let endDateInput = document.getElementById('endDate');
|
||||
let filterBtn = document.getElementById('filterBtn');
|
||||
|
||||
filterBtn.addEventListener('click', () => {
|
||||
let startDate = startDateInput.value;
|
||||
let endDate = endDateInput.value;
|
||||
let filteredData = filterDataByDateRange(allData, startDate, endDate);
|
||||
updateCharts(filteredData);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
Reference in New Issue
Block a user