Добавил локально библиотеку 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() {
|
||||
|
||||
@@ -71,6 +71,7 @@ from .views.tech_analyze import (
|
||||
)
|
||||
from .views.points_averaging import PointsAveragingView, PointsAveragingAPIView, RecalculateGroupAPIView
|
||||
from .views.statistics import StatisticsView, StatisticsAPIView
|
||||
from .views.secret_stats import SecretStatsView
|
||||
|
||||
app_name = 'mainapp'
|
||||
|
||||
@@ -149,5 +150,6 @@ urlpatterns = [
|
||||
path('api/points-averaging/recalculate/', RecalculateGroupAPIView.as_view(), name='points_averaging_recalculate'),
|
||||
path('statistics/', StatisticsView.as_view(), name='statistics'),
|
||||
path('api/statistics/', StatisticsAPIView.as_view(), name='statistics_api'),
|
||||
path('secret-stat/', SecretStatsView.as_view(), name='secret_stats'),
|
||||
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