Добавил локально библиотеку chart js. Сделал секретную статистику
This commit is contained in:
971
dbapp/mainapp/templates/mainapp/secret_stats.html
Normal file
971
dbapp/mainapp/templates/mainapp/secret_stats.html
Normal file
@@ -0,0 +1,971 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🎉 Итоги {{ year }} года</title>
|
||||
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'bootstrap-icons/bootstrap-icons.css' %}" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700;900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--gradient-2: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
--gradient-3: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
--gradient-4: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
--gradient-5: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||
--gradient-6: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||
--dark-bg: #0d1117;
|
||||
--card-bg: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
background: var(--dark-bg);
|
||||
color: #fff;
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.particles {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
animation: float 15s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
100% { transform: translateY(-100vh) rotate(720deg); opacity: 0; }
|
||||
}
|
||||
|
||||
.slide {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.slide-intro { background: var(--gradient-1); }
|
||||
.slide-points { background: var(--gradient-2); }
|
||||
.slide-new { background: var(--gradient-3); }
|
||||
.slide-satellites { background: var(--gradient-4); }
|
||||
.slide-time { background: var(--gradient-5); }
|
||||
.slide-summary { background: var(--gradient-1); }
|
||||
|
||||
.big-number {
|
||||
font-size: clamp(4rem, 15vw, 12rem);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
text-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
animation: popIn 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
.big-text {
|
||||
font-size: clamp(1.5rem, 4vw, 3rem);
|
||||
font-weight: 700;
|
||||
text-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
animation: slideUp 0.6s ease-out 0.3s forwards;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-size: clamp(1rem, 2vw, 1.5rem);
|
||||
font-weight: 400;
|
||||
opacity: 0.9;
|
||||
margin-top: 10px;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.6s ease-out 0.5s forwards;
|
||||
}
|
||||
|
||||
@keyframes popIn {
|
||||
0% { opacity: 0; transform: scale(0.5); }
|
||||
70% { transform: scale(1.1); }
|
||||
100% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to { opacity: 0.9; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 24px;
|
||||
padding: 30px;
|
||||
margin: 15px;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
animation: cardSlideUp 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-10px) scale(1.02);
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
@keyframes cardSlideUp {
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.stat-card:nth-child(1) { animation-delay: 0.2s; }
|
||||
.stat-card:nth-child(2) { animation-delay: 0.4s; }
|
||||
.stat-card:nth-child(3) { animation-delay: 0.6s; }
|
||||
.stat-card:nth-child(4) { animation-delay: 0.8s; }
|
||||
|
||||
.stat-value {
|
||||
font-size: 3rem;
|
||||
font-weight: 900;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.satellite-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
max-width: 1200px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.satellite-item {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
padding: 20px 30px;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
animation: satelliteIn 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.satellite-item:hover {
|
||||
transform: scale(1.1);
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
@keyframes satelliteIn {
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.satellite-item:nth-child(1) { animation-delay: 0.1s; }
|
||||
.satellite-item:nth-child(2) { animation-delay: 0.2s; }
|
||||
.satellite-item:nth-child(3) { animation-delay: 0.3s; }
|
||||
.satellite-item:nth-child(4) { animation-delay: 0.4s; }
|
||||
.satellite-item:nth-child(5) { animation-delay: 0.5s; }
|
||||
.satellite-item:nth-child(6) { animation-delay: 0.6s; }
|
||||
.satellite-item:nth-child(7) { animation-delay: 0.7s; }
|
||||
.satellite-item:nth-child(8) { animation-delay: 0.8s; }
|
||||
.satellite-item:nth-child(9) { animation-delay: 0.9s; }
|
||||
.satellite-item:nth-child(10) { animation-delay: 1.0s; }
|
||||
|
||||
.satellite-name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.satellite-stats {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 24px;
|
||||
padding: 30px;
|
||||
margin: 20px;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.8s ease-out 0.5s forwards;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.year-selector {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
padding: 10px 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.year-selector select {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.year-selector select option {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.scroll-indicator {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% { transform: translateX(-50%) translateY(0); }
|
||||
40% { transform: translateX(-50%) translateY(-20px); }
|
||||
60% { transform: translateX(-50%) translateY(-10px); }
|
||||
}
|
||||
|
||||
.scroll-indicator i {
|
||||
font-size: 2rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.emoji-rain {
|
||||
position: fixed;
|
||||
top: -50px;
|
||||
font-size: 2rem;
|
||||
animation: rain 3s linear forwards;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@keyframes rain {
|
||||
to { transform: translateY(110vh) rotate(360deg); }
|
||||
}
|
||||
|
||||
.glow-text {
|
||||
text-shadow: 0 0 10px currentColor, 0 0 20px currentColor, 0 0 30px currentColor;
|
||||
}
|
||||
|
||||
.counter {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.progress-bar-custom {
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
overflow: hidden;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: linear-gradient(90deg, #fff 0%, rgba(255,255,255,0.7) 100%);
|
||||
transition: width 1.5s ease-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 15px;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.new-emissions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
max-width: 1200px;
|
||||
margin-top: 30px;
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.emission-card {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
padding: 15px 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
animation: slideRight 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideRight {
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
.emission-name {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.emission-info {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.confetti {
|
||||
position: fixed;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: -10px;
|
||||
z-index: 1000;
|
||||
animation: confetti-fall 3s linear forwards;
|
||||
}
|
||||
|
||||
@keyframes confetti-fall {
|
||||
0% { transform: translateY(0) rotate(0deg); opacity: 1; }
|
||||
100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
|
||||
}
|
||||
|
||||
.heatmap-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.heatmap-cell {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.heatmap-cell:hover {
|
||||
transform: scale(1.2);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.nav-dots {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-dot:hover, .nav-dot.active {
|
||||
background: #fff;
|
||||
transform: scale(1.3);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.big-number { font-size: 4rem; }
|
||||
.big-text { font-size: 1.5rem; }
|
||||
.stat-card { padding: 20px; margin: 10px; }
|
||||
.stat-value { font-size: 2rem; }
|
||||
.nav-dots { display: none; }
|
||||
.year-selector { top: 10px; right: 10px; padding: 8px 15px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Particles Background -->
|
||||
<div class="particles" id="particles"></div>
|
||||
|
||||
<!-- Year Selector -->
|
||||
<div class="year-selector">
|
||||
<select id="yearSelect" onchange="changeYear(this.value)">
|
||||
{% for y in available_years %}
|
||||
<option value="{{ y }}" {% if y == year %}selected{% endif %}>{{ y }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Dots -->
|
||||
<div class="nav-dots">
|
||||
<div class="nav-dot active" data-slide="0" title="Начало"></div>
|
||||
<div class="nav-dot" data-slide="1" title="Точки ГЛ"></div>
|
||||
<div class="nav-dot" data-slide="2" title="Новые излучения"></div>
|
||||
<div class="nav-dot" data-slide="3" title="Спутники"></div>
|
||||
<div class="nav-dot" data-slide="4" title="Время"></div>
|
||||
<div class="nav-dot" data-slide="5" title="Итоги"></div>
|
||||
</div>
|
||||
|
||||
<!-- Scroll Indicator -->
|
||||
<div class="scroll-indicator" id="scrollIndicator">
|
||||
<i class="bi bi-chevron-double-down"></i>
|
||||
</div>
|
||||
|
||||
<!-- Slide 1: Intro -->
|
||||
<section class="slide slide-intro" data-slide="0">
|
||||
<div class="text-center">
|
||||
<div class="big-text" style="animation-delay: 0s;">🎉 Ваш {{ year }} год</div>
|
||||
<div class="big-number" style="animation-delay: 0.3s;">в цифрах</div>
|
||||
<div class="sub-text" style="animation-delay: 0.6s;">Итоги работы системы геолокации</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Slide 2: Total Points -->
|
||||
<section class="slide slide-points" data-slide="1">
|
||||
<div class="text-center">
|
||||
<div class="sub-text">За {{ year }} год вы получили</div>
|
||||
<div class="big-number counter" data-target="{{ total_points }}">0</div>
|
||||
<div class="big-text">точек геолокации</div>
|
||||
<div class="sub-text">по <span class="counter" data-target="{{ total_sources }}">0</span> объектам</div>
|
||||
|
||||
{% if busiest_day %}
|
||||
<div class="stat-card mt-5" style="display: inline-block;">
|
||||
<div class="stat-label">🔥 Самый активный день</div>
|
||||
<div class="stat-value">{{ busiest_day.date|date:"d.m.Y" }}</div>
|
||||
<div class="stat-label">{{ busiest_day.points }} точек</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Slide 3: New Emissions -->
|
||||
<section class="slide slide-new" data-slide="2">
|
||||
<div class="text-center">
|
||||
<div class="sub-text">✨ Новые открытия</div>
|
||||
<div class="big-number counter" data-target="{{ new_emissions_count }}">0</div>
|
||||
<div class="big-text">новых излучений</div>
|
||||
<div class="sub-text">впервые обнаруженных в {{ year }} году</div>
|
||||
<div class="sub-text">по <span class="counter" data-target="{{ new_emissions_sources }}">0</span> объектам</div>
|
||||
|
||||
{% if new_emission_objects %}
|
||||
<div class="new-emissions-grid">
|
||||
{% for obj in new_emission_objects %}
|
||||
<div class="emission-card" style="animation-delay: {{ forloop.counter0|divisibleby:10 }}s;">
|
||||
<div class="emission-name">{{ obj.name }}</div>
|
||||
<div class="emission-info">{{ obj.info }} • {{ obj.ownership }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Slide 4: Satellites -->
|
||||
<section class="slide slide-satellites" data-slide="3">
|
||||
<div class="text-center">
|
||||
<div class="sub-text">📡 Спутники</div>
|
||||
<div class="big-number counter" data-target="{{ satellite_count }}">0</div>
|
||||
<div class="big-text">спутников с данными</div>
|
||||
|
||||
<div class="satellite-list">
|
||||
{% for sat in satellite_stats %}
|
||||
<div class="satellite-item">
|
||||
<div class="satellite-name">{{ sat.parameter_obj__id_satellite__name }}</div>
|
||||
<div class="satellite-stats">
|
||||
<strong>{{ sat.points_count }}</strong> точек •
|
||||
<strong>{{ sat.sources_count }}</strong> объектов
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="chart-container mt-4">
|
||||
<div class="chart-title">Распределение точек по спутникам</div>
|
||||
<canvas id="satelliteChart" height="300"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Slide 5: Time Analysis -->
|
||||
<section class="slide slide-time" data-slide="4">
|
||||
<div class="text-center">
|
||||
<div class="sub-text">⏰ Когда вы работали</div>
|
||||
<div class="big-text">Анализ по времени</div>
|
||||
|
||||
<div class="row justify-content-center mt-4">
|
||||
<div class="col-md-5">
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">По месяцам</div>
|
||||
<canvas id="monthlyChart" height="250"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">По дням недели</div>
|
||||
<canvas id="weekdayChart" height="250"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container" style="max-width: 600px;">
|
||||
<div class="chart-title">По часам</div>
|
||||
<canvas id="hourlyChart" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Slide 6: Summary -->
|
||||
<section class="slide slide-summary" data-slide="5">
|
||||
<div class="text-center">
|
||||
<div class="big-text">🏆 Итоги {{ year }}</div>
|
||||
|
||||
<div class="row justify-content-center mt-4">
|
||||
<div class="col-auto">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value glow-text">{{ total_points }}</div>
|
||||
<div class="stat-label">Точек ГЛ</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value glow-text">{{ total_sources }}</div>
|
||||
<div class="stat-label">Объектов</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value glow-text">{{ new_emissions_count }}</div>
|
||||
<div class="stat-label">Новых излучений</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value glow-text">{{ satellite_count }}</div>
|
||||
<div class="stat-label">Спутников</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container mt-4" style="max-width: 700px;">
|
||||
<div class="chart-title">🌟 Топ-10 объектов по количеству точек</div>
|
||||
<canvas id="topObjectsChart" height="300"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<div class="big-text">До встречи в {{ year|add:1 }}! 🚀</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="{% static 'chartjs/chart.js' %}"></script>
|
||||
<script src="{% static 'chartjs/chart-datalabels.js' %}"></script>
|
||||
<script>
|
||||
// Data from Django
|
||||
const monthlyData = {{ monthly_data_json|safe }};
|
||||
const satelliteStats = {{ satellite_stats_json|safe }};
|
||||
const weekdayData = {{ weekday_data_json|safe }};
|
||||
const hourlyData = {{ hourly_data_json|safe }};
|
||||
const topObjects = {{ top_objects_json|safe }};
|
||||
|
||||
// Create particles
|
||||
function createParticles() {
|
||||
const container = document.getElementById('particles');
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const particle = document.createElement('div');
|
||||
particle.className = 'particle';
|
||||
particle.style.left = Math.random() * 100 + '%';
|
||||
particle.style.animationDelay = Math.random() * 15 + 's';
|
||||
particle.style.animationDuration = (10 + Math.random() * 10) + 's';
|
||||
particle.style.width = (5 + Math.random() * 10) + 'px';
|
||||
particle.style.height = particle.style.width;
|
||||
container.appendChild(particle);
|
||||
}
|
||||
}
|
||||
createParticles();
|
||||
|
||||
// Counter animation
|
||||
function animateCounters() {
|
||||
const counters = document.querySelectorAll('.counter');
|
||||
counters.forEach(counter => {
|
||||
const target = parseInt(counter.dataset.target) || 0;
|
||||
const duration = 2000;
|
||||
const step = target / (duration / 16);
|
||||
let current = 0;
|
||||
|
||||
const updateCounter = () => {
|
||||
current += step;
|
||||
if (current < target) {
|
||||
counter.textContent = Math.floor(current).toLocaleString('ru-RU');
|
||||
requestAnimationFrame(updateCounter);
|
||||
} else {
|
||||
counter.textContent = target.toLocaleString('ru-RU');
|
||||
}
|
||||
};
|
||||
updateCounter();
|
||||
});
|
||||
}
|
||||
|
||||
// Intersection Observer for animations
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const slideIndex = entry.target.dataset.slide;
|
||||
document.querySelectorAll('.nav-dot').forEach((dot, i) => {
|
||||
dot.classList.toggle('active', i == slideIndex);
|
||||
});
|
||||
|
||||
// Trigger counter animation when slide is visible
|
||||
if (slideIndex == 1 || slideIndex == 2 || slideIndex == 3 || slideIndex == 5) {
|
||||
entry.target.querySelectorAll('.counter').forEach(counter => {
|
||||
if (!counter.dataset.animated) {
|
||||
counter.dataset.animated = 'true';
|
||||
const target = parseInt(counter.dataset.target) || 0;
|
||||
animateCounter(counter, target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create confetti on summary slide
|
||||
if (slideIndex == 5 && !window.confettiCreated) {
|
||||
window.confettiCreated = true;
|
||||
createConfetti();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.5 });
|
||||
|
||||
document.querySelectorAll('.slide').forEach(slide => observer.observe(slide));
|
||||
|
||||
function animateCounter(element, target) {
|
||||
const duration = 2000;
|
||||
const step = target / (duration / 16);
|
||||
let current = 0;
|
||||
|
||||
const update = () => {
|
||||
current += step;
|
||||
if (current < target) {
|
||||
element.textContent = Math.floor(current).toLocaleString('ru-RU');
|
||||
requestAnimationFrame(update);
|
||||
} else {
|
||||
element.textContent = target.toLocaleString('ru-RU');
|
||||
}
|
||||
};
|
||||
update();
|
||||
}
|
||||
|
||||
// Confetti effect
|
||||
function createConfetti() {
|
||||
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dfe6e9', '#fd79a8', '#a29bfe'];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
setTimeout(() => {
|
||||
const confetti = document.createElement('div');
|
||||
confetti.className = 'confetti';
|
||||
confetti.style.left = Math.random() * 100 + '%';
|
||||
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
confetti.style.animationDuration = (2 + Math.random() * 2) + 's';
|
||||
confetti.style.borderRadius = Math.random() > 0.5 ? '50%' : '0';
|
||||
document.body.appendChild(confetti);
|
||||
setTimeout(() => confetti.remove(), 4000);
|
||||
}, i * 30);
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation dots click
|
||||
document.querySelectorAll('.nav-dot').forEach(dot => {
|
||||
dot.addEventListener('click', () => {
|
||||
const slideIndex = dot.dataset.slide;
|
||||
document.querySelector(`[data-slide="${slideIndex}"]`).scrollIntoView({ behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
|
||||
// Hide scroll indicator on scroll
|
||||
window.addEventListener('scroll', () => {
|
||||
const indicator = document.getElementById('scrollIndicator');
|
||||
if (window.scrollY > 100) {
|
||||
indicator.style.opacity = '0';
|
||||
} else {
|
||||
indicator.style.opacity = '1';
|
||||
}
|
||||
});
|
||||
|
||||
// Year change
|
||||
function changeYear(year) {
|
||||
window.location.href = '?year=' + year;
|
||||
}
|
||||
|
||||
// Chart.js configuration
|
||||
Chart.defaults.color = '#fff';
|
||||
Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)';
|
||||
|
||||
// Monthly Chart
|
||||
if (monthlyData.length > 0) {
|
||||
const monthNames = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'];
|
||||
new Chart(document.getElementById('monthlyChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: monthlyData.map(d => {
|
||||
if (d.month) {
|
||||
const [year, month] = d.month.split('-');
|
||||
return monthNames[parseInt(month) - 1];
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
datasets: [{
|
||||
label: 'Точки',
|
||||
data: monthlyData.map(d => d.points),
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
||||
borderRadius: 8,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
datalabels: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.1)' } },
|
||||
x: { grid: { display: false } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Weekday Chart
|
||||
if (weekdayData.length > 0) {
|
||||
const weekdayNames = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
|
||||
const sortedWeekday = [...weekdayData].sort((a, b) => {
|
||||
// Convert Sunday (1) to 7 for proper sorting (Mon-Sun)
|
||||
const aDay = a.weekday === 1 ? 8 : a.weekday;
|
||||
const bDay = b.weekday === 1 ? 8 : b.weekday;
|
||||
return aDay - bDay;
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('weekdayChart'), {
|
||||
type: 'polarArea',
|
||||
data: {
|
||||
labels: sortedWeekday.map(d => weekdayNames[d.weekday - 1]),
|
||||
datasets: [{
|
||||
data: sortedWeekday.map(d => d.points),
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.7)',
|
||||
'rgba(54, 162, 235, 0.7)',
|
||||
'rgba(255, 206, 86, 0.7)',
|
||||
'rgba(75, 192, 192, 0.7)',
|
||||
'rgba(153, 102, 255, 0.7)',
|
||||
'rgba(255, 159, 64, 0.7)',
|
||||
'rgba(199, 199, 199, 0.7)'
|
||||
],
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { position: 'right' },
|
||||
datalabels: { display: false }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Hourly Chart
|
||||
if (hourlyData.length > 0) {
|
||||
// Fill missing hours with 0
|
||||
const fullHourlyData = Array.from({length: 24}, (_, i) => {
|
||||
const found = hourlyData.find(d => d.hour === i);
|
||||
return found ? found.points : 0;
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('hourlyChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: Array.from({length: 24}, (_, i) => i + ':00'),
|
||||
datasets: [{
|
||||
label: 'Точки',
|
||||
data: fullHourlyData,
|
||||
borderColor: 'rgba(255, 255, 255, 0.9)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#fff'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
datalabels: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.1)' } },
|
||||
x: { grid: { display: false } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Satellite Pie Chart
|
||||
if (satelliteStats.length > 0) {
|
||||
const top10 = satelliteStats.slice(0, 10);
|
||||
const otherPoints = satelliteStats.slice(10).reduce((sum, s) => sum + s.points_count, 0);
|
||||
|
||||
const labels = top10.map(s => s.parameter_obj__id_satellite__name);
|
||||
const data = top10.map(s => s.points_count);
|
||||
|
||||
if (otherPoints > 0) {
|
||||
labels.push('Другие');
|
||||
data.push(otherPoints);
|
||||
}
|
||||
|
||||
const colors = [
|
||||
'#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7',
|
||||
'#dfe6e9', '#fd79a8', '#a29bfe', '#00b894', '#e17055', '#636e72'
|
||||
];
|
||||
|
||||
new Chart(document.getElementById('satelliteChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: data,
|
||||
backgroundColor: colors.slice(0, data.length),
|
||||
borderWidth: 3,
|
||||
borderColor: 'rgba(255,255,255,0.3)'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
cutout: '60%',
|
||||
plugins: {
|
||||
legend: { position: 'right', labels: { padding: 15 } },
|
||||
datalabels: {
|
||||
color: '#fff',
|
||||
font: { weight: 'bold', size: 11 },
|
||||
formatter: (value, ctx) => {
|
||||
const total = ctx.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const pct = ((value / total) * 100).toFixed(1);
|
||||
return pct > 5 ? pct + '%' : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [ChartDataLabels]
|
||||
});
|
||||
}
|
||||
|
||||
// Top Objects Chart
|
||||
if (topObjects.length > 0) {
|
||||
const colors = [
|
||||
'#ffd700', '#c0c0c0', '#cd7f32', '#4ecdc4', '#45b7d1',
|
||||
'#96ceb4', '#ffeaa7', '#fd79a8', '#a29bfe', '#00b894'
|
||||
];
|
||||
|
||||
new Chart(document.getElementById('topObjectsChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: topObjects.map(o => o.name.length > 20 ? o.name.substring(0, 20) + '...' : o.name),
|
||||
datasets: [{
|
||||
label: 'Точки',
|
||||
data: topObjects.map(o => o.points),
|
||||
backgroundColor: colors,
|
||||
borderRadius: 8,
|
||||
borderSkipped: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
datalabels: {
|
||||
anchor: 'end',
|
||||
align: 'end',
|
||||
color: '#fff',
|
||||
font: { weight: 'bold' },
|
||||
formatter: (value) => value.toLocaleString('ru-RU')
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(255,255,255,0.1)' },
|
||||
grace: '15%'
|
||||
},
|
||||
y: { grid: { display: false } }
|
||||
}
|
||||
},
|
||||
plugins: [ChartDataLabels]
|
||||
});
|
||||
}
|
||||
|
||||
// Emoji rain on intro
|
||||
setTimeout(() => {
|
||||
const emojis = ['🛰️', '📡', '🌍', '✨', '🎯', '📍', '🔭', '⭐'];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
setTimeout(() => {
|
||||
const emoji = document.createElement('div');
|
||||
emoji.className = 'emoji-rain';
|
||||
emoji.textContent = emojis[Math.floor(Math.random() * emojis.length)];
|
||||
emoji.style.left = Math.random() * 100 + '%';
|
||||
emoji.style.animationDuration = (2 + Math.random() * 2) + 's';
|
||||
document.body.appendChild(emoji);
|
||||
setTimeout(() => emoji.remove(), 4000);
|
||||
}, i * 200);
|
||||
}
|
||||
}, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -286,8 +286,9 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
|
||||
<!-- <script type="module" src="{% static 'chartjs/color.esm.js' %}"></script> -->
|
||||
<script src="{% static 'chartjs/chart.js' %}"></script>
|
||||
<script src="{% static 'chartjs/chart-datalabels.js' %}"></script>
|
||||
<script src="{% static 'js/checkbox-select-multiple.js' %}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
Reference in New Issue
Block a user