Compare commits
2 Commits
894dd1d92d
...
4e4f6defde
Author | SHA1 | Date | |
---|---|---|---|
4e4f6defde | |||
8a95ff9ad3 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -29,3 +29,4 @@ db.sqlite3
|
||||
pxy_city_digital_twins/__backup__/
|
||||
Dockerfile.dev
|
||||
docker-compose.override.yml
|
||||
docker-compose.override.yml
|
||||
|
@ -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
|
21159
mediafiles/geo_scenarios/combined_with_volumes.csv
Normal file
21159
mediafiles/geo_scenarios/combined_with_volumes.csv
Normal file
File diff suppressed because it is too large
Load Diff
12123
mediafiles/opt_scenarios/all_steps_GEN_ORG_urbana_individual_B.csv
Normal file
12123
mediafiles/opt_scenarios/all_steps_GEN_ORG_urbana_individual_B.csv
Normal file
File diff suppressed because it is too large
Load Diff
@ -15,12 +15,8 @@ sys.path.append(str(BASE_DIR))
|
||||
# Core security settings
|
||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||
DEBUG = os.getenv("DEBUG", "False") == "True"
|
||||
#ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",")
|
||||
ALLOWED_HOSTS = [
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'.ngrok-free.app',
|
||||
]
|
||||
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",")
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
# Django built-in apps
|
||||
@ -64,7 +60,7 @@ INSTALLED_APPS = [
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||
|
||||
SITE_ID = 1
|
||||
SITE_ID = os.getenv("SITE_ID", 1)
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"django.contrib.auth.backends.ModelBackend", # default
|
||||
|
@ -1,3 +1,48 @@
|
||||
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',
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
58
pxy_dashboard/apps/migrations/0001_initial.py
Normal file
58
pxy_dashboard/apps/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
0
pxy_dashboard/apps/migrations/__init__.py
Normal file
0
pxy_dashboard/apps/migrations/__init__.py
Normal file
@ -1,3 +1,43 @@
|
||||
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})"
|
||||
|
@ -11,7 +11,8 @@ from pxy_dashboard.apps.views import (
|
||||
apps_file_manager,
|
||||
|
||||
# New – Waste Collection (Pre-Operation)
|
||||
apps_zone_definition,
|
||||
#apps_zone_definition,
|
||||
zone_definition_view,
|
||||
apps_route_optimization,
|
||||
apps_dispatch_plan,
|
||||
|
||||
@ -51,7 +52,8 @@ urlpatterns = [
|
||||
path("file-manager", apps_file_manager, name="file-manager"),
|
||||
|
||||
# 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("dispatch-plan", apps_dispatch_plan, name="dispatch-plan"),
|
||||
|
||||
|
@ -25,7 +25,7 @@ apps_file_manager = AppsView.as_view(template_name="pxy_dashboard/apps/apps-file
|
||||
# ───── Waste Collection Intelligence ─────────────────────────────────────────
|
||||
|
||||
# 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_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_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")
|
||||
|
||||
|
||||
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,
|
||||
})
|
||||
|
@ -1,5 +1,122 @@
|
||||
{% extends "pxy_dashboard/partials/base.html" %}
|
||||
{% block content %}
|
||||
<h2>Zone Definition</h2>
|
||||
<p>This is a placeholder page for <code>apps-zone-definition.html</code></p>
|
||||
{% load static %}
|
||||
|
||||
{% 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 %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user