Добавил локально библиотеку 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<!-- <script type="module" src="{% static 'chartjs/color.esm.js' %}"></script> -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></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 src="{% static 'js/checkbox-select-multiple.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ from .views.tech_analyze import (
|
|||||||
)
|
)
|
||||||
from .views.points_averaging import PointsAveragingView, PointsAveragingAPIView, RecalculateGroupAPIView
|
from .views.points_averaging import PointsAveragingView, PointsAveragingAPIView, RecalculateGroupAPIView
|
||||||
from .views.statistics import StatisticsView, StatisticsAPIView
|
from .views.statistics import StatisticsView, StatisticsAPIView
|
||||||
|
from .views.secret_stats import SecretStatsView
|
||||||
|
|
||||||
app_name = 'mainapp'
|
app_name = 'mainapp'
|
||||||
|
|
||||||
@@ -149,5 +150,6 @@ urlpatterns = [
|
|||||||
path('api/points-averaging/recalculate/', RecalculateGroupAPIView.as_view(), name='points_averaging_recalculate'),
|
path('api/points-averaging/recalculate/', RecalculateGroupAPIView.as_view(), name='points_averaging_recalculate'),
|
||||||
path('statistics/', StatisticsView.as_view(), name='statistics'),
|
path('statistics/', StatisticsView.as_view(), name='statistics'),
|
||||||
path('api/statistics/', StatisticsAPIView.as_view(), name='statistics_api'),
|
path('api/statistics/', StatisticsAPIView.as_view(), name='statistics_api'),
|
||||||
|
path('secret-stat/', SecretStatsView.as_view(), name='secret_stats'),
|
||||||
path('logout/', custom_logout, name='logout'),
|
path('logout/', custom_logout, name='logout'),
|
||||||
]
|
]
|
||||||
287
dbapp/mainapp/views/secret_stats.py
Normal file
287
dbapp/mainapp/views/secret_stats.py
Normal 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
|
||||||
7
dbapp/static/chartjs/chart-datalabels.js
Normal file
7
dbapp/static/chartjs/chart-datalabels.js
Normal file
File diff suppressed because one or more lines are too long
14
dbapp/static/chartjs/chart.js
Normal file
14
dbapp/static/chartjs/chart.js
Normal file
File diff suppressed because one or more lines are too long
591
dbapp/static/chartjs/color.esm.js
Normal file
591
dbapp/static/chartjs/color.esm.js
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
/*!
|
||||||
|
* @kurkle/color v0.4.0
|
||||||
|
* https://github.com/kurkle/color#readme
|
||||||
|
* (c) 2025 Jukka Kurkela
|
||||||
|
* Released under the MIT License
|
||||||
|
*/
|
||||||
|
function round(v) {
|
||||||
|
return v + 0.5 | 0;
|
||||||
|
}
|
||||||
|
const lim = (v, l, h) => Math.max(Math.min(v, h), l);
|
||||||
|
function p2b(v) {
|
||||||
|
return lim(round(v * 2.55), 0, 255);
|
||||||
|
}
|
||||||
|
function b2p(v) {
|
||||||
|
return lim(round(v / 2.55), 0, 100);
|
||||||
|
}
|
||||||
|
function n2b(v) {
|
||||||
|
return lim(round(v * 255), 0, 255);
|
||||||
|
}
|
||||||
|
function b2n(v) {
|
||||||
|
return lim(round(v / 2.55) / 100, 0, 1);
|
||||||
|
}
|
||||||
|
function n2p(v) {
|
||||||
|
return lim(round(v * 100), 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
const map$1 = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, A: 10, B: 11, C: 12, D: 13, E: 14, F: 15, a: 10, b: 11, c: 12, d: 13, e: 14, f: 15};
|
||||||
|
const hex = [...'0123456789ABCDEF'];
|
||||||
|
const h1 = b => hex[b & 0xF];
|
||||||
|
const h2 = b => hex[(b & 0xF0) >> 4] + hex[b & 0xF];
|
||||||
|
const eq = b => ((b & 0xF0) >> 4) === (b & 0xF);
|
||||||
|
const isShort = v => eq(v.r) && eq(v.g) && eq(v.b) && eq(v.a);
|
||||||
|
function hexParse(str) {
|
||||||
|
var len = str.length;
|
||||||
|
var ret;
|
||||||
|
if (str[0] === '#') {
|
||||||
|
if (len === 4 || len === 5) {
|
||||||
|
ret = {
|
||||||
|
r: 255 & map$1[str[1]] * 17,
|
||||||
|
g: 255 & map$1[str[2]] * 17,
|
||||||
|
b: 255 & map$1[str[3]] * 17,
|
||||||
|
a: len === 5 ? map$1[str[4]] * 17 : 255
|
||||||
|
};
|
||||||
|
} else if (len === 7 || len === 9) {
|
||||||
|
ret = {
|
||||||
|
r: map$1[str[1]] << 4 | map$1[str[2]],
|
||||||
|
g: map$1[str[3]] << 4 | map$1[str[4]],
|
||||||
|
b: map$1[str[5]] << 4 | map$1[str[6]],
|
||||||
|
a: len === 9 ? (map$1[str[7]] << 4 | map$1[str[8]]) : 255
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
const alpha = (a, f) => a < 255 ? f(a) : '';
|
||||||
|
function hexString(v) {
|
||||||
|
var f = isShort(v) ? h1 : h2;
|
||||||
|
return v
|
||||||
|
? '#' + f(v.r) + f(v.g) + f(v.b) + alpha(v.a, f)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HUE_RE = /^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;
|
||||||
|
function hsl2rgbn(h, s, l) {
|
||||||
|
const a = s * Math.min(l, 1 - l);
|
||||||
|
const f = (n, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||||
|
return [f(0), f(8), f(4)];
|
||||||
|
}
|
||||||
|
function hsv2rgbn(h, s, v) {
|
||||||
|
const f = (n, k = (n + h / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0);
|
||||||
|
return [f(5), f(3), f(1)];
|
||||||
|
}
|
||||||
|
function hwb2rgbn(h, w, b) {
|
||||||
|
const rgb = hsl2rgbn(h, 1, 0.5);
|
||||||
|
let i;
|
||||||
|
if (w + b > 1) {
|
||||||
|
i = 1 / (w + b);
|
||||||
|
w *= i;
|
||||||
|
b *= i;
|
||||||
|
}
|
||||||
|
for (i = 0; i < 3; i++) {
|
||||||
|
rgb[i] *= 1 - w - b;
|
||||||
|
rgb[i] += w;
|
||||||
|
}
|
||||||
|
return rgb;
|
||||||
|
}
|
||||||
|
function hueValue(r, g, b, d, max) {
|
||||||
|
if (r === max) {
|
||||||
|
return ((g - b) / d) + (g < b ? 6 : 0);
|
||||||
|
}
|
||||||
|
if (g === max) {
|
||||||
|
return (b - r) / d + 2;
|
||||||
|
}
|
||||||
|
return (r - g) / d + 4;
|
||||||
|
}
|
||||||
|
function rgb2hsl(v) {
|
||||||
|
const range = 255;
|
||||||
|
const r = v.r / range;
|
||||||
|
const g = v.g / range;
|
||||||
|
const b = v.b / range;
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
const l = (max + min) / 2;
|
||||||
|
let h, s, d;
|
||||||
|
if (max !== min) {
|
||||||
|
d = max - min;
|
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
|
h = hueValue(r, g, b, d, max);
|
||||||
|
h = h * 60 + 0.5;
|
||||||
|
}
|
||||||
|
return [h | 0, s || 0, l];
|
||||||
|
}
|
||||||
|
function calln(f, a, b, c) {
|
||||||
|
return (
|
||||||
|
Array.isArray(a)
|
||||||
|
? f(a[0], a[1], a[2])
|
||||||
|
: f(a, b, c)
|
||||||
|
).map(n2b);
|
||||||
|
}
|
||||||
|
function hsl2rgb(h, s, l) {
|
||||||
|
return calln(hsl2rgbn, h, s, l);
|
||||||
|
}
|
||||||
|
function hwb2rgb(h, w, b) {
|
||||||
|
return calln(hwb2rgbn, h, w, b);
|
||||||
|
}
|
||||||
|
function hsv2rgb(h, s, v) {
|
||||||
|
return calln(hsv2rgbn, h, s, v);
|
||||||
|
}
|
||||||
|
function hue(h) {
|
||||||
|
return (h % 360 + 360) % 360;
|
||||||
|
}
|
||||||
|
function hueParse(str) {
|
||||||
|
const m = HUE_RE.exec(str);
|
||||||
|
let a = 255;
|
||||||
|
let v;
|
||||||
|
if (!m) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m[5] !== v) {
|
||||||
|
a = m[6] ? p2b(+m[5]) : n2b(+m[5]);
|
||||||
|
}
|
||||||
|
const h = hue(+m[2]);
|
||||||
|
const p1 = +m[3] / 100;
|
||||||
|
const p2 = +m[4] / 100;
|
||||||
|
if (m[1] === 'hwb') {
|
||||||
|
v = hwb2rgb(h, p1, p2);
|
||||||
|
} else if (m[1] === 'hsv') {
|
||||||
|
v = hsv2rgb(h, p1, p2);
|
||||||
|
} else {
|
||||||
|
v = hsl2rgb(h, p1, p2);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
r: v[0],
|
||||||
|
g: v[1],
|
||||||
|
b: v[2],
|
||||||
|
a: a
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function rotate(v, deg) {
|
||||||
|
var h = rgb2hsl(v);
|
||||||
|
h[0] = hue(h[0] + deg);
|
||||||
|
h = hsl2rgb(h);
|
||||||
|
v.r = h[0];
|
||||||
|
v.g = h[1];
|
||||||
|
v.b = h[2];
|
||||||
|
}
|
||||||
|
function hslString(v) {
|
||||||
|
if (!v) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const a = rgb2hsl(v);
|
||||||
|
const h = a[0];
|
||||||
|
const s = n2p(a[1]);
|
||||||
|
const l = n2p(a[2]);
|
||||||
|
return v.a < 255
|
||||||
|
? `hsla(${h}, ${s}%, ${l}%, ${b2n(v.a)})`
|
||||||
|
: `hsl(${h}, ${s}%, ${l}%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = {
|
||||||
|
x: 'dark',
|
||||||
|
Z: 'light',
|
||||||
|
Y: 're',
|
||||||
|
X: 'blu',
|
||||||
|
W: 'gr',
|
||||||
|
V: 'medium',
|
||||||
|
U: 'slate',
|
||||||
|
A: 'ee',
|
||||||
|
T: 'ol',
|
||||||
|
S: 'or',
|
||||||
|
B: 'ra',
|
||||||
|
C: 'lateg',
|
||||||
|
D: 'ights',
|
||||||
|
R: 'in',
|
||||||
|
Q: 'turquois',
|
||||||
|
E: 'hi',
|
||||||
|
P: 'ro',
|
||||||
|
O: 'al',
|
||||||
|
N: 'le',
|
||||||
|
M: 'de',
|
||||||
|
L: 'yello',
|
||||||
|
F: 'en',
|
||||||
|
K: 'ch',
|
||||||
|
G: 'arks',
|
||||||
|
H: 'ea',
|
||||||
|
I: 'ightg',
|
||||||
|
J: 'wh'
|
||||||
|
};
|
||||||
|
const names$1 = {
|
||||||
|
OiceXe: 'f0f8ff',
|
||||||
|
antiquewEte: 'faebd7',
|
||||||
|
aqua: 'ffff',
|
||||||
|
aquamarRe: '7fffd4',
|
||||||
|
azuY: 'f0ffff',
|
||||||
|
beige: 'f5f5dc',
|
||||||
|
bisque: 'ffe4c4',
|
||||||
|
black: '0',
|
||||||
|
blanKedOmond: 'ffebcd',
|
||||||
|
Xe: 'ff',
|
||||||
|
XeviTet: '8a2be2',
|
||||||
|
bPwn: 'a52a2a',
|
||||||
|
burlywood: 'deb887',
|
||||||
|
caMtXe: '5f9ea0',
|
||||||
|
KartYuse: '7fff00',
|
||||||
|
KocTate: 'd2691e',
|
||||||
|
cSO: 'ff7f50',
|
||||||
|
cSnflowerXe: '6495ed',
|
||||||
|
cSnsilk: 'fff8dc',
|
||||||
|
crimson: 'dc143c',
|
||||||
|
cyan: 'ffff',
|
||||||
|
xXe: '8b',
|
||||||
|
xcyan: '8b8b',
|
||||||
|
xgTMnPd: 'b8860b',
|
||||||
|
xWay: 'a9a9a9',
|
||||||
|
xgYF: '6400',
|
||||||
|
xgYy: 'a9a9a9',
|
||||||
|
xkhaki: 'bdb76b',
|
||||||
|
xmagFta: '8b008b',
|
||||||
|
xTivegYF: '556b2f',
|
||||||
|
xSange: 'ff8c00',
|
||||||
|
xScEd: '9932cc',
|
||||||
|
xYd: '8b0000',
|
||||||
|
xsOmon: 'e9967a',
|
||||||
|
xsHgYF: '8fbc8f',
|
||||||
|
xUXe: '483d8b',
|
||||||
|
xUWay: '2f4f4f',
|
||||||
|
xUgYy: '2f4f4f',
|
||||||
|
xQe: 'ced1',
|
||||||
|
xviTet: '9400d3',
|
||||||
|
dAppRk: 'ff1493',
|
||||||
|
dApskyXe: 'bfff',
|
||||||
|
dimWay: '696969',
|
||||||
|
dimgYy: '696969',
|
||||||
|
dodgerXe: '1e90ff',
|
||||||
|
fiYbrick: 'b22222',
|
||||||
|
flSOwEte: 'fffaf0',
|
||||||
|
foYstWAn: '228b22',
|
||||||
|
fuKsia: 'ff00ff',
|
||||||
|
gaRsbSo: 'dcdcdc',
|
||||||
|
ghostwEte: 'f8f8ff',
|
||||||
|
gTd: 'ffd700',
|
||||||
|
gTMnPd: 'daa520',
|
||||||
|
Way: '808080',
|
||||||
|
gYF: '8000',
|
||||||
|
gYFLw: 'adff2f',
|
||||||
|
gYy: '808080',
|
||||||
|
honeyMw: 'f0fff0',
|
||||||
|
hotpRk: 'ff69b4',
|
||||||
|
RdianYd: 'cd5c5c',
|
||||||
|
Rdigo: '4b0082',
|
||||||
|
ivSy: 'fffff0',
|
||||||
|
khaki: 'f0e68c',
|
||||||
|
lavFMr: 'e6e6fa',
|
||||||
|
lavFMrXsh: 'fff0f5',
|
||||||
|
lawngYF: '7cfc00',
|
||||||
|
NmoncEffon: 'fffacd',
|
||||||
|
ZXe: 'add8e6',
|
||||||
|
ZcSO: 'f08080',
|
||||||
|
Zcyan: 'e0ffff',
|
||||||
|
ZgTMnPdLw: 'fafad2',
|
||||||
|
ZWay: 'd3d3d3',
|
||||||
|
ZgYF: '90ee90',
|
||||||
|
ZgYy: 'd3d3d3',
|
||||||
|
ZpRk: 'ffb6c1',
|
||||||
|
ZsOmon: 'ffa07a',
|
||||||
|
ZsHgYF: '20b2aa',
|
||||||
|
ZskyXe: '87cefa',
|
||||||
|
ZUWay: '778899',
|
||||||
|
ZUgYy: '778899',
|
||||||
|
ZstAlXe: 'b0c4de',
|
||||||
|
ZLw: 'ffffe0',
|
||||||
|
lime: 'ff00',
|
||||||
|
limegYF: '32cd32',
|
||||||
|
lRF: 'faf0e6',
|
||||||
|
magFta: 'ff00ff',
|
||||||
|
maPon: '800000',
|
||||||
|
VaquamarRe: '66cdaa',
|
||||||
|
VXe: 'cd',
|
||||||
|
VScEd: 'ba55d3',
|
||||||
|
VpurpN: '9370db',
|
||||||
|
VsHgYF: '3cb371',
|
||||||
|
VUXe: '7b68ee',
|
||||||
|
VsprRggYF: 'fa9a',
|
||||||
|
VQe: '48d1cc',
|
||||||
|
VviTetYd: 'c71585',
|
||||||
|
midnightXe: '191970',
|
||||||
|
mRtcYam: 'f5fffa',
|
||||||
|
mistyPse: 'ffe4e1',
|
||||||
|
moccasR: 'ffe4b5',
|
||||||
|
navajowEte: 'ffdead',
|
||||||
|
navy: '80',
|
||||||
|
Tdlace: 'fdf5e6',
|
||||||
|
Tive: '808000',
|
||||||
|
TivedBb: '6b8e23',
|
||||||
|
Sange: 'ffa500',
|
||||||
|
SangeYd: 'ff4500',
|
||||||
|
ScEd: 'da70d6',
|
||||||
|
pOegTMnPd: 'eee8aa',
|
||||||
|
pOegYF: '98fb98',
|
||||||
|
pOeQe: 'afeeee',
|
||||||
|
pOeviTetYd: 'db7093',
|
||||||
|
papayawEp: 'ffefd5',
|
||||||
|
pHKpuff: 'ffdab9',
|
||||||
|
peru: 'cd853f',
|
||||||
|
pRk: 'ffc0cb',
|
||||||
|
plum: 'dda0dd',
|
||||||
|
powMrXe: 'b0e0e6',
|
||||||
|
purpN: '800080',
|
||||||
|
YbeccapurpN: '663399',
|
||||||
|
Yd: 'ff0000',
|
||||||
|
Psybrown: 'bc8f8f',
|
||||||
|
PyOXe: '4169e1',
|
||||||
|
saddNbPwn: '8b4513',
|
||||||
|
sOmon: 'fa8072',
|
||||||
|
sandybPwn: 'f4a460',
|
||||||
|
sHgYF: '2e8b57',
|
||||||
|
sHshell: 'fff5ee',
|
||||||
|
siFna: 'a0522d',
|
||||||
|
silver: 'c0c0c0',
|
||||||
|
skyXe: '87ceeb',
|
||||||
|
UXe: '6a5acd',
|
||||||
|
UWay: '708090',
|
||||||
|
UgYy: '708090',
|
||||||
|
snow: 'fffafa',
|
||||||
|
sprRggYF: 'ff7f',
|
||||||
|
stAlXe: '4682b4',
|
||||||
|
tan: 'd2b48c',
|
||||||
|
teO: '8080',
|
||||||
|
tEstN: 'd8bfd8',
|
||||||
|
tomato: 'ff6347',
|
||||||
|
Qe: '40e0d0',
|
||||||
|
viTet: 'ee82ee',
|
||||||
|
JHt: 'f5deb3',
|
||||||
|
wEte: 'ffffff',
|
||||||
|
wEtesmoke: 'f5f5f5',
|
||||||
|
Lw: 'ffff00',
|
||||||
|
LwgYF: '9acd32'
|
||||||
|
};
|
||||||
|
function unpack() {
|
||||||
|
const unpacked = {};
|
||||||
|
const keys = Object.keys(names$1);
|
||||||
|
const tkeys = Object.keys(map);
|
||||||
|
let i, j, k, ok, nk;
|
||||||
|
for (i = 0; i < keys.length; i++) {
|
||||||
|
ok = nk = keys[i];
|
||||||
|
for (j = 0; j < tkeys.length; j++) {
|
||||||
|
k = tkeys[j];
|
||||||
|
nk = nk.replace(k, map[k]);
|
||||||
|
}
|
||||||
|
k = parseInt(names$1[ok], 16);
|
||||||
|
unpacked[nk] = [k >> 16 & 0xFF, k >> 8 & 0xFF, k & 0xFF];
|
||||||
|
}
|
||||||
|
return unpacked;
|
||||||
|
}
|
||||||
|
|
||||||
|
let names;
|
||||||
|
function nameParse(str) {
|
||||||
|
if (!names) {
|
||||||
|
names = unpack();
|
||||||
|
names.transparent = [0, 0, 0, 0];
|
||||||
|
}
|
||||||
|
const a = names[str.toLowerCase()];
|
||||||
|
return a && {
|
||||||
|
r: a[0],
|
||||||
|
g: a[1],
|
||||||
|
b: a[2],
|
||||||
|
a: a.length === 4 ? a[3] : 255
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const RGB_RE = /^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;
|
||||||
|
function rgbParse(str) {
|
||||||
|
const m = RGB_RE.exec(str);
|
||||||
|
let a = 255;
|
||||||
|
let r, g, b;
|
||||||
|
if (!m) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m[7] !== r) {
|
||||||
|
const v = +m[7];
|
||||||
|
a = m[8] ? p2b(v) : lim(v * 255, 0, 255);
|
||||||
|
}
|
||||||
|
r = +m[1];
|
||||||
|
g = +m[3];
|
||||||
|
b = +m[5];
|
||||||
|
r = 255 & (m[2] ? p2b(r) : lim(r, 0, 255));
|
||||||
|
g = 255 & (m[4] ? p2b(g) : lim(g, 0, 255));
|
||||||
|
b = 255 & (m[6] ? p2b(b) : lim(b, 0, 255));
|
||||||
|
return {
|
||||||
|
r: r,
|
||||||
|
g: g,
|
||||||
|
b: b,
|
||||||
|
a: a
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function rgbString(v) {
|
||||||
|
return v && (
|
||||||
|
v.a < 255
|
||||||
|
? `rgba(${v.r}, ${v.g}, ${v.b}, ${b2n(v.a)})`
|
||||||
|
: `rgb(${v.r}, ${v.g}, ${v.b})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const to = v => v <= 0.0031308 ? v * 12.92 : Math.pow(v, 1.0 / 2.4) * 1.055 - 0.055;
|
||||||
|
const from = v => v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||||
|
function interpolate(rgb1, rgb2, t) {
|
||||||
|
const r = from(b2n(rgb1.r));
|
||||||
|
const g = from(b2n(rgb1.g));
|
||||||
|
const b = from(b2n(rgb1.b));
|
||||||
|
return {
|
||||||
|
r: n2b(to(r + t * (from(b2n(rgb2.r)) - r))),
|
||||||
|
g: n2b(to(g + t * (from(b2n(rgb2.g)) - g))),
|
||||||
|
b: n2b(to(b + t * (from(b2n(rgb2.b)) - b))),
|
||||||
|
a: rgb1.a + t * (rgb2.a - rgb1.a)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMMENT_REGEXP = /\/\*[^]*?\*\//g;
|
||||||
|
function modHSL(v, i, ratio) {
|
||||||
|
if (v) {
|
||||||
|
let tmp = rgb2hsl(v);
|
||||||
|
tmp[i] = Math.max(0, Math.min(tmp[i] + tmp[i] * ratio, i === 0 ? 360 : 1));
|
||||||
|
tmp = hsl2rgb(tmp);
|
||||||
|
v.r = tmp[0];
|
||||||
|
v.g = tmp[1];
|
||||||
|
v.b = tmp[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function clone(v, proto) {
|
||||||
|
return v ? Object.assign(proto || {}, v) : v;
|
||||||
|
}
|
||||||
|
function fromObject(input) {
|
||||||
|
var v = {r: 0, g: 0, b: 0, a: 255};
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
if (input.length >= 3) {
|
||||||
|
v = {r: input[0], g: input[1], b: input[2], a: 255};
|
||||||
|
if (input.length > 3) {
|
||||||
|
v.a = n2b(input[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v = clone(input, {r: 0, g: 0, b: 0, a: 1});
|
||||||
|
v.a = n2b(v.a);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
function functionParse(str) {
|
||||||
|
if (str.charAt(0) === 'r') {
|
||||||
|
return rgbParse(str);
|
||||||
|
}
|
||||||
|
return hueParse(str);
|
||||||
|
}
|
||||||
|
class Color {
|
||||||
|
constructor(input) {
|
||||||
|
if (input instanceof Color) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
const type = typeof input;
|
||||||
|
let v;
|
||||||
|
if (type === 'object') {
|
||||||
|
v = fromObject(input);
|
||||||
|
} else if (type === 'string') {
|
||||||
|
const clean = input.replace(COMMENT_REGEXP, '');
|
||||||
|
v = hexParse(clean) || nameParse(clean) || functionParse(clean);
|
||||||
|
}
|
||||||
|
this._rgb = v;
|
||||||
|
this._valid = !!v;
|
||||||
|
}
|
||||||
|
get valid() {
|
||||||
|
return this._valid;
|
||||||
|
}
|
||||||
|
get rgb() {
|
||||||
|
var v = clone(this._rgb);
|
||||||
|
if (v) {
|
||||||
|
v.a = b2n(v.a);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
set rgb(obj) {
|
||||||
|
this._rgb = fromObject(obj);
|
||||||
|
}
|
||||||
|
rgbString() {
|
||||||
|
return this._valid ? rgbString(this._rgb) : undefined;
|
||||||
|
}
|
||||||
|
hexString() {
|
||||||
|
return this._valid ? hexString(this._rgb) : undefined;
|
||||||
|
}
|
||||||
|
hslString() {
|
||||||
|
return this._valid ? hslString(this._rgb) : undefined;
|
||||||
|
}
|
||||||
|
mix(color, weight) {
|
||||||
|
if (color) {
|
||||||
|
const c1 = this.rgb;
|
||||||
|
const c2 = color.rgb;
|
||||||
|
let w2;
|
||||||
|
const p = weight === w2 ? 0.5 : weight;
|
||||||
|
const w = 2 * p - 1;
|
||||||
|
const a = c1.a - c2.a;
|
||||||
|
const w1 = ((w * a === -1 ? w : (w + a) / (1 + w * a)) + 1) / 2.0;
|
||||||
|
w2 = 1 - w1;
|
||||||
|
c1.r = 0xFF & w1 * c1.r + w2 * c2.r + 0.5;
|
||||||
|
c1.g = 0xFF & w1 * c1.g + w2 * c2.g + 0.5;
|
||||||
|
c1.b = 0xFF & w1 * c1.b + w2 * c2.b + 0.5;
|
||||||
|
c1.a = p * c1.a + (1 - p) * c2.a;
|
||||||
|
this.rgb = c1;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
interpolate(color, t) {
|
||||||
|
if (color) {
|
||||||
|
this._rgb = interpolate(this._rgb, color._rgb, t);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
clone() {
|
||||||
|
return new Color(this.rgb);
|
||||||
|
}
|
||||||
|
alpha(a) {
|
||||||
|
this._rgb.a = n2b(a);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
clearer(ratio) {
|
||||||
|
const rgb = this._rgb;
|
||||||
|
rgb.a *= 1 - ratio;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
greyscale() {
|
||||||
|
const rgb = this._rgb;
|
||||||
|
const val = round(rgb.r * 0.3 + rgb.g * 0.59 + rgb.b * 0.11);
|
||||||
|
rgb.r = rgb.g = rgb.b = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
opaquer(ratio) {
|
||||||
|
const rgb = this._rgb;
|
||||||
|
rgb.a *= 1 + ratio;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
negate() {
|
||||||
|
const v = this._rgb;
|
||||||
|
v.r = 255 - v.r;
|
||||||
|
v.g = 255 - v.g;
|
||||||
|
v.b = 255 - v.b;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
lighten(ratio) {
|
||||||
|
modHSL(this._rgb, 2, ratio);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
darken(ratio) {
|
||||||
|
modHSL(this._rgb, 2, -ratio);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
saturate(ratio) {
|
||||||
|
modHSL(this._rgb, 1, ratio);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
desaturate(ratio) {
|
||||||
|
modHSL(this._rgb, 1, -ratio);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
rotate(deg) {
|
||||||
|
rotate(this._rgb, deg);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function index_esm(input) {
|
||||||
|
return new Color(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Color, b2n, b2p, index_esm as default, hexParse, hexString, hsl2rgb, hslString, hsv2rgb, hueParse, hwb2rgb, lim, n2b, n2p, nameParse, p2b, rgb2hsl, rgbParse, rgbString, rotate, round };
|
||||||
Reference in New Issue
Block a user