User:WhoSaidThat/RDSet.js

// This instances of RDSet objects can be used to filter the data in a RevisionData object, produce pie charts, lists and graphs, and preform // calculations based on the revisons data.

//	function	RDSet		RevisionData object		Array of Strings/String

function RDSet(revsObj,filters){

if(!filters) filters='';

this.data=revsObj; this.graphs=[];

var r=this.data.revisions; this.users={}; for(var i=0; i$2' ) str=str.replace(/\[\[([^\]]+)\]\]/g,'$1' ) return str; }

this.calculateContributions=function{ for(var i=0; i0) this.users[r[i].user].add+=c; if(c<0) this.users[r[i].user].removed+=-1*c; this.users[r[i].user].absContrib+=Math.abs(c); this.users[r[i].user].netContrib+=c; }		}	}

this.newUserPieChart=function(size){

this.calculateContributions;

var hs=size/2; var l=[]; var t=0; for(var i in this.users){ this.users[i].user=i; if( !this.users[i].bad ){ l.push(this.users[i]); t+=Math.abs(this.users[i].netContrib); }		}		l.sort(function(a,b){ return Math.abs(a.netContrib)>Math.abs(b.netContrib); });

var can=document.createElement('canvas'); can.width=size; can.height=size;

var c=can.getContext('2d'); c.fillStyle='#ffffff'; c.fillRect(0,0,size,size);

c.moveTo(hs,hs); c.fillStyle='#000000'; var a=0; while(l.length>0){ var n=l.pop; var w=Math.PI*2*Math.abs(n.netContrib)/t; //alert(n.user+' '+n.absContrib+' '+a+' '+w); c.moveTo(hs,hs); c.beginPath; c.arc(hs,hs,hs*0.9,a,a+w,false); a+=w; c.lineTo(hs,hs); c.closePath; c.stroke; c.fillStyle=n.netContrib>0?'rgba(200,250,200,0.5)':'rgba(150,100,100,0.5)'; c.fill; c.fillStyle='#003355'; var ta=a-w/2; if(w>0.3){ var tw=c.measureText(n.user).width; c.fillText(n.user,-tw/2+hs+hs*Math.cos(ta)*0.75,hs+hs*Math.sin(ta)*0.75); }		}		return can; }

this.newRevisionsList=function{ var r=this.data.revisions; var m=' ';

var tR=this.getTimeRange; for(var i=0; i=tR.min && new Date(r[i].timestamp)<=tR.max){ if(r[i].bad) c='filtered'; else c='';

var u='http://en.wikipedia.org/w/index.php?title='+ this.data.page.replace(/ /g,'_') + '&diff=prev&oldid=' + r[i].revid;

m += ' '+r[i].revid+' '; m += ' '+ r[i].timestamp +' ';

m += ' '+r[i].user+' '; m += ' '+r[i].size+' ('+r[i].contrib+') '; m += ' '+ this.parseWikiText(r[i].comment)+' ';

}		}		return $(m+' '); }

this.newGraph=function(canvas,width,height,clickEvent){ canvas=document.createElement('canvas'); /*if(typeof canvas.getContext != 'function'){ var c=$(canvas).get; if(canvas.indexOf('#')==0){ alert('new '+c+'n'); c=$(' ').get; }			canvas=c; }*/

if(typeof canvas.getContext != 'function') alert(canvas+' not a canvas '+(typeof canvas.getContext));

var g=new RDGraph(this, canvas, width, height);

g.attachClickEvent(clickEvent);

this.graphs.push(g);

return g.canvas;

}

this.redrawGraphs=function{ for(var i=0; i=0){

for(var i=0; i<filters.length; i++){ switch(filters[i]){

case 'minor': this.filterSmall(true); break; case 'small': this.filterSmall(pA,pB); break;

case 'major': this.filterLarge(true); break; case 'large': this.filterLarge(pA,pB); break;

case 'last-7-days': this.filterTime(today-846e5*7,today); break; case 'last-30-days': this.filterTime(today-846e5*30,today); break; case 'last-90-days': this.filterTime(today-846e5*90,today); break; case 'last-365-days': this.filterTime(today-846e5*365,today); break; case 'first-year': this.filterTime(firstday,firstday+846e5*365); break; case 'time': this.filterTime(pA,pB); break;

case 'outliers': this.filterOutlierEdits(pA,pB); break; case 'outliers-b': this.filterOutlierContributions(pA,pB); break; case 'mentioned': this.filterMentionedUsers(pA,pB); break; case 'reverts': this.filterRevertEdits(pA,pB); break; case 'ip': this.filterIPEdits(pA,pB); break; case 'ip-only': this.filterIPEdits(true,pA,pB); break;

case 'clear': this.clearFilters(pA,pB); break; case 'reset': case 'show-all': this.clearFilters(false,false); break; case 'clear-user-record-only': this.clearFilters(false,true); break;

case 'clear-revisons-only': this.clearFilters(true,false); break; }			}		}/*else if(typeof filters == 'object'){ // Unfinished support for filter objects specifying individual paramters

for(var i in filters){ switch(i){

case 'last-7-days': this.filterTime(today-846e5*7,today); break; case 'last-30-days': this.filterTime(today-846e5*30,today); break; case 'last-365-days': this.filterTime(today-846e5*365,today); break; case 'first-year': this.filterTime(firstday,firstday+846e5*365); break; case 'time': this.filterTime(pA,pB); break;

case 'outliers': this.filterOutlierEdits(pA,pB); break; case 'outliers-b': this.filterOutlierContributions(pA,pB); break; case 'mentioned': this.filterMentionedUsers(pA,pB); break; case 'ip': this.filterIPEdits(pA,pB); break; case 'ip-only': this.filterIPEdits(true,pA,pB); break; case 'reverts': this.filterRevertEdits(pA,pB); break; case 'clear': this.clearFilters(pA,pB); break; case 'reset': case 'show-all': this.clearFilters(false,false); break; case 'clear-user-record-only': this.clearFilters(false,true); break;

case 'clear-revisons-only': this.clearFilters(true,false); break; }			}

}*/

}

this.load=function(after,loadAll){

this.afterLoading=after;

if(!loadAll){

if(this.msg)	alert('loading for authors');

this.loadAuthorsMin; return; }		this.loading=0; var r=this.data.revisions; var t=0; for(var i=0; i0; i--){ while(i>0 && r[i].user==r[i-1].user ) i--; if((typeof r[i].content != 'string') && !r[i].bad && !this.users[r[i].user].bad){ // this.data.loadRevision(i,function(a,b){ b.loading++; },null,this); t++; }		}		this.loadingTarget=t; }

this.clearFilters=function(leaveUserRecord,opposite){ if(!leaveUserRecord ^ opposite){ for(var i in this.users){ this.users[i].bad=false; }		}		if(!opposite){ var r=this.data.revisions; for(var i=0; i<r.length; i++){ r[i].bad=false; }		}	}

this.filterTime=function(t0,t1){

var r=this.data.revisions;

var uintime={};

if(!t0) t0=(new Date(r[r.length-1].timestamp)).getTime; if(!t1) t1=(new Date(r[0].timestamp)).getTime;

if(typeof t0 == 'object') t0=t0.getTime; if(typeof t1 == 'object') t1=t1.getTime;

for(var i=0; i<r.length; i++){ var tI=(new Date(r[i].timestamp)).getTime; if(tI<t0 || tI>t1){ r[i].outoftime=true; //r[i].bad=true; }else{r[i].outoftime=false; uintime[r[i].user]=true; }		}

/*for(var i in this.users){ if(!uintime[i]) this.users[i].bad=true; } this.load;*/ }

this.filterMentionedUsers=function(pA,pB){ pA=pA>=1?pA:1; pB=pB?true:false;

for(var i in this.users) this.users[i].mentions=0;

var r=this.data.revisions; for(var i=0; i<r.length; i++){ if(r[i].comment){ var p=r[i].comment.indexOf('[[Special:Contributions/');				if(p>=0){					p+=('[[Special:Contributions/').length;					var q=r[i].comment.indexOf('|',p);					var un=r[i].comment.substring(p,q);					if(this.users[un]) this.users[un].mentions++;				}			}		}

for(var i=0; i<r.length; i++){ if(this.users[r[i].user].mentions>pA){ this.users[r[i].user].bad=true; if(!pB) r[i].bad=true; }		}	}	this.filterRevertEdits=function(pA,pB){ var r=this.data.revisions; for(var i=0; i<r.length; i++){ if(r[i].comment){ var p=r[i].comment.indexOf('[[Special:Contributions/');				if(p>=0) r[i].bad=true;			}		}

}

this.filterSmall=function(threshold,range){ if(threshold === true){

var r=this.data.revisions; for(var i=0; i<r.length; i++){ if(r[i].minor) r[i].bad=true; }		}else{ threshold=threshold>0?threshold:35;

var r=this.data.revisions; for(var i=0; i<r.length; i++){ if(Math.abs(r[i].contrib)<threshold) r[i].bad=true; }		}

}	this.filterLarge=function(threshold,range){ if(threshold === true){

var r=this.data.revisions; for(var i=0; i<r.length; i++){ if(!r[i].minor) r[i].bad=true; }

}else{ threshold=threshold>0?threshold:150;

var r=this.data.revisions; for(var i=0; i<r.length; i++){ if(Math.abs(r[i].contrib)>threshold) r[i].bad=true; }		}

}

this.filterOutlierEdits=function(threshold,range){ range=range>=0?range:11; threshold=threshold>0?threshold:0.1;

var r=this.data.revisions; for(var i=0; i<r.length; i++){ if(!r[i].bad){ var s=0; var k=0; for(var j=-range; j<=range; j++){ if(j!=0 && i+j<r.length && j+i>=0 && !r[i+j].bad){ s+=r[i+j].size; k++; }				}				r[i].rollingMean=s/k; }		}

for(var i=0; i<r.length; i++){ var deviation=Math.abs(r[i].size-r[i].rollingMean)/r[i].rollingMean; if(!r[i].bad && deviation>threshold) r[i].bad=true; }	}

this.filterOutlierContributions=function(threshold ,range){ range=range>=0?range:11; threshold=threshold>0?threshold:0.45;

var r=this.data.revisions; for(var i=0; i<r.length; i++){ //if(!r[i].bad){ var s=0; var k=0; for(var j=-range; j<=range; j++){ if(j!=0 && i+j<r.length && j+i>=0 ){ //&& !r[i+j].bad){

s+=Math.abs(r[i+j].contrib); k+=1; }				}				r[i].rollingContributionMean=s/k; //}		}

for(var i=0; i<r.length; i++){ var spf=15*Math.abs(r[i].contrib)/r[i].size; var deviationFromMean=spf*Math.abs(r[i].contrib-r[i].rollingContributionMean)/Math.abs(r[i].rollingContributionMean); r[i].cOutlierDev=deviationFromMean; if(!r[i].bad && deviationFromMean>threshold){ this.users[r[i].user].bad=true; r[i].bad=true; } }	}

this.filterIPEdits=function(onlyIP){ onlyIP=onlyIP?true:false; var r=this.data.revisions; for(var i=0; i<r.length; i++){ var found=r[i].user.match(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/); if(found) found=true; else found=false;

if( onlyIP ^ found){ this.users[r[i].user].bad=true; r[i].bad=true; }		}	}

this.getRange=function(vname,all,opposite){

var r=this.data.revisions; var vmax=r[0][vname]; var vmin=vmax;

for(var i=0; i<r.length; i++){ if(all || ((!r[i].bad && !r[i].outoftime) ^ opposite) ){ var v=r[i][vname]; if(v>vmax) vmax=v; if(v<vmin) vmin=v; }		}

return {'max':vmax,'min':vmin,'range':vmax-vmin}; }

this.getTimeRange=function(all,opposite){

var r=this.data.revisions; var vmax=new Date(r[0].timestamp); var vmin=vmax;

for(var i=0; i<r.length; i++){ if(all || ((!r[i].bad && !r[i].outoftime) ^ opposite)){ var v=new Date(r[i].timestamp); if(v>vmax) vmax=v; if(v<vmin) vmin=v; }		}

return {'max':vmax,'min':vmin,'range':vmax-vmin}; }

this.getRevisions=function(all,opposite){ all=all?true:false; opposite=opposite?true:false; var res=[]; for(var i=0; i<this.data.revisions.length; i++){ var r=this.data.revisions[i]; if(all || ((!r.bad && !r.outoftime) ^ opposite)) res.push(r); }		return res; }

this.countUsersInRange=function(all,opposite){ all=all?true:false; opposite=opposite?true:false;

var foundUsers={};

var r=this.data.revisions; for(var i=0; i<r.length; i++){ if( !r[i].outoftime && !r[i].bad ){ foundUsers[r[i].user]=true; }		}		var k=0; for(var i in foundUsers){ if( (all || (!this.users[i].bad ^ opposite))) k++; }		return k;	} this.countUsers=function(all,opposite){ all=all?true:false; opposite=opposite?true:false; var k=0; for(var i in this.users){ if(all || (!this.users[i].bad ^ opposite)) k++; }		return k;	}

this.countRevisions=function(all,opposite){ var k=0; var r=this.data.revisions; for(var i=0; i<r.length; i++){ if(all || ((!r[i].bad && !r[i].outoftime) ^ opposite)) k++; }		return k;	} this.countAllRevisions=function(all,opposite){ var k=0; var r=this.data.revisions; for(var i=0; i<r.length; i++){ if(all || ((!r[i].bad ) ^ opposite)) k++; }		return k;	}

this.getRevisonNearestTime=function(t){ if(typeof t == 'object') t=t.getTime; var m=Math.abs((new Date).getTime-t); var j=0; var r=this.data.revisions; for(var i=0; i<r.length; i++){ var d=Math.abs((new Date(r[i].timestamp)).getTime-t); if(d<m){ m=d; j=i; }		}		return r[j]; }

this.loadWordAuthors=function{

var key='authors'+this.data.page.replace(/ /g,'_');

var d=localStorage.getItem(key); if(d){ if(this.msg)	alert('found');		this.wordAuthors=JSON.parse(d); }else{ if(this.msg) alert('loading '+key+' not found');

if(this.buildAuthorship){ var fn=function{ this.findAuthors; } }else{ var fn=function{ this.loadingDone=true; } }

this.load(fn); return true; }

}

this.findAuthors=function{

if(this.msg) alert('find em'); var curText=this.cleanUpText(this.data.revisions[0].content); var words=curText.split(' ');

var soughtWords={}; var foundWords={};

for(var i=0; i<words.length; i++){ var wn=words[i]; if(i>0) wn=words[i-1]+' '+wn; if(i<words.length-1) wn+=' '+words[i+1]; soughtWords[wn]=true; }

var r=this.data.revisions; for(var i=r.length-1; i>=0; i--){ if((typeof r[i].content == 'string')){ var tt=this.cleanUpText(r[i].content); for(var j in soughtWords){ if(soughtWords[j] && tt.indexOf(j)>=0){ foundWords[j]=r[i]; soughtWords[j]=false; }				}			}		}

this.wordAuthors=foundWords;

if(this.cacheAuthors){ var key='authors'+this.data.page.replace(/ /g,'_'); localStorage.setItem(key,JSON.stringify(foundWords)); if(this.msg) alert('saving '+key+''); }

} this.wordAuthors={}; this.cleanUpText=function(t){ var tt=t; tt=tt.replace(/\[/g,''); tt=tt.replace(/\]/g,''); tt=tt.replace(/\|/g,' '); tt=tt.replace(/ /g,' '); return tt; }

this.findFirstOccurence=function(str){

if(this.wordAuthors[str]){ if(this.msg) alert('using pre determined'); return this.wordAuthors[str];

}

var r=this.data.revisions; var k=0; var s=0;

for(var i=r.length-1; i>=0; i--){ if((typeof r[i].content == 'string')){ k++; var tt=this.cleanUpText(r[i].content); if(tt.indexOf(str)>=0) return r[i]; else s+=r[i].content.length; }		} if(this.msg) alert(k+' '+str+' '+s); }

this.filter(filters);

}

// This section should really have its own file, but RDGraph requires RDSet and vice versa so they are staying together

// The RDGraph object can plot multi variable time data series // it has its own event attachment methods that allow variable values equivalent to mouse click coordinates to be calculated // and nearest data point to be identified and passed to the attached event function. // The idea being that the road is smoothed a little of the interface designer who can access these handy precalculated values. // multiple functions can be attached for each event and are executed in the order they are attached.

function RDGraph(dataSource,canvas,width,height){

this.dataSource=dataSource;

canvas.width=width; canvas.height=height;

canvas.graphClicks=[]; canvas.graphDoubleClicks=[]; canvas.graphMoves=[]; canvas.graphObject=this; this.canvas=canvas;

this.cursorCol='#000000'; this.cursorIndex=0;

this.moveCursor=function(n){

this.cursorIndex-=n; if(this.cursorIndex<0) this.cursorIndex=0; if(this.cursorIndex>=this.data.length) this.cursorIndex=this.data.length-1;

}

this.vars=null;

$(canvas).click(function(e){

this.graphObject.event=e;

var x=e.pageX-$(e.target).offset.left; var y=e.pageY-$(e.target).offset.top

var coords={ 'x':x, 'y':y };

var t=(this.graphObject.timeRange*x/this.width)+this.graphObject.time0; var r=(this.height-y)/this.height;

var nearest=this.graphObject.getRevisonNearestTime(t);

var vars={}; for(var i in this.graphObject.variables){ var v=this.graphObject.variables[i]; vars[i]=(v.range*r)+v.min; }

for(var i=0; i<this.graphClicks.length; i++){ this.graphClicks[i](e.target,coords,t,nearest,vars); }

});	$(canvas).dblclick(function(e){ this.graphObject.event=e;

var x=e.pageX-$(e.target).offset.left; var y=e.pageY-$(e.target).offset.top

var coords={ 'x':x, 'y':y };

var t=(this.graphObject.timeRange*x/this.width)+this.graphObject.time0; var r=(this.height-y)/this.height;

var nearest=this.graphObject.getRevisonNearestTime(t);

var vars={}; for(var i in this.graphObject.variables){ var v=this.graphObject.variables[i]; vars[i]=(v.range*r)+v.min; }

for(var i=0; i<this.graphDoubleClicks.length; i++){ this.graphDoubleClicks[i](e.target,coords,t,nearest,vars); }

});	$(canvas).mousemove(function(e){ this.graphObject.event=e;

var x=e.pageX-$(e.target).offset.left; var y=e.pageY-$(e.target).offset.top

var coords={ 'x':x, 'y':y };

var t=(this.graphObject.timeRange*x/this.width)+this.graphObject.time0; var r=(this.height-y)/this.height;

var nearest=this.graphObject.getRevisonNearestTime(t);

var vars={}; for(var i in this.graphObject.variables){ var v=this.graphObject.variables[i]; vars[i]=(v.range*r)+v.min; }

for(var i=0; i<this.graphMoves.length; i++){ this.graphMoves[i](e.target,coords,t,nearest,vars); }

});

this.defaultColours=['#ff0000','#00ff00','#0000ff','#ffff00'];

this.attachClickEvent=function(f){ if(typeof f =='function') this.canvas.graphClicks.push(f); }	this.attachDoubleClickEvent=function(f){ if(typeof f =='function') this.canvas.graphDoubleClicks.push(f); }	this.attachMoveEvent=function(f){ if(typeof f =='function') this.canvas.graphMoves.push(f); }	this.getRevisonNearestTime=function(t){ if(typeof t == 'object') t=t.getTime; var m=Math.abs((new Date).getTime-t); var j=0; var r=this.data; for(var i=0; i<r.length; i++){ var d=Math.abs((new Date(r[i].timestamp)).getTime-t); if(d<m){ m=d; j=i; }		}		r[j].index=j; return r[j]; }	this.plot=function(vars){

var data=this.dataSource.getRevisions; this.data=data; if(data.length==0) return false;

if(!vars && this.vars) vars=this.vars;

if( typeof vars == 'string') vars=[vars];

if( typeof vars == 'object'){ if(vars.length){ var v={}; for(var i=0; i<vars.length; i++){ v[vars[i]]=this.dataSource.getRange(vars[i]); v[vars[i]].col=this.defaultColours[i%this.defaultColours.length]; }				vars=v; }else{ var i=0; for(var j in vars){ if(typeof vars[j] != 'object'){ vars[j]=this.dataSource.getRange(j); vars[j].col=this.variables[j].col; }					i++; }			}		}

this.variables={}; for(var i in vars){ this.variables[i]={}; for(var j in vars[i]){ this.variables[i][j]=vars[i][j]; }		}		for(var i=0; i<data.length; i++) data[i].coords={}; this.canvas.width=this.canvas.width; var c=this.canvas.getContext('2d');

/*	if(!data[data.length-1]){ alert('missing final revisions '+(data.length-1)); return; }*/		this.time0=(new Date(data[data.length-1].timestamp)).getTime; this.time1=(new Date(data[0].timestamp)).getTime;

var tRange=this.time1-this.time0; this.timeRange=tRange;

for(var j in vars){

c.beginPath; c.strokeStyle=vars[j].col;

var tI=(new Date(data[0].timestamp)).getTime;

var x=width*(tI-this.time0)/tRange; var y=height-height*(data[0][j]-vars[j].min)/(vars[j].range ); c.moveTo(x,y); for(var i=0; i<data.length; i++){ var tI=(new Date(data[i].timestamp)).getTime; var x=width*(tI-this.time0)/tRange; var y=height-height*(data[i][j]-vars[j].min)/(vars[j].range ); data[i].coords[j]={'x':x,'y':y}; c.lineTo(x,y); c.stroke; c.beginPath; c.moveTo(x,y); if(this.cursorIndex==i){ c.strokeStyle=this.cursorCol; c.arc(x,y,3,0,Math.PI*2,true); c.stroke; c.beginPath; c.moveTo(x,y); c.strokeStyle=vars[j].col }			}

vars[j]=true; }

} }