Добавил локально библиотеку chart js. Сделал секретную статистику

This commit is contained in:
2025-12-04 12:35:08 +03:00
parent 027f971f5a
commit f954f77a6d
7 changed files with 1875 additions and 2 deletions

View File

@@ -0,0 +1,287 @@
"""
Секретная страница статистики в стиле Spotify Wrapped / Яндекс.Музыка.
Красивые анимации, диаграммы и визуализации.
"""
import json
from datetime import timedelta, datetime
from collections import defaultdict
from django.db.models import Count, Q, Min, Max, Avg, Sum
from django.db.models.functions import TruncDate, TruncMonth, ExtractWeekDay, ExtractHour
from django.utils import timezone
from django.views.generic import TemplateView
from ..models import ObjItem, Source, Satellite, Geo, Parameter
class SecretStatsView(TemplateView):
"""Секретная страница статистики - итоги года в стиле Spotify Wrapped."""
template_name = 'mainapp/secret_stats.html'
def get_year_range(self):
"""Получает диапазон дат для текущего года."""
now = timezone.now()
year = self.request.GET.get('year', now.year)
try:
year = int(year)
except (ValueError, TypeError):
year = now.year
date_from = datetime(year, 1, 1).date()
date_to = datetime(year, 12, 31).date()
return date_from, date_to, year
def get_base_queryset(self, date_from, date_to):
"""Возвращает базовый queryset ObjItem с фильтрами по дате ГЛ."""
qs = ObjItem.objects.filter(
geo_obj__isnull=False,
geo_obj__timestamp__isnull=False,
geo_obj__timestamp__date__gte=date_from,
geo_obj__timestamp__date__lte=date_to
)
return qs
def get_main_stats(self, date_from, date_to):
"""Основная статистика: точки и объекты."""
base_qs = self.get_base_queryset(date_from, date_to)
total_points = base_qs.count()
total_sources = base_qs.filter(source__isnull=False).values('source').distinct().count()
return {
'total_points': total_points,
'total_sources': total_sources,
}
def get_new_emissions(self, date_from, date_to):
"""
Новые излучения - объекты, у которых имя появилось впервые в выбранном периоде.
"""
# Получаем все имена объектов, которые появились ДО выбранного периода
existing_names = set(
ObjItem.objects.filter(
geo_obj__isnull=False,
geo_obj__timestamp__isnull=False,
geo_obj__timestamp__date__lt=date_from,
name__isnull=False
).exclude(name='').values_list('name', flat=True).distinct()
)
# Базовый queryset для выбранного периода
period_qs = self.get_base_queryset(date_from, date_to).filter(
name__isnull=False
).exclude(name='')
# Получаем уникальные имена в выбранном периоде
period_names = set(period_qs.values_list('name', flat=True).distinct())
# Новые имена = имена в периоде, которых не было раньше
new_names = period_names - existing_names
if not new_names:
return {'count': 0, 'objects': [], 'sources_count': 0}
# Получаем данные о новых объектах
objitems_data = period_qs.filter(
name__in=new_names
).select_related(
'source__info', 'source__ownership'
).values(
'name',
'source__info__name',
'source__ownership__name'
).distinct()
seen_names = set()
new_objects = []
for item in objitems_data:
name = item['name']
if name not in seen_names:
seen_names.add(name)
new_objects.append({
'name': name,
'info': item['source__info__name'] or '-',
'ownership': item['source__ownership__name'] or '-',
})
new_objects.sort(key=lambda x: x['name'])
# Количество источников для новых излучений
new_sources_count = period_qs.filter(
name__in=new_names, source__isnull=False
).values('source').distinct().count()
return {
'count': len(new_names),
'objects': new_objects[:20], # Топ-20 для отображения
'sources_count': new_sources_count
}
def get_satellite_stats(self, date_from, date_to):
"""Статистика по спутникам."""
base_qs = self.get_base_queryset(date_from, date_to)
stats = base_qs.filter(
parameter_obj__id_satellite__isnull=False
).values(
'parameter_obj__id_satellite__id',
'parameter_obj__id_satellite__name'
).annotate(
points_count=Count('id'),
sources_count=Count('source', distinct=True),
unique_names=Count('name', distinct=True)
).order_by('-points_count')
return list(stats)
def get_monthly_stats(self, date_from, date_to):
"""Статистика по месяцам."""
base_qs = self.get_base_queryset(date_from, date_to)
monthly = base_qs.annotate(
month=TruncMonth('geo_obj__timestamp')
).values('month').annotate(
points=Count('id'),
sources=Count('source', distinct=True)
).order_by('month')
return list(monthly)
def get_weekday_stats(self, date_from, date_to):
"""Статистика по дням недели."""
base_qs = self.get_base_queryset(date_from, date_to)
weekday = base_qs.annotate(
weekday=ExtractWeekDay('geo_obj__timestamp')
).values('weekday').annotate(
points=Count('id')
).order_by('weekday')
return list(weekday)
def get_hourly_stats(self, date_from, date_to):
"""Статистика по часам."""
base_qs = self.get_base_queryset(date_from, date_to)
hourly = base_qs.annotate(
hour=ExtractHour('geo_obj__timestamp')
).values('hour').annotate(
points=Count('id')
).order_by('hour')
return list(hourly)
def get_top_objects(self, date_from, date_to):
"""Топ объектов по количеству точек."""
base_qs = self.get_base_queryset(date_from, date_to)
top = base_qs.filter(
name__isnull=False
).exclude(name='').values('name').annotate(
points=Count('id')
).order_by('-points')[:10]
return list(top)
def get_busiest_day(self, date_from, date_to):
"""Самый активный день."""
base_qs = self.get_base_queryset(date_from, date_to)
daily = base_qs.annotate(
date=TruncDate('geo_obj__timestamp')
).values('date').annotate(
points=Count('id')
).order_by('-points').first()
return daily
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
date_from, date_to, year = self.get_year_range()
# Основная статистика
main_stats = self.get_main_stats(date_from, date_to)
# Новые излучения
new_emissions = self.get_new_emissions(date_from, date_to)
# Статистика по спутникам
satellite_stats = self.get_satellite_stats(date_from, date_to)
# Статистика по месяцам
monthly_stats = self.get_monthly_stats(date_from, date_to)
# Статистика по дням недели
weekday_stats = self.get_weekday_stats(date_from, date_to)
# Статистика по часам
hourly_stats = self.get_hourly_stats(date_from, date_to)
# Топ объектов
top_objects = self.get_top_objects(date_from, date_to)
# Самый активный день
busiest_day = self.get_busiest_day(date_from, date_to)
# Доступные годы для выбора
years_with_data = ObjItem.objects.filter(
geo_obj__isnull=False,
geo_obj__timestamp__isnull=False
).dates('geo_obj__timestamp', 'year')
available_years = sorted([d.year for d in years_with_data], reverse=True)
# JSON данные для графиков
monthly_data_json = json.dumps([
{
'month': item['month'].strftime('%Y-%m') if item['month'] else None,
'month_name': item['month'].strftime('%B') if item['month'] else None,
'points': item['points'],
'sources': item['sources'],
}
for item in monthly_stats
])
satellite_stats_json = json.dumps(satellite_stats)
weekday_names = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб']
weekday_data_json = json.dumps([
{
'weekday': item['weekday'],
'weekday_name': weekday_names[item['weekday'] - 1] if item['weekday'] else '',
'points': item['points'],
}
for item in weekday_stats
])
hourly_data_json = json.dumps([
{
'hour': item['hour'],
'points': item['points'],
}
for item in hourly_stats
])
top_objects_json = json.dumps(top_objects)
context.update({
'year': year,
'available_years': available_years,
'total_points': main_stats['total_points'],
'total_sources': main_stats['total_sources'],
'new_emissions_count': new_emissions['count'],
'new_emissions_sources': new_emissions['sources_count'],
'new_emission_objects': new_emissions['objects'],
'satellite_stats': satellite_stats[:10], # Топ-10
'satellite_count': len(satellite_stats),
'busiest_day': busiest_day,
'monthly_data_json': monthly_data_json,
'satellite_stats_json': satellite_stats_json,
'weekday_data_json': weekday_data_json,
'hourly_data_json': hourly_data_json,
'top_objects_json': top_objects_json,
})
return context