(function(){ var SERIES_COLORS = ['var(--s1)','var(--s2)','var(--s3)','var(--s4)']; var SERIES_HEX = ['#3987e5','#d95926','#199e70','#9085e9']; var DASH = '—'; var ATTRS = [ {key:'peso', label:'Peso', unit:'g', fmt:function(p){return p.raw.peso+' g';}, score:function(p){return clamp(100-((p.raw.peso-190)/170*100));}}, {key:'portabilidad',label:'Portabilidad (plegado)', unit:'cm', fmt:function(p){return p.plegado+' cm';}, score:function(p){return p.raw.portabilidad;}}, {key:'ajuste', label:'Rango de ajuste', unit:'cm', fmt:function(p){return p.raw.ajusteMin+'–'+p.raw.ajusteMax+' cm';}, score:function(p){return p.raw.ajuste;}}, {key:'rigidez', label:'Rigidez', unit:'', fmt:function(p){return p.rigidezLabel+' — '+p.material;}, score:function(p){return p.raw.rigidez;}}, {key:'terreno', label:'Versatilidad terreno', unit:'', fmt:function(p){return p.terrenoTags;}, score:function(p){return p.raw.terreno;}}, {key:'ergonomia', label:'Ergonomía del puño', unit:'', fmt:function(p){return p.grip;}, score:function(p){return p.raw.ergonomia;}}, {key:'absorcion', label:'Absorción de impactos', unit:'', fmt:function(p){return p.antishock || DASH;}, score:function(p){return p.raw.absorcion;}} ]; function clamp(v){return Math.max(0,Math.min(100,v));} function attrByKey(k){return ATTRS.filter(function(a){return a.key===k;})[0];} var ALL_PRODUCTS = [ {id:'cima', name:'Cima Ultralight', price:'89,90 €', material:'Aluminio', rigidezLabel:'Media', plegado:33, terrenoTags:'Asfalto, senderos preparados', grip:'EVA, recta', antishock:null, raw:{peso:210, rigidez:55, portabilidad:90, terreno:60, ergonomia:70, absorcion:50, ajuste:65, ajusteMin:100, ajusteMax:130}}, {id:'roca', name:'Roca Trail', price:'109,90 €', material:'Aluminio + fibra de vidrio', rigidezLabel:'Media-alta', plegado:60, terrenoTags:'Senderos técnicos, grava, roca', grip:'Corcho natural, contorneado', antishock:'Antishock (muelle interno)', raw:{peso:280, rigidez:80, portabilidad:65, terreno:85, ergonomia:85, absorcion:80, ajuste:90, ajusteMin:65, ajusteMax:140}}, {id:'sendero', name:'Sendero Classic', price:'79,90 €', material:'Fibra de carbono', rigidezLabel:'Alta', plegado:55, terrenoTags:'Senderos técnicos, alta montaña, roca', grip:'EVA, contorneado', antishock:'Antishock (muelle interno, ajustable)', raw:{peso:320, rigidez:90, portabilidad:50, terreno:90, ergonomia:75, absorcion:85, ajuste:95, ajusteMin:60, ajusteMax:145}}, {id:'altitud', name:'Altitud Pro', price:'139,90 €', material:'Fibra de carbono', rigidezLabel:'Media-alta', plegado:38, terrenoTags:'Alta montaña, hielo, nieve, roca', grip:'Corcho natural, contorneado + extensión', antishock:'Antishock (muelle interno, ajustable)', raw:{peso:245, rigidez:85, portabilidad:75, terreno:95, ergonomia:90, absorcion:90, ajuste:80, ajusteMin:70, ajusteMax:135}}, {id:'valle', name:'Valle Compact', price:'64,90 €', material:'Aluminio', rigidezLabel:'Media', plegado:30, terrenoTags:'Asfalto, caminos llanos', grip:'Espuma, recta', antishock:null, raw:{peso:230, rigidez:60, portabilidad:95, terreno:55, ergonomia:65, absorcion:55, ajuste:60, ajusteMin:105, ajusteMax:125}}, {id:'pico', name:'Pico Expedición', price:'159,90 €', material:'Fibra de carbono', rigidezLabel:'Alta', plegado:50, terrenoTags:'Expediciones, nieve, hielo, alta montaña', grip:'Corcho natural, contorneado + extensión larga', antishock:'Antishock (muelle interno, ajustable + tope regulable)', raw:{peso:355, rigidez:95, portabilidad:45, terreno:92, ergonomia:88, absorcion:95, ajuste:88, ajusteMin:55, ajusteMax:150}} ]; var state = { selected: ['cima','roca','sendero'], colors: {cima:0, roca:1, sendero:2}, axes: ['peso','rigidez','portabilidad','terreno','ergonomia'] }; function productsSelected(){ return state.selected.map(function(id){return ALL_PRODUCTS.filter(function(p){return p.id===id;})[0];}); } function colorFor(id){ var i = state.colors[id]; return {css: SERIES_COLORS[i], hex: SERIES_HEX[i]}; } function nextColorSlot(){ var used = Object.keys(state.colors).map(function(k){return state.colors[k];}); for(var i=0;i=4 || state.selected.indexOf(id)!==-1) return; var slot = nextColorSlot(); state.selected.push(id); state.colors[id] = slot; } function removeProduct(id){ state.selected = state.selected.filter(function(x){return x!==id;}); delete state.colors[id]; } var chipbar = document.getElementById('chipbar'); var addInput = document.getElementById('addInput'); var addList = document.getElementById('addList'); var PLACEHOLDER_DEFAULT = 'Escribe para añadir…'; var PLACEHOLDER_CAPPED = 'Descarta un producto antes de añadir otro nuevo al comparador.'; function updateAddInputState(){ var capped = state.selected.length>=4; addInput.classList.toggle('capped', capped); if(document.activeElement!==addInput || addInput.value===''){ addInput.placeholder = capped ? PLACEHOLDER_CAPPED : PLACEHOLDER_DEFAULT; } } function renderChips(){ Array.prototype.slice.call(chipbar.querySelectorAll('.chip')).forEach(function(c){c.remove();}); productsSelected().forEach(function(p){ var c = colorFor(p.id); var chip = document.createElement('div'); chip.className = 'chip'; chip.innerHTML = ''+escapeHtml(p.name)+ '· '+p.price+''+ ''; chip.querySelector('button').addEventListener('click', function(){ removeProduct(p.id); renderAll(); }); chipbar.insertBefore(chip, chipbar.querySelector('.addwrap')); }); var capNote = chipbar.querySelector('.cap-note'); if(state.selected.length>=4){ if(!capNote){ capNote = document.createElement('div'); capNote.className='cap-note'; capNote.textContent='Máximo 4 productos a la vez para que el gráfico se lea con claridad.'; chipbar.appendChild(capNote); } } else if(capNote){ capNote.remove(); } updateAddInputState(); } function renderAddList(query){ addList.innerHTML=''; var capped = state.selected.length>=4; var q = (query||'').toLowerCase().trim(); var pool = ALL_PRODUCTS.filter(function(p){ return state.selected.indexOf(p.id)===-1 && p.name.toLowerCase().indexOf(q)>-1; }); if(pool.length===0){ var d=document.createElement('div'); d.className='empty'; d.textContent='Sin más productos de ejemplo que coincidan.'; addList.appendChild(d); } else { pool.forEach(function(p){ var b=document.createElement('button'); b.type='button'; if(capped) b.classList.add('disabled'); b.setAttribute('aria-disabled', capped ? 'true' : 'false'); var swatchColor = capped ? 'var(--ink-muted)' : SERIES_COLORS[nextColorSlot()]; b.innerHTML=''+escapeHtml(p.name)+' · '+p.price+''; b.addEventListener('click', function(){ if(state.selected.length>=4){ addInput.classList.remove('nudge'); void addInput.offsetWidth; addInput.classList.add('nudge'); addInput.focus(); return; } addProduct(p.id); addInput.value=''; addList.classList.remove('open'); addInput.blur(); renderAll(); }); addList.appendChild(b); }); } } addInput.addEventListener('focus', function(){ renderAddList(addInput.value); addList.classList.add('open'); }); addInput.addEventListener('input', function(){ renderAddList(addInput.value); addList.classList.add('open'); }); document.addEventListener('click', function(e){ if(!addList.contains(e.target) && e.target!==addInput){ addList.classList.remove('open'); } }); function escapeHtml(s){ var d=document.createElement('div'); d.textContent=s; return d.innerHTML; } var svg = document.getElementById('radarSvg'); var chartwrap = document.getElementById('chartwrap'); var tooltip = document.getElementById('tooltip'); var CX=240, CY=225, R=155; var openSwapFor = null; var currentPoints = {}; function axisAngle(i,n){ return -Math.PI/2 + i*(2*Math.PI/n); } function ptOn(i,n,frac){ var a = axisAngle(i,n); return [CX + Math.cos(a)*R*frac, CY + Math.sin(a)*R*frac]; } function buildSvg(){ var n = state.axes.length; var ns = 'http://www.w3.org/2000/svg'; while(svg.firstChild) svg.removeChild(svg.firstChild); [0.25,0.5,0.75,1].forEach(function(frac){ var poly = document.createElementNS(ns,'polygon'); var pts=[]; for(var i=0;i'+attr.fmt(product)+''+ '
'+escapeHtml(product.name)+' · '+attr.label+'
'; tooltip.classList.add('show'); moveTooltip(e); } function moveTooltip(e){ var rect = chartwrap.getBoundingClientRect(); var x,y; if(e.clientX!==undefined){ x=e.clientX-rect.left; y=e.clientY-rect.top; } else { var t=e.target.getBoundingClientRect(); x=t.left-rect.left+t.width/2; y=t.top-rect.top; } tooltip.style.left = x+'px'; tooltip.style.top = (y-10)+'px'; } function hideTooltip(){ tooltip.classList.remove('show'); } function renderAxisChrome(){ Array.prototype.slice.call(chartwrap.querySelectorAll('.axisgroup,.swap-menu')).forEach(function(n){n.remove();}); var n = state.axes.length; state.axes.forEach(function(key,i){ var attr = attrByKey(key); var labelR = R + 40; var a = axisAngle(i,n); var lx = CX + Math.cos(a)*labelR, ly = CY + Math.sin(a)*labelR; var pct = (lx/480*100), pcy=(ly/460*100); var group = document.createElement('div'); group.className='axisgroup'; group.style.left=pct+'%'; group.style.top=pcy+'%'; group.setAttribute('tabindex','0'); group.setAttribute('role','button'); group.setAttribute('aria-label','Cambiar característica del eje '+attr.label); var btn = document.createElement('span'); btn.className='axisbtn'; btn.innerHTML=''; group.appendChild(btn); var label = document.createElement('div'); label.className='axislabel'; label.textContent = attr.label; group.appendChild(label); var menu = buildSwapMenu(i); chartwrap.appendChild(group); chartwrap.appendChild(menu); function toggleMenu(ev){ ev.stopPropagation(); var willOpen = openSwapFor!==i; closeAllSwapMenus(); if(willOpen){ openSwapFor=i; group.classList.add('open'); menu.classList.add('open'); positionMenu(menu, pct, pcy); } } group.addEventListener('click', toggleMenu); group.addEventListener('keydown', function(ev){ if(ev.key==='Enter' || ev.key===' '){ ev.preventDefault(); toggleMenu(ev); } }); }); } function positionMenu(menu, pctLeft, pctTop){ menu.style.left = pctLeft+'%'; menu.style.top = 'calc(' + pctTop + '% + 34px)'; menu.style.transform='translate(-50%,0)'; } function buildSwapMenu(axisIndex){ var menu = document.createElement('div'); menu.className='swap-menu'; var lbl = document.createElement('div'); lbl.className='lbl'; lbl.textContent='Mostrar en este eje'; menu.appendChild(lbl); var others = ATTRS.filter(function(a){ return state.axes.indexOf(a.key)===-1; }); others.forEach(function(a){ var b=document.createElement('button'); b.type='button'; b.textContent=a.label; b.addEventListener('click', function(){ state.axes[axisIndex]=a.key; closeAllSwapMenus(); renderAll(); }); menu.appendChild(b); }); return menu; } function closeAllSwapMenus(){ openSwapFor=null; Array.prototype.forEach.call(document.querySelectorAll('.swap-menu'), function(m){m.classList.remove('open');}); Array.prototype.forEach.call(document.querySelectorAll('.axisbtn'), function(b){b.classList.remove('open');}); } document.addEventListener('click', closeAllSwapMenus); function renderLegend(){ var legend = document.getElementById('legend'); legend.innerHTML=''; productsSelected().forEach(function(p){ var col = colorFor(p.id); var item = document.createElement('div'); item.className='legend-item'; item.innerHTML=''+escapeHtml(p.name); legend.appendChild(item); }); } var ICON_CLOSE = ''; function initColumnDrag(th, productId){ function clientX(e){ return e.touches ? e.touches[0].clientX : e.clientX; } function onStart(e){ if(e.target.closest('.colbtn-remove')) return; if(e.type==='mousedown' && e.button!==0) return; var table = th.closest('table'); var colCount = productsSelected().length; var fromIdx = parseInt(th.getAttribute('data-colindex'), 10); var startX = clientX(e); var moved = false; var isTouch = e.type==='touchstart'; var toIdx = fromIdx; function cellsFor(i){ return Array.prototype.slice.call(table.querySelectorAll('[data-colindex="'+i+'"]')); } var colRects = []; for(var i=0;i=colRects[j].left && x<=colRects[j].right) return j; } if(x < colRects[0].left) return 0; if(x > colRects[colRects.length-1].right) return colRects.length-1; return fromIdx; } function applyShift(target){ var w = colRects[fromIdx].width; for(var j=0;jfromIdx && j<=target) tx=-w; else if(fromIdx>target && j>=target && j6){ moved=true; startDragVisual(); } if(!moved) return; if(isTouch && ev.cancelable) ev.preventDefault(); cellsFor(fromIdx).forEach(function(c){ c.style.transform = 'translateX('+dx+'px)'; }); toIdx = findToIndex(x); applyShift(toIdx); } function finish(ev){ document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', finish); document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', finish); document.removeEventListener('touchcancel', finish); var x = ev.changedTouches ? ev.changedTouches[0].clientX : ev.clientX; if(!moved && Math.abs(x-startX)>6){ moved=true; toIdx=findToIndex(x); } cleanupVisual(); if(moved && toIdx!==fromIdx){ reorderProductByIndex(fromIdx, toIdx); } if(moved) renderAll(); } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', finish); document.addEventListener('touchmove', onMove, {passive:false}); document.addEventListener('touchend', finish); document.addEventListener('touchcancel', finish); } th.addEventListener('mousedown', onStart); th.addEventListener('touchstart', onStart, {passive:true}); } function reorderProductByIndex(fromIdx, toIdx){ var arr = state.selected; if(fromIdx<0 || fromIdx>=arr.length || toIdx<0 || toIdx>=arr.length) return; var id = arr[fromIdx]; arr.splice(fromIdx,1); arr.splice(toIdx,0,id); } function renderTable(){ var table = document.getElementById('specTable'); var prods = productsSelected(); table.innerHTML=''; var thead = document.createElement('thead'); var headRow = document.createElement('tr'); var thFirst = document.createElement('th'); thFirst.className='pcol'; thFirst.textContent='Característica'; headRow.appendChild(thFirst); prods.forEach(function(p, colIndex){ var col = colorFor(p.id); var th = document.createElement('th'); th.className = 'pcol-head'; th.setAttribute('data-pid', p.id); th.setAttribute('data-colindex', colIndex); th.setAttribute('title', 'Arrastra para reordenar'); var removeBtn = document.createElement('button'); removeBtn.type='button'; removeBtn.className='colbtn colbtn-remove'; removeBtn.innerHTML = ICON_CLOSE; removeBtn.setAttribute('aria-label','Quitar '+p.name+' del comparador'); removeBtn.addEventListener('click', function(){ removeProduct(p.id); renderAll(); }); th.appendChild(removeBtn); var info = document.createElement('div'); info.className = 'pcolinfo'; var pname = document.createElement('div'); pname.className='pname'; var swatch = document.createElement('span'); swatch.className='swatch'; swatch.style.background=col.css; pname.appendChild(swatch); pname.appendChild(document.createTextNode(p.name)); info.appendChild(pname); var psep = document.createElement('span'); psep.className='psep'; psep.textContent = '·'; info.appendChild(psep); var pprice = document.createElement('span'); pprice.className='pprice'; pprice.textContent = p.price; info.appendChild(pprice); th.appendChild(info); initColumnDrag(th, p.id); headRow.appendChild(th); }); thead.appendChild(headRow); table.appendChild(thead); var tbody = document.createElement('tbody'); ATTRS.forEach(function(attr){ var tr = document.createElement('tr'); var th = document.createElement('th'); th.textContent = attr.label; tr.appendChild(th); prods.forEach(function(p, colIndex){ var td = document.createElement('td'); td.textContent = attr.fmt(p); td.setAttribute('data-colindex', colIndex); tr.appendChild(td); }); tbody.appendChild(tr); }); table.appendChild(tbody); } function renderAll(){ renderChips(); buildSvg(); renderLegend(); renderTable(); } renderAll(); })();