Добавил статистики

This commit is contained in:
2025-12-04 11:33:43 +03:00
parent 30b56de709
commit 027f971f5a
6 changed files with 781 additions and 2 deletions

View File

@@ -104,6 +104,9 @@
<a href="{% url 'mainapp:tech_analyze_list' %}" class="btn btn-info btn-sm" title="Технический анализ">
<i class="bi bi-gear-wide-connected"></i> Тех. анализ
</a>
<a href="{% url 'mainapp:statistics' %}" class="btn btn-secondary btn-sm" title="Статистика">
<i class="bi bi-bar-chart-line"></i> Статистика
</a>
</div>
<!-- Add to List Button -->

View File

@@ -0,0 +1,485 @@
{% extends 'mainapp/base.html' %}
{% load static %}
{% block title %}Статистика{% endblock %}
{% block extra_css %}
<link href="{% static 'css/checkbox-select-multiple.css' %}" rel="stylesheet">
<style>
.stat-card {
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-value {
font-size: 2.5rem;
font-weight: bold;
line-height: 1.2;
}
.stat-label {
font-size: 0.9rem;
color: #6c757d;
}
.satellite-stat-row:hover {
background-color: #f8f9fa;
}
.preset-btn.active {
background-color: #0d6efd;
color: white;
}
#dailyChart {
min-height: 300px;
}
.new-emission-badge {
font-size: 0.75rem;
margin: 2px;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-3">
<!-- Header -->
<div class="row mb-3">
<div class="col-12 d-flex justify-content-between align-items-center">
<h2><i class="bi bi-bar-chart-line"></i> Статистика</h2>
<a href="{% url 'mainapp:source_list' %}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> К списку объектов
</a>
</div>
</div>
<!-- Filters -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<form method="get" id="filter-form">
<div class="row g-3 align-items-end">
<!-- Date presets -->
<div class="col-auto">
<label class="form-label">Период:</label>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary preset-btn {% if preset == 'week' %}active{% endif %}"
data-preset="week">Неделя</button>
<button type="button" class="btn btn-outline-primary preset-btn {% if preset == 'month' %}active{% endif %}"
data-preset="month">Месяц</button>
<button type="button" class="btn btn-outline-primary preset-btn {% if preset == '3months' %}active{% endif %}"
data-preset="3months">3 месяца</button>
<button type="button" class="btn btn-outline-primary preset-btn {% if preset == '6months' %}active{% endif %}"
data-preset="6months">Полгода</button>
<button type="button" class="btn btn-outline-primary preset-btn {% if preset == 'all' or not preset and not date_from %}active{% endif %}"
data-preset="all">Всё время</button>
</div>
</div>
<!-- Custom date range -->
<div class="col-auto">
<label for="date_from" class="form-label">С:</label>
<input type="date" class="form-control" id="date_from" name="date_from"
value="{{ date_from }}">
</div>
<div class="col-auto">
<label for="date_to" class="form-label">По:</label>
<input type="date" class="form-control" id="date_to" name="date_to"
value="{{ date_to }}">
</div>
<!-- Satellite filter with custom widget -->
<div class="col-md-3">
<label class="form-label">Спутники:</label>
<div class="checkbox-multiselect-wrapper" data-widget-id="satellite_id">
<div class="multiselect-input-container">
<div class="multiselect-tags" id="satellite_id_tags"></div>
<input type="text"
class="multiselect-search form-control"
placeholder="Выберите спутники..."
id="satellite_id_search"
autocomplete="off">
<button type="button" class="multiselect-clear" id="satellite_id_clear" title="Очистить все">×</button>
</div>
<div class="multiselect-dropdown" id="satellite_id_dropdown">
<div class="multiselect-options">
{% for satellite in satellites %}
<label class="multiselect-option">
<input type="checkbox"
name="satellite_id"
value="{{ satellite.id }}"
{% if satellite.id in selected_satellites %}checked{% endif %}
data-label="{{ satellite.name }}">
<span class="option-label">{{ satellite.name }}</span>
</label>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Submit -->
<div class="col-auto">
<button type="submit" class="btn btn-primary">
<i class="bi bi-funnel"></i> Применить
</button>
<a href="{% url 'mainapp:statistics' %}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> Сбросить
</a>
</div>
</div>
<input type="hidden" name="preset" id="preset-input" value="{{ preset }}">
</form>
</div>
</div>
</div>
</div>
<!-- Main Statistics Cards -->
<div class="row mb-4">
<!-- Total Points -->
<div class="col-md-4">
<div class="card stat-card h-100 border-primary">
<div class="card-body text-center">
<div class="stat-value text-primary">{{ total_points }}</div>
<div class="stat-label">Точек геолокации</div>
<small class="text-muted">по {{ total_sources }} объектам</small>
</div>
</div>
</div>
<!-- New Emissions -->
<div class="col-md-4">
<div class="card stat-card h-100 border-success">
<div class="card-body text-center">
<div class="stat-value text-success">{{ new_emissions_count }}</div>
<div class="stat-label">Новых уникальных излучений</div>
<small class="text-muted">впервые появившихся за период</small>
</div>
</div>
</div>
<!-- Satellites Count -->
<div class="col-md-4">
<div class="card stat-card h-100 border-info">
<div class="card-body text-center">
<div class="stat-value text-info">{{ satellite_stats|length }}</div>
<div class="stat-label">Спутников с данными</div>
</div>
</div>
</div>
</div>
<!-- New Emissions Table -->
{% if new_emission_objects %}
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-light">
<i class="bi bi-stars"></i> Новые излучения (уникальные имена, появившиеся впервые в выбранном периоде)
</div>
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 320px; overflow-y: auto;">
<table class="table table-sm table-hover table-striped mb-0">
<thead class="table-light sticky-top">
<tr>
<th style="width: 5%;" class="text-center"></th>
<th style="width: 45%;">Имя объекта</th>
<th style="width: 25%;">Тип объекта</th>
<th style="width: 25%;">Принадлежность</th>
</tr>
</thead>
<tbody>
{% for obj in new_emission_objects %}
<tr>
<td class="text-center">{{ forloop.counter }}</td>
<td>{{ obj.name }}</td>
<td>{{ obj.info }}</td>
<td>{{ obj.ownership }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="row">
<!-- Daily Chart -->
<div class="col-md-8 mb-4">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-graph-up"></i> Динамика по дням
</div>
<div class="card-body">
<canvas id="dailyChart"></canvas>
</div>
</div>
</div>
<!-- Satellite Statistics -->
<div class="col-md-4 mb-4">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-broadcast"></i> Статистика по спутникам
</div>
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
<table class="table table-sm table-hover mb-0">
<thead class="table-light sticky-top">
<tr>
<th>Спутник</th>
<th class="text-center">Точек</th>
<th class="text-center">Объектов</th>
</tr>
</thead>
<tbody>
{% for stat in satellite_stats %}
<tr class="satellite-stat-row">
<td>{{ stat.parameter_obj__id_satellite__name }}</td>
<td class="text-center">
<span class="badge bg-primary">{{ stat.points_count }}</span>
</td>
<td class="text-center">
<span class="badge bg-secondary">{{ stat.sources_count }}</span>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center text-muted">Нет данных</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Satellite Charts -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-pie-chart"></i> Распределение точек по спутникам
</div>
<div class="card-body">
<canvas id="satellitePieChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-bar-chart"></i> Топ-10 спутников по количеству точек
</div>
<div class="card-body">
<canvas id="satelliteBarChart"></canvas>
</div>
</div>
</div>
</div>
</div>
{% 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 src="{% static 'js/checkbox-select-multiple.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize multiselect widget
const wrapper = document.querySelector('.checkbox-multiselect-wrapper[data-widget-id="satellite_id"]');
if (wrapper) {
initCheckboxMultiselect(wrapper);
}
// Preset buttons handling
const presetBtns = document.querySelectorAll('.preset-btn');
const presetInput = document.getElementById('preset-input');
const dateFromInput = document.getElementById('date_from');
const dateToInput = document.getElementById('date_to');
presetBtns.forEach(btn => {
btn.addEventListener('click', function() {
presetBtns.forEach(b => b.classList.remove('active'));
this.classList.add('active');
presetInput.value = this.dataset.preset;
// Clear custom dates when using preset
dateFromInput.value = '';
dateToInput.value = '';
// Submit form
document.getElementById('filter-form').submit();
});
});
// Clear preset when custom dates are entered
dateFromInput.addEventListener('change', function() {
presetInput.value = '';
presetBtns.forEach(b => b.classList.remove('active'));
});
dateToInput.addEventListener('change', function() {
presetInput.value = '';
presetBtns.forEach(b => b.classList.remove('active'));
});
// Register datalabels plugin
Chart.register(ChartDataLabels);
// Daily Chart
const dailyData = {{ daily_data|safe }};
const dailyLabels = dailyData.map(d => {
if (d.date) {
const date = new Date(d.date);
return date.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year:"2-digit" });
}
return '';
});
const dailyPoints = dailyData.map(d => d.points);
const dailySources = dailyData.map(d => d.sources);
if (dailyData.length > 0) {
new Chart(document.getElementById('dailyChart'), {
type: 'line',
data: {
labels: dailyLabels,
datasets: [{
label: 'Точки ГЛ',
data: dailyPoints,
borderColor: 'rgb(13, 110, 253)',
backgroundColor: 'rgba(13, 110, 253, 0.1)',
fill: false,
// tension: 0.3
}, {
label: 'Объекты',
data: dailySources,
borderColor: 'rgb(25, 135, 84)',
backgroundColor: 'rgba(25, 135, 84, 0.1)',
fill: false,
// tension: 0.3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
},
datalabels: {
display: false
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// Satellite Statistics
const satelliteStats = {{ satellite_stats_json|safe }};
// Pie Chart (top 10)
const top10Stats = satelliteStats.slice(0, 10);
const otherPoints = satelliteStats.slice(10).reduce((sum, s) => sum + s.points_count, 0);
const pieLabels = top10Stats.map(s => s.parameter_obj__id_satellite__name);
const pieData = top10Stats.map(s => s.points_count);
if (otherPoints > 0) {
pieLabels.push('Другие');
pieData.push(otherPoints);
}
const colors = [
'#0d6efd', '#198754', '#dc3545', '#ffc107', '#0dcaf0',
'#6f42c1', '#fd7e14', '#20c997', '#6c757d', '#d63384', '#adb5bd'
];
if (pieData.length > 0) {
new Chart(document.getElementById('satellitePieChart'), {
type: 'doughnut',
data: {
labels: pieLabels,
datasets: [{
data: pieData,
backgroundColor: colors.slice(0, pieData.length)
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'right',
},
datalabels: {
color: '#fff',
font: {
weight: 'bold',
size: 11
},
formatter: function(value, context) {
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(1);
if (percentage < 5) return '';
return value + '\n(' + percentage + '%)';
},
textAlign: 'center'
}
}
}
});
}
// Bar Chart (top 10) with data labels
if (top10Stats.length > 0) {
new Chart(document.getElementById('satelliteBarChart'), {
type: 'bar',
data: {
labels: top10Stats.map(s => s.parameter_obj__id_satellite__name),
datasets: [{
label: 'Количество точек',
data: top10Stats.map(s => s.points_count),
backgroundColor: colors.slice(0, top10Stats.length)
}]
},
options: {
responsive: true,
indexAxis: 'y',
plugins: {
legend: {
display: false
},
datalabels: {
anchor: 'end',
align: 'end',
color: '#333',
font: {
weight: 'bold',
size: 11
},
formatter: function(value) {
return value;
}
}
},
scales: {
x: {
beginAtZero: true,
grace: '10%'
}
}
}
});
}
});
</script>
{% endblock %}