Compare commits
No commits in common. "4e4f6defded56e06ed6c5cfe16f0ed191969dd55" and "894dd1d92dd6015bccd37bcb960139ebb793ab13" have entirely different histories.
4e4f6defde
...
894dd1d92d
1
.gitignore
vendored
1
.gitignore
vendored
@ -29,4 +29,3 @@ 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
|
|
||||||
|
15
docker-compose.override.yml
Normal file
15
docker-compose.override.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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
@ -15,8 +15,12 @@ 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
|
||||||
@ -60,7 +64,7 @@ INSTALLED_APPS = [
|
|||||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||||
|
|
||||||
SITE_ID = os.getenv("SITE_ID", 1)
|
SITE_ID = 1
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
"django.contrib.auth.backends.ModelBackend", # default
|
"django.contrib.auth.backends.ModelBackend", # default
|
||||||
|
@ -1,48 +1,3 @@
|
|||||||
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',
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
# 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',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,43 +1,3 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
class Country(models.Model):
|
# Create your models here.
|
||||||
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,8 +11,7 @@ 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,
|
||||||
|
|
||||||
@ -52,8 +51,7 @@ 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"),
|
||||||
|
|
||||||
|
@ -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,87 +49,3 @@ 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,
|
|
||||||
})
|
|
||||||
|
@ -1,122 +1,5 @@
|
|||||||
{% extends "pxy_dashboard/partials/base.html" %}
|
{% extends "pxy_dashboard/partials/base.html" %}
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}Zone Definition{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
<link rel="stylesheet" href="{% static 'dashboard/vendor/apexcharts/apexcharts.css' %}">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<h2>Zone Definition</h2>
|
||||||
{% include "pxy_dashboard/partials/dashboard/kpi_row.html" %}
|
<p>This is a placeholder page for <code>apps-zone-definition.html</code></p>
|
||||||
<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 %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user