Add virtual dashboard for vehicles
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
60df25b39a
commit
8ed81f89e8
25
pxy_city_digital_twins/migrations/0001_initial.py
Normal file
25
pxy_city_digital_twins/migrations/0001_initial.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2025-05-21 21:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WastePickup',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('subdivision', models.CharField(max_length=100)),
|
||||||
|
('vehicle', models.CharField(max_length=50)),
|
||||||
|
('step_id', models.CharField(max_length=50)),
|
||||||
|
('quarter', models.PositiveSmallIntegerField()),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,11 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
# Create your models here.
|
class WastePickup(models.Model):
|
||||||
|
subdivision = models.CharField(max_length=100)
|
||||||
|
vehicle = models.CharField(max_length=50)
|
||||||
|
step_id = models.CharField(max_length=50)
|
||||||
|
quarter = models.PositiveSmallIntegerField() # 1 through 4
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.subdivision}/{self.vehicle}/{self.step_id} → Q{self.quarter} @ {self.timestamp}"
|
||||||
|
@ -35,50 +35,61 @@
|
|||||||
color="{{ building.color }}"
|
color="{{ building.color }}"
|
||||||
opacity="0.8">
|
opacity="0.8">
|
||||||
</a-box>
|
</a-box>
|
||||||
<a-entity position="{{ building.position_x }} {{ building.height }} {{ building.position_z }}">
|
|
||||||
|
|
||||||
</a-entity>
|
|
||||||
</a-entity>
|
</a-entity>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<!-- 2) Draw each job-sphere & gauge from step_positions -->
|
<!-- 2) Draw each job-sphere & gauge from step_positions -->
|
||||||
{% for pos in step_positions %}
|
{% for pos in step_positions %}
|
||||||
<a-entity position="{{ pos.x }} 1 {{ pos.z }}">
|
{% with sid=pos.step.id stype=pos.step.step_type %}
|
||||||
<a-sphere radius="10"
|
<a-entity id="step-{{ sid }}" position="{{ pos.x }} 1 {{ pos.z }}">
|
||||||
|
<!-- sphere now semi-transparent by default -->
|
||||||
|
<a-sphere id="sphere-{{ sid }}"
|
||||||
|
radius="10"
|
||||||
color="#FF4136"
|
color="#FF4136"
|
||||||
emissive="#FF4136"
|
emissive="#FF4136"
|
||||||
opacity="0.7">
|
transparent="true"
|
||||||
|
opacity="0.4">
|
||||||
</a-sphere>
|
</a-sphere>
|
||||||
<!-- gauge above the sphere -->
|
|
||||||
<a-entity class="status-gauge" gauge-click-toggle position="11 3 0">
|
|
||||||
{% include "pxy_city_digital_twins/_status_gauge_waste.html" with ring_color="#FF4136" status=pos.step.step_type id=pos.step.id %}
|
|
||||||
</a-entity>
|
|
||||||
</a-entity>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
|
<!-- gauge above the sphere -->
|
||||||
|
<a-entity
|
||||||
|
class="status-gauge"
|
||||||
|
gauge-click-toggle
|
||||||
|
position="11 3 0"
|
||||||
|
data-step-id="{{ sid }}"
|
||||||
|
data-step-type="{{ stype }}">
|
||||||
|
{% include "pxy_city_digital_twins/_status_gauge_waste.html" with ring_color="#FF4136" status=stype id=sid %}
|
||||||
|
</a-entity>
|
||||||
|
</a-entity>
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
</a-scene>
|
</a-scene>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Expose Django vars into JS
|
||||||
|
const subdivision = "{{ subdivision }}";
|
||||||
|
const vehicle = "{{ vehicle }}";
|
||||||
|
const recordUrl = "{% url 'waste-record-pickup' subdivision=subdivision vehicle=vehicle %}";
|
||||||
|
|
||||||
document.querySelector('#scene').addEventListener('loaded', () => {
|
document.querySelector('#scene').addEventListener('loaded', () => {
|
||||||
const sceneEl = document.getElementById('scene');
|
const sceneEl = document.getElementById('scene');
|
||||||
// 3) rel_coords was passed in context
|
|
||||||
const rel = {{ rel_coords|safe }};
|
const rel = {{ rel_coords|safe }};
|
||||||
|
|
||||||
// Compute bounding box & stageSize
|
// 3) Compute bounding box & stageSize
|
||||||
const xs = rel.map(r => r[0]), zs = rel.map(r => r[1]);
|
const xs = rel.map(r => r[0]), zs = rel.map(r => r[1]);
|
||||||
const minX = Math.min(...xs), maxX = Math.max(...xs);
|
const minX = Math.min(...xs), maxX = Math.max(...xs);
|
||||||
const minZ = Math.min(...zs), maxZ = Math.max(...zs);
|
const minZ = Math.min(...zs), maxZ = Math.max(...zs);
|
||||||
const width = maxX - minX, height = maxZ - minZ;
|
const width = maxX - minX, height = maxZ - minZ;
|
||||||
const stageSize = Math.ceil(Math.max(width, height) * 1.2);
|
const stageSize = Math.ceil(Math.max(width, height) * 1.2);
|
||||||
|
|
||||||
// Update invisible base-plane
|
// 4) Update invisible base-plane
|
||||||
const base = document.getElementById('basePlane');
|
const base = document.getElementById('basePlane');
|
||||||
base.setAttribute('width', width * 1.5);
|
base.setAttribute('width', width * 1.5);
|
||||||
base.setAttribute('height', height * 1.5);
|
base.setAttribute('height', height * 1.5);
|
||||||
base.setAttribute('position',
|
base.setAttribute('position',
|
||||||
`${(minX+maxX)/2} 0 ${(minZ+maxZ)/2}`);
|
`${(minX+maxX)/2} 0 ${(minZ+maxZ)/2}`);
|
||||||
|
|
||||||
// Add environment
|
// 5) Add environment
|
||||||
const envEl = document.createElement('a-entity');
|
const envEl = document.createElement('a-entity');
|
||||||
envEl.setAttribute('environment', `
|
envEl.setAttribute('environment', `
|
||||||
preset: forest;
|
preset: forest;
|
||||||
@ -92,7 +103,7 @@
|
|||||||
`);
|
`);
|
||||||
sceneEl.appendChild(envEl);
|
sceneEl.appendChild(envEl);
|
||||||
|
|
||||||
// Draw & animate each segment
|
// 6) Draw & animate each segment
|
||||||
const pulseDuration = 600;
|
const pulseDuration = 600;
|
||||||
rel.forEach(([x1,z1], i) => {
|
rel.forEach(([x1,z1], i) => {
|
||||||
if (i + 1 >= rel.length) return;
|
if (i + 1 >= rel.length) return;
|
||||||
@ -116,16 +127,72 @@
|
|||||||
sceneEl.appendChild(seg);
|
sceneEl.appendChild(seg);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Position camera at first step
|
// 7) Position camera at first step
|
||||||
if (rel.length) {
|
if (rel.length) {
|
||||||
const [fx,fz] = rel[0];
|
const [fx,fz] = rel[0];
|
||||||
const cam = document.getElementById('mainCamera');
|
const cam = document.getElementById('mainCamera');
|
||||||
cam.setAttribute('position', `${fx.toFixed(2)} 6 ${fz.toFixed(2)+6}`);
|
cam.setAttribute('position', `${fx.toFixed(2)} 6 ${fz.toFixed(2)+6}`);
|
||||||
cam.setAttribute('look-at', `${fx.toFixed(2)} 1 ${fz.toFixed(2)}`);
|
cam.setAttribute('look-at', `${fx.toFixed(2)} 1 ${fz.toFixed(2)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 8) Record pickups by clicking each gauge
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// CSRF helper
|
||||||
|
function getCookie(name) {
|
||||||
|
const v = document.cookie.match('(^|;)\\s*'+name+'\\s*=\\s*([^;]+)');
|
||||||
|
return v ? v.pop() : '';
|
||||||
|
}
|
||||||
|
const csrftoken = getCookie('csrftoken');
|
||||||
|
|
||||||
|
// Track click counts & define colors
|
||||||
|
const clickCounts = {};
|
||||||
|
const colors = ['#2ECC40','#FFDC00','#FF851B','#FF4136'];
|
||||||
|
|
||||||
|
document.querySelectorAll('.status-gauge').forEach(el => {
|
||||||
|
const stepId = el.dataset.stepId;
|
||||||
|
clickCounts[stepId] = 0;
|
||||||
|
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
// cycle 1→4
|
||||||
|
clickCounts[stepId] = (clickCounts[stepId] % 4) + 1;
|
||||||
|
const quarter = clickCounts[stepId];
|
||||||
|
|
||||||
|
// recolor ring
|
||||||
|
const ring = el.querySelector('.gauge-ring');
|
||||||
|
if (ring) ring.setAttribute('color', colors[quarter-1]);
|
||||||
|
|
||||||
|
// recolor sphere
|
||||||
|
const sph = document.getElementById(`sphere-${stepId}`);
|
||||||
|
if (sph) {
|
||||||
|
sph.setAttribute(
|
||||||
|
'material',
|
||||||
|
`color: ${colors[quarter-1]};
|
||||||
|
emissive: ${colors[quarter-1]};
|
||||||
|
transparent: true; opacity: 0.4`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// log & POST
|
||||||
|
console.log('Recording pickup', { step_id: stepId, quarter }, '→', recordUrl);
|
||||||
|
fetch(recordUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': csrftoken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ step_id: stepId, quarter })
|
||||||
|
}).then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error('Record pickup failed', res.status);
|
||||||
|
} else {
|
||||||
|
console.log('Pickup recorded');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
<!--
|
|
||||||
-->
|
|
@ -14,4 +14,8 @@ urlpatterns = [
|
|||||||
path('city/digital/twin/waste/debug/<str:subdivision>/<str:vehicle>/', views.waste_route_debug, name='waste_route_debug'),
|
path('city/digital/twin/waste/debug/<str:subdivision>/<str:vehicle>/', views.waste_route_debug, name='waste_route_debug'),
|
||||||
|
|
||||||
path('city/augmented/reality/digital/twin/waste/<str:subdivision>/<str:vehicle>/', views.augmented_waste_route, name='waste_route'),
|
path('city/augmented/reality/digital/twin/waste/<str:subdivision>/<str:vehicle>/', views.augmented_waste_route, name='waste_route'),
|
||||||
|
|
||||||
|
path('city/augmented/reality/digital/twin/waste/<str:subdivision>/<str:vehicle>/record/', views.record_pickup, name="waste-record-pickup"),
|
||||||
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -284,7 +284,9 @@ def waste_route(request, subdivision, vehicle):
|
|||||||
step_positions.append({
|
step_positions.append({
|
||||||
'x': x - avg_x,
|
'x': x - avg_x,
|
||||||
'z': z - avg_z,
|
'z': z - avg_z,
|
||||||
'step': step
|
'step_id': step.get('id'),
|
||||||
|
'step_type': step.get('step_type'),
|
||||||
|
'popup': step.get('popup'),
|
||||||
})
|
})
|
||||||
|
|
||||||
# 7) render
|
# 7) render
|
||||||
@ -368,3 +370,34 @@ def augmented_waste_route(request, subdivision, vehicle):
|
|||||||
'rel_coords': rel_coords,
|
'rel_coords': rel_coords,
|
||||||
'step_positions': step_positions,
|
'step_positions': step_positions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
from django.http import JsonResponse, HttpResponseBadRequest
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
from django.views.decorators.csrf import csrf_exempt # or use the csrf token client-side
|
||||||
|
from .models import WastePickup
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@require_POST
|
||||||
|
def record_pickup(request, subdivision, vehicle):
|
||||||
|
"""
|
||||||
|
Expect JSON body:
|
||||||
|
{ "step_id": "<id>", "quarter": <1-4> }
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.body)
|
||||||
|
step_id = payload['step_id']
|
||||||
|
quarter = int(payload['quarter'])
|
||||||
|
if quarter not in (1,2,3,4):
|
||||||
|
raise ValueError
|
||||||
|
except (KeyError, ValueError, json.JSONDecodeError):
|
||||||
|
return HttpResponseBadRequest("Invalid payload")
|
||||||
|
|
||||||
|
WastePickup.objects.create(
|
||||||
|
subdivision=subdivision,
|
||||||
|
vehicle=vehicle,
|
||||||
|
step_id=step_id,
|
||||||
|
quarter=quarter
|
||||||
|
)
|
||||||
|
return JsonResponse({"status":"ok"})
|
||||||
|
@ -469,3 +469,50 @@ def apps_facebook_pages_bot(request):
|
|||||||
"bots_info": bots_info,
|
"bots_info": bots_info,
|
||||||
"weekly_data_json": json.dumps(weekly_data),
|
"weekly_data_json": json.dumps(weekly_data),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from pxy_city_digital_twins.models import WastePickup
|
||||||
|
from collections import defaultdict
|
||||||
|
import json
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def apps_urban_digital_twin(request):
|
||||||
|
# ───── Ruta alterna para depuración: JSON completo ─────
|
||||||
|
if request.GET.get("dump") == "1":
|
||||||
|
full_table = list(WastePickup.objects.all().values(
|
||||||
|
"id", "subdivision", "vehicle", "step_id", "quarter", "timestamp"
|
||||||
|
))
|
||||||
|
return JsonResponse({"waste_pickup_table": full_table}, safe=False)
|
||||||
|
|
||||||
|
# ───── Lógica del gráfico ─────
|
||||||
|
subdivisions = WastePickup.objects.values_list("subdivision", flat=True).distinct()
|
||||||
|
selected_subdivision = request.GET.get("subdivision") or (subdivisions[0] if subdivisions else None)
|
||||||
|
|
||||||
|
raw = WastePickup.objects.filter(subdivision=selected_subdivision)
|
||||||
|
data_by_vehicle = defaultdict(lambda: defaultdict(set)) # vehicle → date → set de cuartiles
|
||||||
|
|
||||||
|
for pickup in raw:
|
||||||
|
date_str = pickup.timestamp.strftime("%Y-%m-%d")
|
||||||
|
veh = f"Vehículo {pickup.vehicle}"
|
||||||
|
data_by_vehicle[veh][date_str].add(pickup.quarter)
|
||||||
|
|
||||||
|
all_points = []
|
||||||
|
unique_vehicles = set()
|
||||||
|
for vehicle, date_map in data_by_vehicle.items():
|
||||||
|
unique_vehicles.add(vehicle)
|
||||||
|
for date, quarters in date_map.items():
|
||||||
|
all_points.append({
|
||||||
|
"x": date,
|
||||||
|
"y": vehicle,
|
||||||
|
"z": len(quarters) # cuartiles únicos ese día
|
||||||
|
})
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"subdivisions": subdivisions,
|
||||||
|
"selected_subdivision": selected_subdivision,
|
||||||
|
"scatter_data_json": json.dumps(all_points),
|
||||||
|
"vehicle_categories": json.dumps(sorted(unique_vehicles)),
|
||||||
|
}
|
||||||
|
return render(request, "pxy_dashboard/apps/apps-urban-digital-twin.html", context)
|
||||||
|
@ -1,5 +1,144 @@
|
|||||||
{% extends "pxy_dashboard/partials/base.html" %}
|
{% extends "pxy_dashboard/partials/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Urban Digital Twin{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Urban Digital Twin</h2>
|
{% include "pxy_dashboard/partials/dashboard/kpi_row.html" %}
|
||||||
<p>This is a placeholder page for <code>apps-urban-digital-twin.html</code></p>
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="subdivisionSelect" class="form-label">Selecciona una subdivisión:</label>
|
||||||
|
<select id="subdivisionSelect" class="form-select">
|
||||||
|
{% for s in subdivisions %}
|
||||||
|
<option value="{{ s }}" {% if s == selected_subdivision %}selected{% endif %}>{{ s }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Column Chart -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header"><h5 class="mb-0">Column Chart: Cuartiles por Fecha y Vehículo</h5></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="chart-bar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Heatmap -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header"><h5 class="mb-0">Heatmap: Distribución de Cuartiles</h5></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="chart-heatmap"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
// Datos desde Django
|
||||||
|
const rawData = {{ scatter_data_json|safe }};
|
||||||
|
const vehicles = {{ vehicle_categories|safe }};
|
||||||
|
const subdivision = "{{ selected_subdivision|escapejs }}";
|
||||||
|
|
||||||
|
// Escala de color para z
|
||||||
|
const colorScale = {1:"#FF4136",2:"#FF851B",3:"#FFDC00",4:"#2ECC40",5:"#0074D9"};
|
||||||
|
|
||||||
|
// Fechas únicas y ordenadas
|
||||||
|
const dates = [...new Set(rawData.map(p => p.x))].sort();
|
||||||
|
|
||||||
|
// Función para extraer solo dígitos del string "Vehículo X"
|
||||||
|
function extractVehicleId(label) {
|
||||||
|
const m = label.match(/\d+/);
|
||||||
|
return m ? m[0] : label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Column Chart Series ---
|
||||||
|
const seriesBar = vehicles.map(veh => ({
|
||||||
|
name: veh,
|
||||||
|
data: dates.map(d => {
|
||||||
|
const rec = rawData.find(p => p.x === d && p.y === veh);
|
||||||
|
return rec ? rec.z : 0;
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Render Column Chart con click
|
||||||
|
new ApexCharts(document.querySelector("#chart-bar"), {
|
||||||
|
chart: {
|
||||||
|
type: 'bar',
|
||||||
|
height: 350,
|
||||||
|
events: {
|
||||||
|
dataPointSelection: function(_, __, { seriesIndex, dataPointIndex }) {
|
||||||
|
const vehLabel = vehicles[seriesIndex];
|
||||||
|
const vehId = extractVehicleId(vehLabel);
|
||||||
|
const url = `/city/virtual/reality/digital/twin/waste/${encodeURIComponent(subdivision)}/${encodeURIComponent(vehId)}/`;
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plotOptions: { bar: { horizontal: false, columnWidth: '50%' } },
|
||||||
|
dataLabels: { enabled: false },
|
||||||
|
series: seriesBar,
|
||||||
|
xaxis: { categories: dates, title: { text: 'Fecha' } },
|
||||||
|
yaxis: { title: { text: 'Cuartiles únicos' } },
|
||||||
|
legend: { position: 'bottom' },
|
||||||
|
title: { text: `Cuartiles únicos por Fecha en ${subdivision}` }
|
||||||
|
}).render();
|
||||||
|
|
||||||
|
|
||||||
|
// --- Heatmap Series ---
|
||||||
|
const seriesHeat = vehicles.map(veh => ({
|
||||||
|
name: veh,
|
||||||
|
data: dates.map(d => {
|
||||||
|
const rec = rawData.find(p => p.x === d && p.y === veh);
|
||||||
|
return { x: d, y: rec ? rec.z : 0 };
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Render Heatmap con click
|
||||||
|
new ApexCharts(document.querySelector("#chart-heatmap"), {
|
||||||
|
chart: {
|
||||||
|
type: 'heatmap',
|
||||||
|
height: 350,
|
||||||
|
events: {
|
||||||
|
dataPointSelection: function(_, __, { seriesIndex, dataPointIndex }) {
|
||||||
|
const vehLabel = vehicles[seriesIndex];
|
||||||
|
const vehId = extractVehicleId(vehLabel);
|
||||||
|
const url = `/city/virtual/reality/digital/twin/waste/${encodeURIComponent(subdivision)}/${encodeURIComponent(vehId)}/`;
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: seriesHeat,
|
||||||
|
plotOptions: {
|
||||||
|
heatmap: {
|
||||||
|
shadeIntensity: 0.5,
|
||||||
|
colorScale: {
|
||||||
|
ranges: [
|
||||||
|
{ from: 0, to: 0, name: '0', color: '#f2f2f2' },
|
||||||
|
{ from: 1, to: 1, name: '1', color: '#FF4136' },
|
||||||
|
{ from: 2, to: 2, name: '2', color: '#FF851B' },
|
||||||
|
{ from: 3, to: 3, name: '3', color: '#FFDC00' },
|
||||||
|
{ from: 4, to: 4, name: '4', color: '#2ECC40' },
|
||||||
|
{ from: 5, to: 5, name: '5+', color: '#0074D9' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataLabels: { enabled: false },
|
||||||
|
xaxis: { type: 'category', categories: dates, title: { text: 'Fecha' } },
|
||||||
|
yaxis: { title: { text: 'Vehículo' } },
|
||||||
|
title: { text: `Heatmap de Cuartiles en ${subdivision}` }
|
||||||
|
}).render();
|
||||||
|
|
||||||
|
// Recarga al cambiar subdivisión
|
||||||
|
document.getElementById("subdivisionSelect")
|
||||||
|
.addEventListener("change", function(){
|
||||||
|
const q = new URLSearchParams({ subdivision: this.value }).toString();
|
||||||
|
location.search = q;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
<img src="{% static 'dashboard/images/users/avatar-1.jpg' %}" alt="user-image" height="42" class="rounded-circle shadow">
|
<img src="{% static 'dashboard/images/users/avatar-1.jpg' %}" alt="user-image" height="42" class="rounded-circle shadow">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow-1 ms-2">
|
<div class="flex-grow-1 ms-2">
|
||||||
<span class="fw-semibold fs-15 d-block">Doris Larson</span>
|
<span class="fw-semibold fs-15 d-block">Cucha Hernández</span>
|
||||||
<span class="fs-13">Founder</span>
|
<span class="fs-13">Founder</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-auto">
|
<div class="ms-auto">
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
<img src="{% static 'dashboard/images/users/avatar-1.jpg' %}" alt="user-image" height="42" class="rounded-circle shadow">
|
<img src="{% static 'dashboard/images/users/avatar-1.jpg' %}" alt="user-image" height="42" class="rounded-circle shadow">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow-1 ms-2">
|
<div class="flex-grow-1 ms-2">
|
||||||
<span class="fw-semibold fs-15 d-block">Doris Larson</span>
|
<span class="fw-semibold fs-15 d-block">Cucha Hernández</span>
|
||||||
<span class="fs-13">Founder</span>
|
<span class="fs-13">Founder</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-auto">
|
<div class="ms-auto">
|
||||||
|
@ -347,7 +347,7 @@
|
|||||||
<img src="{% static 'dashboard/images/users/avatar-1.jpg' %}" alt="user-image" width="32" class="rounded-circle">
|
<img src="{% static 'dashboard/images/users/avatar-1.jpg' %}" alt="user-image" width="32" class="rounded-circle">
|
||||||
</span>
|
</span>
|
||||||
<span class="d-lg-flex flex-column gap-1 d-none">
|
<span class="d-lg-flex flex-column gap-1 d-none">
|
||||||
<h5 class="my-0">Doris Larson</h5>
|
<h5 class="my-0">Cucha Hernández</h5>
|
||||||
<h6 class="my-0 fw-normal">Founder</h6>
|
<h6 class="my-0 fw-normal">Founder</h6>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user