Download app.js from elvis-hf/ensemble: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/spaces/elvis-hf/ensemble/resolve/main/app.js
- Command line
-
hf download hf://spaces/elvis-hf/ensemble/app.js
-
curl -L -o app.js https://huggingface.co/spaces/elvis-hf/ensemble/resolve/main/app.js
12.3 kB
| const $=id=>document.getElementById(id); | |
| let points=[],models=[],label=-1,drawing=false,last=null,keyboard={x:.5,y:.5},showCursor=false; | |
| const colors={'-1':'#557cd4','1':'#e39a57','2':'#42a58b'}; | |
| const classNames={'-1':'A','1':'B','2':'C'},regionColors={'-1':'#e1e9fb','1':'#fae8d8','2':'#dbf1e9'}; | |
| const modelRegionColors={'-1':'#eaf0fc','1':'#fcefe4','2':'#e6f5f0'}; | |
| let hoveredModel=null,backendReady=false,busy=false,revision=0,classifiers=[]; | |
| let backend; | |
| let customPoints=[],datasetType='custom'; | |
| function isCustomDataset(){return datasetType==='custom';} | |
| function sampleWeights(){return $('method').value==='boosting'&&points.length?Ensemble.weightsFor(points,models):null;} | |
| function pointRadius(weight){return 4.5*Math.sqrt(weight===undefined?1:weight*points.length);} | |
| function hideTooltip(){ $('point-tooltip').hidden=true; } | |
| function hoverPoint(e){ | |
| const weights=sampleWeights(); | |
| if(!weights){hideTooltip();return;} | |
| const rect=$('boundary').getBoundingClientRect(),w=rect.width-2,h=rect.height-2; | |
| const x=e.clientX-rect.left-1,y=e.clientY-rect.top-1; | |
| let hit=-1,best=Infinity; | |
| points.forEach((p,i)=>{ | |
| const distance=Math.hypot(p.x*w-x,(1-p.y)*h-y); | |
| if(distance<=Math.max(6,pointRadius(weights[i]))&&distance<best){hit=i;best=distance;} | |
| }); | |
| if(hit<0){hideTooltip();return;} | |
| const p=points[hit],tip=$('point-tooltip'); | |
| tip.textContent=`Class ${classNames[p.label]} · x: ${p.x.toFixed(3)} · y: ${p.y.toFixed(3)}\nSample weight: ${weights[hit].toPrecision(4)} (${(weights[hit]*100).toFixed(2)}%)`; | |
| tip.hidden=false; | |
| tip.style.left=`${Math.max(8,Math.min(e.clientX+14,window.innerWidth-tip.offsetWidth-8))}px`; | |
| tip.style.top=`${Math.max(8,Math.min(e.clientY+14,window.innerHeight-tip.offsetHeight-8))}px`; | |
| } | |
| function previewModel(index){hoveredModel=index;hideTooltip();paint($('boundary'),true);} | |
| function paint(canvas,boundary=false) { | |
| const rect=canvas.getBoundingClientRect?.(); | |
| // Draw in CSS pixels, with a separate high-resolution backing buffer. | |
| // Account for the canvas's 1px border on each side. | |
| const w=rect?rect.width-2:canvas.width,h=rect?rect.height-2:canvas.height; | |
| if(w<=0||h<=0) return; | |
| const ratio=globalThis.devicePixelRatio||1; | |
| const bufferWidth=Math.max(1,Math.round(w*ratio)),bufferHeight=Math.max(1,Math.round(h*ratio)); | |
| if(canvas.width!==bufferWidth) canvas.width=bufferWidth; | |
| if(canvas.height!==bufferHeight) canvas.height=bufferHeight; | |
| const ctx=canvas.getContext('2d'); | |
| ctx.setTransform(bufferWidth/w,0,0,bufferHeight/h,0,0); | |
| ctx.clearRect(0,0,w,h); | |
| const preview=hoveredModel===null?null:models[hoveredModel]; | |
| if(boundary&&models.length) for(let x=0;x<w;x+=6) for(let y=0;y<h;y+=6) { | |
| const point={x:x/w,y:1-y/h}; | |
| ctx.fillStyle=preview?modelRegionColors[Ensemble.predict(preview.tree,point)]:regionColors[Ensemble.vote(models,point)];ctx.fillRect(x,y,6,6); | |
| } | |
| ctx.strokeStyle='#e9edf4';ctx.lineWidth=1; | |
| for(let i=1;i<10;i++){ctx.beginPath();ctx.moveTo(i*w/10,0);ctx.lineTo(i*w/10,h);ctx.moveTo(0,i*h/10);ctx.lineTo(w,i*h/10);ctx.stroke();} | |
| const weights=sampleWeights(); | |
| points.forEach((p,i)=>{ | |
| // Circle area is proportional to sample weight; uniform weights keep the default size. | |
| const radius=pointRadius(weights?.[i]); | |
| ctx.beginPath();ctx.arc(p.x*w,(1-p.y)*h,radius,0,2*Math.PI);ctx.fillStyle=colors[p.label];ctx.fill();ctx.strokeStyle='white';ctx.lineWidth=Math.min(1.3,radius/3);ctx.stroke(); | |
| }); | |
| if(showCursor){ctx.strokeStyle='#26334c';ctx.strokeRect(keyboard.x*w-7,(1-keyboard.y)*h-7,14,14);} | |
| } | |
| function render(message) { | |
| hoveredModel=null;hideTooltip(); | |
| paint($('boundary'),true); | |
| $('count').textContent=models.length;$('points').textContent=points.length; | |
| $('accuracy').textContent=models.length?`${Math.round(100*points.filter((p,i)=>Ensemble.trainingVote(models,i)===p.label).length/points.length)}%`:'—'; | |
| $('empty').hidden=!!models.length || !!points.length; | |
| $('class-count').textContent=[-1,1,2].map(c=>`${classNames[c]}: ${points.filter(p=>p.label===c).length}`).join(' · '); | |
| $('remove').disabled=busy||!models.length;$('undo').disabled=!isCustomDataset()||!points.length;$('clear').disabled=!isCustomDataset()||!points.length; | |
| $('add').disabled=busy||!backendReady||new Set(points.map(p=>p.label)).size<2||models.length>=100||($('method').value==='boosting'&&!classifiers.find(c=>c.name===$('base').value)?.weighted); | |
| $('method-badge').textContent={boosting:'AdaBoost',bagging:'Bagging',random_forest:'Random Forest',extra_trees:'Extra Trees'}[$('method').value]; | |
| $('models').replaceChildren(...models.map((m,i)=>{ | |
| const el=document.createElement('span');el.className='model-chip';el.tabIndex=0; | |
| const errorRate=100*points.filter((p,j)=>m.predictions[j]!==p.label).length/points.length; | |
| const weights=$('method').value==='boosting'?Ensemble.weightsFor(points,models.slice(0,i)):null; | |
| const weightedError=weights?100*points.reduce((sum,p,j)=>sum+(m.predictions[j]!==p.label?weights[j]:0),0):errorRate; | |
| el.textContent=`${m.forest?'Tree':'Model'} ${i+1}`;el.title=`Training error: ${errorRate.toFixed(1)}%\nWeighted error: ${weightedError.toFixed(1)}%\nVote weight: ${m.alpha.toFixed(3)}`; | |
| el.onmouseenter=el.onfocus=()=>previewModel(i); | |
| el.onmouseleave=el.onblur=()=>previewModel(null); | |
| return el; | |
| })); | |
| if(message!==undefined) $('status').textContent=message; | |
| } | |
| function reset(){revision++;models=[];render('Ensemble reset. Add a model when your dataset is ready.');} | |
| function addPoint(p){if(!isCustomDataset())return;if(points.length>=1500){render('Dataset limit reached (1,500 points).');return;}points.push({...p,label});revision++;models=[];render('Dataset updated. Add a model to start training.');} | |
| document.querySelectorAll('[data-label]').forEach(button=>button.onclick=()=>{label=Number(button.dataset.label);document.querySelectorAll('[data-label]').forEach(b=>{b.classList.toggle('selected',b===button);b.setAttribute('aria-pressed',String(b===button));});}); | |
| function position(e){const r=$('boundary').getBoundingClientRect();return {x:Math.max(0,Math.min(1,(e.clientX-r.left-1)/(r.width-2))),y:Math.max(0,Math.min(1,1-(e.clientY-r.top-1)/(r.height-2)))};} | |
| $('boundary').onpointerdown=e=>{if(e.button!==0||!isCustomDataset())return;drawing=true;showCursor=false;$('boundary').setPointerCapture(e.pointerId);last=position(e);addPoint(last);}; | |
| $('boundary').onpointermove=e=>{if(!drawing){hoverPoint(e);return;}const p=position(e);if(Math.hypot(p.x-last.x,p.y-last.y)>.025){last=p;addPoint(p);}}; | |
| $('boundary').onpointerleave=hideTooltip; | |
| for(const event of ['pointerup','pointercancel','lostpointercapture']) $('boundary').addEventListener(event,()=>{drawing=false;last=null;}); | |
| $('boundary').onkeydown=e=>{if(!isCustomDataset())return;const moves={ArrowLeft:[-.02,0],ArrowRight:[.02,0],ArrowUp:[0,.02],ArrowDown:[0,-.02]};if(moves[e.key]){e.preventDefault();showCursor=true;keyboard.x=Math.max(0,Math.min(1,keyboard.x+moves[e.key][0]));keyboard.y=Math.max(0,Math.min(1,keyboard.y+moves[e.key][1]));paint($('boundary'),true);}else if(e.key===' '||e.key==='Enter'){e.preventDefault();addPoint(keyboard);}}; | |
| $('undo').onclick=()=>{if(!isCustomDataset())return;points.pop();reset();};$('clear').onclick=()=>{if(!isCustomDataset())return;points=[];reset();}; | |
| $('dataset-type').onchange=()=>{ | |
| if(isCustomDataset())customPoints=points.map(p=>({...p})); | |
| datasetType=$('dataset-type').value; | |
| points=isCustomDataset()?customPoints.map(p=>({...p})):Datasets.create(datasetType); | |
| drawing=false;last=null;showCursor=false; | |
| document.querySelectorAll('[data-label]').forEach(button=>{button.disabled=!isCustomDataset();}); | |
| $('boundary').style.cursor=isCustomDataset()?'crosshair':'default'; | |
| $('boundary').setAttribute('aria-label',isCustomDataset()?'Draw training points and view the ensemble decision boundary. Click or drag to add the selected class. Arrow keys move the keyboard cursor; Space or Enter adds a point.':'View the selected dataset and ensemble decision boundary.'); | |
| $('dataset-help').textContent=isCustomDataset()?'Draw on the decision-boundary plot':datasetType==='xor'?'For AdaBoost with decision trees, use {"max_depth": 2}.':'Select Custom to draw your own dataset.'; | |
| reset(); | |
| }; | |
| function updateClassifiers(){ | |
| const forest=['random_forest','extra_trees'].includes($('method').value); | |
| $('base').disabled=forest||!backendReady; | |
| $('params-help').textContent=forest?'Forest arguments, e.g. {"max_depth": 3, "max_features": "sqrt"}. Add/Remove sets the tree count.':'Use {} for defaults. Example: {"max_depth": 2}'; | |
| $('add').textContent=forest?'+ Add one tree':'+ Add one model'; | |
| $('remove').textContent=forest?'↶ Remove last tree':'↶ Remove last model'; | |
| const selected=forest?'DecisionTreeClassifier':$('base').value; | |
| $('base').replaceChildren(...classifiers.filter(c=>!['ExtraTreeClassifier','ExtraTreesClassifier'].includes(c.name)).map(c=>{ | |
| const option=document.createElement('option');option.value=c.name; | |
| option.textContent=c.name+(c.weighted?'':' (bagging only)'); | |
| option.disabled=$('method').value==='boosting'&&!c.weighted; | |
| return option; | |
| })); | |
| $('base').value=selected; | |
| if(!classifiers.some(c=>c.name===selected&&($('method').value!=='boosting'||c.weighted))){ | |
| $('base').value='DecisionTreeClassifier';$('params').value='{}'; | |
| } | |
| } | |
| $('method').onchange=()=>{if(['random_forest','extra_trees'].includes($('method').value)||$('base').disabled)$('params').value='{}';updateClassifiers();reset();}; | |
| $('base').onchange=()=>{$('params').value='{}';reset();}; | |
| $('params').oninput=reset; | |
| $('seed').oninput=()=>{$('seed').removeAttribute('aria-invalid');reset();}; | |
| $('add').onclick=async()=>{ | |
| if($('add').disabled)return; | |
| const seed=Number($('seed').value); | |
| if(!$('seed').value.trim()||!Number.isInteger(seed)||seed<0||seed>4294967295){ | |
| $('seed').setAttribute('aria-invalid','true'); | |
| render('Invalid random seed: enter an integer from 0 to 4294967295.');return; | |
| } | |
| $('seed').removeAttribute('aria-invalid'); | |
| let params; | |
| try{ | |
| params=JSON.parse($('params').value.trim()||'{}'); | |
| if(!params||Array.isArray(params)||typeof params!=='object')throw new Error('Enter a JSON object for classifier arguments.'); | |
| $('params').removeAttribute('aria-invalid'); | |
| }catch(error){$('params').setAttribute('aria-invalid','true');render(`Invalid arguments: ${error.message}`);return;} | |
| const currentRevision=revision; | |
| const request={points:points.map(p=>({...p})),count:models.length,method:$('method').value,classifier:$('base').value,params,seed}; | |
| busy=true;render('Training one scikit-learn model…'); | |
| try{ | |
| const model=await backend.add(request); | |
| if(currentRevision!==revision)return; | |
| models.push(model); | |
| render(`Model ${models.length} added.${model.perfect?' Perfect fit; further models may leave the boundary unchanged.':''}${model.warning?' '+model.warning:''}`); | |
| }catch(error){if(currentRevision===revision)render(`Training failed: ${error.message}`);} | |
| finally{busy=false;render();} | |
| }; | |
| $('remove').onclick=()=>{models.pop();render('Last model removed. The previous ensemble is restored.');}; | |
| render('Choose a class and draw points on the decision-boundary plot, or select a built-in dataset.'); | |
| // Match the drawing buffer to the responsive plot so points remain circular. | |
| if(typeof ResizeObserver!=='undefined') { | |
| new ResizeObserver(()=>paint($('boundary'),true)).observe($('boundary')); | |
| } | |
| function setRuntimeStatus(state,text){ | |
| $('runtime-status').setAttribute('data-state',state); | |
| $('runtime-status-text').textContent=text; | |
| } | |
| setRuntimeStatus('loading','Loading Pythong runtime...'); | |
| backend=new PythonBackend(message=>{if(!backendReady)$('status').textContent=message;}); | |
| $('status').textContent='Loading Python and scikit-learn… You can draw while they load.'; | |
| backend.ready.then(available=>{ | |
| classifiers=available;backendReady=true;$('base').disabled=false;updateClassifiers(); | |
| setRuntimeStatus('ready','Python runtime ready'); | |
| render('scikit-learn is ready. Draw at least two classes and add a model.'); | |
| }).catch(error=>{ | |
| backendReady=false; | |
| setRuntimeStatus('error','Python runtime failed to load'); | |
| render(`Could not load Python: ${error.message} Serve this app over HTTP and reload to retry.`); | |
| }); | |