Добавил статистики
This commit is contained in:
@@ -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 -->
|
||||
|
||||
485
dbapp/mainapp/templates/mainapp/statistics.html
Normal file
485
dbapp/mainapp/templates/mainapp/statistics.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user