Compare commits

...

2 Commits

Author SHA1 Message Date
4e4f6defde Zones Dashboard
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-05-18 22:01:51 -06:00
8a95ff9ad3 zone visualization 2025-05-18 22:01:18 -06:00
12 changed files with 33640 additions and 30 deletions

1
.gitignore vendored
View File

@ -29,3 +29,4 @@ db.sqlite3
pxy_city_digital_twins/__backup__/ pxy_city_digital_twins/__backup__/
Dockerfile.dev Dockerfile.dev
docker-compose.override.yml docker-compose.override.yml
docker-compose.override.yml

View File

@ -1,15 +0,0 @@
version: "3.8"
services:
web:
build:
context: .
dockerfile: Dockerfile.dev
command: >
sh -c "python manage.py migrate &&
python manage.py runserver 0.0.0.0:8000"
ports:
- "8011:8000"
volumes:
- .:/app
- ./staticfiles:/app/staticfiles

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -15,12 +15,8 @@ sys.path.append(str(BASE_DIR))
# Core security settings # Core security settings
SECRET_KEY = os.getenv("SECRET_KEY") SECRET_KEY = os.getenv("SECRET_KEY")
DEBUG = os.getenv("DEBUG", "False") == "True" DEBUG = os.getenv("DEBUG", "False") == "True"
#ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",") ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",")
ALLOWED_HOSTS = [
'localhost',
'127.0.0.1',
'.ngrok-free.app',
]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
# Django built-in apps # Django built-in apps
@ -64,7 +60,7 @@ INSTALLED_APPS = [
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5" CRISPY_TEMPLATE_PACK = "bootstrap5"
SITE_ID = 1 SITE_ID = os.getenv("SITE_ID", 1)
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend", # default "django.contrib.auth.backends.ModelBackend", # default

View File

@ -1,3 +1,48 @@
from django.contrib import admin from django.contrib import admin
from .models import Country, GeoScenario, OptScenario
# Register your models here.
@admin.register(Country)
class CountryAdmin(admin.ModelAdmin):
list_display = ('code', 'name')
search_fields = ('code', 'name')
ordering = ('name',)
@admin.register(GeoScenario)
class GeoScenarioAdmin(admin.ModelAdmin):
list_display = ('name', 'country', 'upload_date', 'geographic_field_name')
list_filter = ('country', 'upload_date')
search_fields = ('name', 'country__name')
readonly_fields = ('upload_date',)
fieldsets = (
(None, {
'fields': (
'name',
'country',
'geographic_field_name',
'csv_file',
'upload_date',
)
}),
)
@admin.register(OptScenario)
class OptScenarioAdmin(admin.ModelAdmin):
list_display = ('name', 'geo_scenario', 'type_of_waste', 'strategy', 'upload_date')
list_filter = ('type_of_waste', 'upload_date')
search_fields = ('name', 'geo_scenario__name', 'strategy')
readonly_fields = ('upload_date',)
fieldsets = (
(None, {
'fields': (
'geo_scenario',
'name',
'type_of_waste',
'strategy',
'optimized_csv',
'upload_date',
)
}),
)

View File

@ -0,0 +1,58 @@
# Generated by Django 5.0.3 on 2025-05-19 00:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Country',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=3, unique=True)),
('name', models.CharField(max_length=100)),
],
options={
'verbose_name': 'País',
'verbose_name_plural': 'Países',
},
),
migrations.CreateModel(
name='GeoScenario',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Nombre del escenario geográfico base', max_length=255)),
('upload_date', models.DateTimeField(auto_now_add=True)),
('geographic_field_name', models.CharField(help_text='Columna con el identificador geográfico (ej. N_URBANO, PUEBLO)', max_length=100)),
('csv_file', models.FileField(upload_to='geo_scenarios/')),
('country', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='apps.country')),
],
options={
'verbose_name': 'Escenario Geográfico',
'verbose_name_plural': 'Escenarios Geográficos',
},
),
migrations.CreateModel(
name='OptScenario',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Nombre del escenario de optimización', max_length=255)),
('type_of_waste', models.CharField(help_text='Tipo de residuo (ej. orgánico, reciclable)', max_length=100)),
('strategy', models.CharField(help_text='Método de optimización utilizado', max_length=100)),
('upload_date', models.DateTimeField(auto_now_add=True)),
('optimized_csv', models.FileField(upload_to='opt_scenarios/')),
('geo_scenario', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opt_scenarios', to='apps.geoscenario')),
],
options={
'verbose_name': 'Escenario de Optimización',
'verbose_name_plural': 'Escenarios de Optimización',
},
),
]

