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

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

@@ -6,7 +6,7 @@
.multiselect-input-container { .multiselect-input-container {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: flex-start;
min-height: 38px; min-height: 38px;
border: 1px solid #ced4da; border: 1px solid #ced4da;
border-radius: 0.25rem; border-radius: 0.25rem;
@@ -27,7 +27,8 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
flex: 0 0 auto; flex: 1 1 auto;
max-width: calc(100% - 150px);
} }
.multiselect-tag { .multiselect-tag {

View File

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

View File

@@ -70,6 +70,7 @@ from .views.tech_analyze import (
TechAnalyzeAPIView, TechAnalyzeAPIView,
) )
from .views.points_averaging import PointsAveragingView, PointsAveragingAPIView, RecalculateGroupAPIView from .views.points_averaging import PointsAveragingView, PointsAveragingAPIView, RecalculateGroupAPIView
from .views.statistics import StatisticsView, StatisticsAPIView
app_name = 'mainapp' app_name = 'mainapp'
@@ -146,5 +147,7 @@ urlpatterns = [
path('points-averaging/', PointsAveragingView.as_view(), name='points_averaging'), path('points-averaging/', PointsAveragingView.as_view(), name='points_averaging'),
path('api/points-averaging/', PointsAveragingAPIView.as_view(), name='points_averaging_api'), path('api/points-averaging/', PointsAveragingAPIView.as_view(), name='points_averaging_api'),
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('api/statistics/', StatisticsAPIView.as_view(), name='statistics_api'),
path('logout/', custom_logout, name='logout'), path('logout/', custom_logout, name='logout'),
] ]

View File

@@ -71,6 +71,10 @@ from .points_averaging import (
PointsAveragingAPIView, PointsAveragingAPIView,
RecalculateGroupAPIView, RecalculateGroupAPIView,
) )
from .statistics import (
StatisticsView,
StatisticsAPIView,
)
__all__ = [ __all__ = [
# Base # Base
@@ -144,4 +148,7 @@ __all__ = [
'PointsAveragingView', 'PointsAveragingView',
'PointsAveragingAPIView', 'PointsAveragingAPIView',
'RecalculateGroupAPIView', 'RecalculateGroupAPIView',
# Statistics
'StatisticsView',
'StatisticsAPIView',
] ]

View File

@@ -0,0 +1,280 @@
"""
Представление для страницы статистики.
"""
import json
from datetime import timedelta
from django.db.models import Count, Q, Min
from django.db.models.functions import TruncDate
from django.utils import timezone
from django.views.generic import TemplateView
from django.http import JsonResponse
from ..models import ObjItem, Source, Satellite, Geo
class StatisticsView(TemplateView):
"""Страница статистики по данным геолокации."""
template_name = 'mainapp/statistics.html'
def get_date_range(self):
"""Получает диапазон дат из параметров запроса."""
date_from = self.request.GET.get('date_from')
date_to = self.request.GET.get('date_to')
preset = self.request.GET.get('preset')
now = timezone.now()
# Обработка пресетов
if preset == 'week':
date_from = (now - timedelta(days=7)).date()
date_to = now.date()
elif preset == 'month':
date_from = (now - timedelta(days=30)).date()
date_to = now.date()
elif preset == '3months':
date_from = (now - timedelta(days=90)).date()
date_to = now.date()
elif preset == '6months':
date_from = (now - timedelta(days=180)).date()
date_to = now.date()
elif preset == 'all':
date_from = None
date_to = None
else:
# Парсинг дат из параметров
from datetime import datetime
if date_from:
try:
date_from = datetime.strptime(date_from, '%Y-%m-%d').date()
except ValueError:
date_from = None
if date_to:
try:
date_to = datetime.strptime(date_to, '%Y-%m-%d').date()
except ValueError:
date_to = None
return date_from, date_to, preset
def get_selected_satellites(self):
"""Получает выбранные спутники из параметров запроса."""
satellite_ids = self.request.GET.getlist('satellite_id')
return [int(sid) for sid in satellite_ids if sid.isdigit()]
def get_base_queryset(self, date_from, date_to, satellite_ids):
"""Возвращает базовый queryset ObjItem с фильтрами."""
qs = ObjItem.objects.filter(
geo_obj__isnull=False,
geo_obj__timestamp__isnull=False
)
if date_from:
qs = qs.filter(geo_obj__timestamp__date__gte=date_from)
if date_to:
qs = qs.filter(geo_obj__timestamp__date__lte=date_to)
if satellite_ids:
qs = qs.filter(parameter_obj__id_satellite__id__in=satellite_ids)
return qs
def get_statistics(self, date_from, date_to, satellite_ids):
"""Вычисляет основную статистику."""
base_qs = self.get_base_queryset(date_from, date_to, satellite_ids)
# Общее количество точек
total_points = base_qs.count()
# Количество уникальных объектов (Source)
total_sources = base_qs.filter(source__isnull=False).values('source').distinct().count()
# Новые излучения - объекты, у которых имя появилось впервые в выбранном периоде
new_emissions_data = self._calculate_new_emissions(date_from, date_to, satellite_ids)
# Статистика по спутникам
satellite_stats = self._get_satellite_statistics(date_from, date_to, satellite_ids)
# Данные для графика по дням
daily_data = self._get_daily_statistics(date_from, date_to, satellite_ids)
return {
'total_points': total_points,
'total_sources': total_sources,
'new_emissions_count': new_emissions_data['count'],
'new_emission_objects': new_emissions_data['objects'],
'satellite_stats': satellite_stats,
'daily_data': daily_data,
}
def _calculate_new_emissions(self, date_from, date_to, satellite_ids):
"""
Вычисляет новые излучения - уникальные имена объектов,
которые появились впервые в выбранном периоде.
Возвращает количество уникальных новых имён и данные об объектах.
Оптимизировано для минимизации SQL запросов.
"""
if not date_from:
# Если нет начальной даты, берём все данные - новых излучений нет
return {'count': 0, 'objects': []}
# Получаем все имена объектов, которые появились ДО выбранного периода
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, satellite_ids).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': []}
# Оптимизация: получаем все данные одним запросом с группировкой по имени
# Используем values() для получения уникальных комбинаций name + info + ownership
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'])
return {'count': len(new_names), 'objects': new_objects}
def _get_satellite_statistics(self, date_from, date_to, satellite_ids):
"""Получает статистику по каждому спутнику."""
base_qs = self.get_base_queryset(date_from, date_to, satellite_ids)
# Группируем по спутникам
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)
).order_by('-points_count')
return list(stats)
def _get_daily_statistics(self, date_from, date_to, satellite_ids):
"""Получает статистику по дням для графика."""
base_qs = self.get_base_queryset(date_from, date_to, satellite_ids)
daily = base_qs.annotate(
date=TruncDate('geo_obj__timestamp')
).values('date').annotate(
points=Count('id'),
sources=Count('source', distinct=True)
).order_by('date')
return list(daily)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
date_from, date_to, preset = self.get_date_range()
satellite_ids = self.get_selected_satellites()
# Получаем только спутники, у которых есть точки ГЛ
satellites_with_points = ObjItem.objects.filter(
geo_obj__isnull=False,
geo_obj__timestamp__isnull=False,
parameter_obj__id_satellite__isnull=False
).values_list('parameter_obj__id_satellite__id', flat=True).distinct()
satellites = Satellite.objects.filter(
id__in=satellites_with_points
).order_by('name')
# Получаем статистику
stats = self.get_statistics(date_from, date_to, satellite_ids)
# Сериализуем данные для JavaScript
daily_data_json = json.dumps([
{
'date': item['date'].isoformat() if item['date'] else None,
'points': item['points'],
'sources': item['sources'],
}
for item in stats['daily_data']
])
satellite_stats_json = json.dumps(stats['satellite_stats'])
context.update({
'satellites': satellites,
'selected_satellites': satellite_ids,
'date_from': date_from.isoformat() if date_from else '',
'date_to': date_to.isoformat() if date_to else '',
'preset': preset or '',
'total_points': stats['total_points'],
'total_sources': stats['total_sources'],
'new_emissions_count': stats['new_emissions_count'],
'new_emission_objects': stats['new_emission_objects'],
'satellite_stats': stats['satellite_stats'],
'daily_data': daily_data_json,
'satellite_stats_json': satellite_stats_json,
})
return context
class StatisticsAPIView(StatisticsView):
"""API endpoint для получения статистики в JSON формате."""
def get(self, request, *args, **kwargs):
date_from, date_to, preset = self.get_date_range()
satellite_ids = self.get_selected_satellites()
stats = self.get_statistics(date_from, date_to, satellite_ids)
# Преобразуем даты в строки для JSON
daily_data = []
for item in stats['daily_data']:
daily_data.append({
'date': item['date'].isoformat() if item['date'] else None,
'points': item['points'],
'sources': item['sources'],
})
return JsonResponse({
'total_points': stats['total_points'],
'total_sources': stats['total_sources'],
'new_emissions_count': stats['new_emissions_count'],
'new_emission_objects': stats['new_emission_objects'],
'satellite_stats': stats['satellite_stats'],
'daily_data': daily_data,
})