View File

@ -1,3 +1,43 @@
from django.db import models from django.db import models
# Create your models here. class Country(models.Model):
code = models.CharField(max_length=3, unique=True)
name = models.CharField(max_length=100)
class Meta:
verbose_name = 'País'
verbose_name_plural = 'Países'
def __str__(self):
return self.name
class GeoScenario(models.Model):
name = models.CharField(max_length=255, help_text='Nombre del escenario geográfico base')
upload_date = models.DateTimeField(auto_now_add=True)
country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True)
geographic_field_name = models.CharField(max_length=100, help_text='Columna con el identificador geográfico (ej. N_URBANO, PUEBLO)')
csv_file = models.FileField(upload_to='geo_scenarios/')
class Meta:
verbose_name = 'Escenario Geográfico'
verbose_name_plural = 'Escenarios Geográficos'
def __str__(self):
return f"{self.name} ({self.country})"
class OptScenario(models.Model):
geo_scenario = models.ForeignKey(GeoScenario, on_delete=models.CASCADE, related_name='opt_scenarios')
name = models.CharField(max_length=255, help_text='Nombre del escenario de optimización')
type_of_waste = models.CharField(max_length=100, help_text='Tipo de residuo (ej. orgánico, reciclable)')
strategy = models.CharField(max_length=100, help_text='Método de optimización utilizado')
upload_date = models.DateTimeField(auto_now_add=True)
optimized_csv = models.FileField(upload_to='opt_scenarios/')
class Meta:
verbose_name = 'Escenario de Optimización'
verbose_name_plural = 'Escenarios de Optimización'
def __str__(self):
return f"{self.name} ({self.type_of_waste})"

View File

@ -11,7 +11,8 @@ from pxy_dashboard.apps.views import (
apps_file_manager, apps_file_manager,
# New Waste Collection (Pre-Operation) # New Waste Collection (Pre-Operation)
apps_zone_definition, #apps_zone_definition,
zone_definition_view,
apps_route_optimization, apps_route_optimization,
apps_dispatch_plan, apps_dispatch_plan,
@ -51,7 +52,8 @@ urlpatterns = [
path("file-manager", apps_file_manager, name="file-manager"), path("file-manager", apps_file_manager, name="file-manager"),
# Pre-Operation # Pre-Operation
path("zone-definition", apps_zone_definition, name="zone-definition"), #path("zone-definition", apps_zone_definition, name="zone-definition"),
path("zone-definition", zone_definition_view, name="zone-definition"),
path("route-optimization", apps_route_optimization, name="route-optimization"), path("route-optimization", apps_route_optimization, name="route-optimization"),
path("dispatch-plan", apps_dispatch_plan, name="dispatch-plan"), path("dispatch-plan", apps_dispatch_plan, name="dispatch-plan"),

View File

@ -25,7 +25,7 @@ apps_file_manager = AppsView.as_view(template_name="pxy_dashboard/apps/apps-file
# ───── Waste Collection Intelligence ───────────────────────────────────────── # ───── Waste Collection Intelligence ─────────────────────────────────────────
# Pre-Operation # Pre-Operation
apps_zone_definition = AppsView.as_view(template_name="pxy_dashboard/apps/apps-zone-definition.html") #apps_zone_definition = AppsView.as_view(template_name="pxy_dashboard/apps/apps-zone-definition.html")
apps_route_optimization = AppsView.as_view(template_name="pxy_dashboard/apps/apps-route-optimization.html") apps_route_optimization = AppsView.as_view(template_name="pxy_dashboard/apps/apps-route-optimization.html")
apps_dispatch_plan = AppsView.as_view(template_name="pxy_dashboard/apps/apps-dispatch-plan.html") apps_dispatch_plan = AppsView.as_view(template_name="pxy_dashboard/apps/apps-dispatch-plan.html")
@ -49,3 +49,87 @@ apps_logs_limits = AppsView.as_view(template_name="pxy_dashboard/apps/apps-logs-
apps_config_api = AppsView.as_view(template_name="pxy_dashboard/apps/apps-config-api.html") apps_config_api = AppsView.as_view(template_name="pxy_dashboard/apps/apps-config-api.html")
apps_config_map = AppsView.as_view(template_name="pxy_dashboard/apps/apps-config-map.html") apps_config_map = AppsView.as_view(template_name="pxy_dashboard/apps/apps-config-map.html")
apps_config_collection = AppsView.as_view(template_name="pxy_dashboard/apps/apps-config-collection.html") apps_config_collection = AppsView.as_view(template_name="pxy_dashboard/apps/apps-config-collection.html")
from django.shortcuts import render
from .models import GeoScenario
import pandas as pd
def zone_definition_view(request):
scenario = GeoScenario.objects.last()
chart_data = {}
viviendas_data = {}
city_options = []
scatter_series = {}
if scenario and scenario.csv_file:
df = pd.read_csv(scenario.csv_file.path)
df = df.fillna(0)
# Solo conservar zonas con generación total > 0
df = df[df["GEN_TOT"] > 0]
# Opciones de ciudad
city_options = sorted(df["N_URBANO"].dropna().unique())
selected_city = request.GET.get("city") or (city_options[0] if city_options else None)
# Barras por zona (residuos)
if selected_city:
city_df = df[df["N_URBANO"] == selected_city]
grouped = (
city_df.groupby("COD_ZONA")[["GEN_ORG", "GEN_INVA", "GEN_RESTO"]]
.sum()
.reset_index()
.sort_values(by="GEN_ORG", ascending=False)
)
chart_data = {
"zones": grouped["COD_ZONA"].astype(str).tolist(),
"gen_org": grouped["GEN_ORG"].round(2).tolist(),
"gen_inva": grouped["GEN_INVA"].round(2).tolist(),
"gen_resto": grouped["GEN_RESTO"].round(2).tolist(),
}
# Barras por zona (viviendas)
viviendas_grouped = (
city_df.groupby("COD_ZONA")["num_viviendas"]
.sum()
.reset_index()
.sort_values(by="num_viviendas", ascending=False)
)
viviendas_data = {
"zones": viviendas_grouped["COD_ZONA"].astype(str).tolist(),
"viviendas": viviendas_grouped["num_viviendas"].astype(int).tolist(),
}
# Dispersión por ciudad
scatter_series = {
"GEN_ORG": [],
"GEN_INVA": [],
"GEN_RESTO": [],
}
city_grouped = (
df.groupby("N_URBANO")[["num_viviendas", "GEN_ORG", "GEN_INVA", "GEN_RESTO"]]
.sum()
.reset_index()
)
for _, row in city_grouped.iterrows():
viviendas = float(row["num_viviendas"])
if viviendas == 0:
continue
city = row["N_URBANO"]
if row["GEN_ORG"] > 0:
scatter_series["GEN_ORG"].append({"x": viviendas, "y": float(row["GEN_ORG"]), "city": city})
if row["GEN_INVA"] > 0:
scatter_series["GEN_INVA"].append({"x": viviendas, "y": float(row["GEN_INVA"]), "city": city})
if row["GEN_RESTO"] > 0:
scatter_series["GEN_RESTO"].append({"x": viviendas, "y": float(row["GEN_RESTO"]), "city": city})
return render(request, "pxy_dashboard/apps/apps-zone-definition.html", {
"chart_data": chart_data,
"viviendas_data": viviendas_data,
"scatter_series": scatter_series,
"cities": city_options,
"selected_city": selected_city,
})

View File

@ -1,5 +1,122 @@
{% extends "pxy_dashboard/partials/base.html" %} {% extends "pxy_dashboard/partials/base.html" %}
{% block content %} {% load static %}
<h2>Zone Definition</h2>
<p>This is a placeholder page for <code>apps-zone-definition.html</code></p> {% block title %}Zone Definition{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'dashboard/vendor/apexcharts/apexcharts.css' %}">
{% endblock %}
{% block content %}
{% include "pxy_dashboard/partials/dashboard/kpi_row.html" %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="fw-bold">Pre-Operation · Zone Definition</h4>
<form method="get" class="d-flex align-items-center">
<label for="city-select" class="me-2 mb-0">Select City:</label>
<select name="city" id="city-select" class="form-select" onchange="this.form.submit()">
{% for city in cities %}
<option value="{{ city }}" {% if city == selected_city %}selected{% endif %}>{{ city }}</option>
{% endfor %}
</select>
</form>
</div>
{% if chart_data.zones %}
<!-- Residuals by Zone -->
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0">Waste Generation by Zone</h5></div>
<div class="card-body">
<div id="chart-residues"></div>
</div>
</div>
<!-- Housing by Zone -->
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0">Number of Households by Zone</h5></div>
<div class="card-body">
<div id="chart-housing"></div>
</div>
</div>
{% endif %}
{% if scatter_series %}
<!-- Scatter Chart -->
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0">Scatter: Waste vs Households (All Cities)</h5></div>
<div class="card-body">
<div id="scatter-plot"></div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'dashboard/vendor/apexcharts/apexcharts.min.js' %}"></script>
<script>
{% if chart_data.zones %}
// Residue Bar Chart
var options1 = {
chart: { type: 'bar', stacked: true, height: 350 },
series: [
{ name: 'Organic', data: {{ chart_data.gen_org|safe }} },
{ name: 'Inorganic', data: {{ chart_data.gen_inva|safe }} },
{ name: 'Other', data: {{ chart_data.gen_resto|safe }} }
],
xaxis: { categories: {{ chart_data.zones|safe }} },
title: { text: "Waste Generation per Zone" }
};
new ApexCharts(document.querySelector("#chart-residues"), options1).render();
// Housing Chart
var options2 = {
chart: { type: 'bar', height: 350 },
series: [{ name: 'Households', data: {{ viviendas_data.viviendas|safe }} }],
xaxis: { categories: {{ viviendas_data.zones|safe }} },
title: { text: "Number of Households per Zone" }
};
new ApexCharts(document.querySelector("#chart-housing"), options2).render();
{% endif %}
{% if scatter_series %}
// Scatter Plot
var options3 = {
chart: { type: 'scatter', height: 400, zoom: { enabled: true } },
series: [
{
name: 'Organic',
data: {{ scatter_series.GEN_ORG|safe }},
},
{
name: 'Inorganic',
data: {{ scatter_series.GEN_INVA|safe }},
},
{
name: 'Other',
data: {{ scatter_series.GEN_RESTO|safe }},
},
],
xaxis: {
title: { text: "Number of Households" }
},
yaxis: {
title: { text: "Waste Generated (kg/day)" }
},
tooltip: {
custom: function({ series, seriesIndex, dataPointIndex, w }) {
var d = w.config.series[seriesIndex].data[dataPointIndex];
return '<div class="p-2"><strong>' + d.city + '</strong><br/>Waste: ' + d.y + '<br/>Homes: ' + d.x + '</div>';
}
},
title: {
text: "Waste Generation vs Households by City"
}
};
new ApexCharts(document.querySelector("#scatter-plot"), options3).render();
{% endif %}
</script>
{% endblock %} {% endblock %}