Source: physgraphs.js

/**
* Creates a Point that can be used with the Plot and Graph objects.
* @constructor Point
* @param {number} x The x value.
* @param {number} y The y value.
* @property {method} Point.add(x,y) Add x, subtract y.
* @property {method} Point.getPoint(p5.Vector) Converts a p5.Vector into a Point object.
* @property {method} Point.fixChoord() Inverts the y axis so that positive is up not down. Allows Points to be compatible with p5.js.

* @property {method} Point.addPix(xoff,yoff,scalex,scaley)  Convert choord to pixel position.
* @property {method} Point.getDist(Point,Point)  Takes in two Point objects and returns a new Point object whose x and y values are the respective x and y distances.

*/


//GLOBAL CONSTANTS
FR = 30; // must match the framerate of the draw function.

//Declaration for a point object to be used when graphing.
function Point(x,y){
	//basic coordinate variables
	this.x = x;
	this.y = y;
}
Point.prototype.add = function(x,y){
	this.x += x;
	this.y -= y;
};
//fixes the y choordinates to work with p5.js
Point.prototype.fixChoord = function(){
	if(this.y > 0){
		this.y = this.y*-1; //inverts the y axis so that positive is up not down
	}
};
//inverts both choordinates
Point.prototype.invert = function(){
	this.y *= -1;
	this.x *= -1;
};
//convert choord to pixel position
Point.prototype.makePix = function(xoff, yoff, scalex, scaley){

	this.fixChoord();
	this.x = this.x*scalex+xoff;
	this.y = this.y*scaley+yoff;

};
/*
 * input = takes in two point objects
 * output = returns a new point object
 * whose x and y coords are the respective x and y distances.
 * (basically using the point object like a vector)*/
Point.getDist = function(p1,p2){
	var pFinal = new Point(0,0);
	pFinal.x = Math.abs(p1.x-p2.x); //gets the x distance between points
	pFinal.y = Math.abs(p1.y-p2.y); //gets the y distance between points
	return pFinal;
};
Point.getPoint = function(p){
	var point = new Point(p.x,p.y);
	return point;
};
//a function object for graphing functions
function Equation(){
	//not implemented
}

/*Declaration of plot object which will be graphed
 * on graph object
 * */
function Plot(pointArray, red, green, blue, weight, title){
	if (title === undefined){
		title = "default";
	}
	this.data = pointArray; //plot data (an array of points)
	this.color = color(red, green, blue, title = "default"); //the color that the graph will be drawn in
	this.weight = weight; // a number for the stroke thickness of the graph
	this.pointSize = 8;
	this.drawData = true;
	this.plotTitle = title;
	this.timeplot = false;
}
//regular plotting function
Plot.prototype.plot = function(graph){
	//set up the styles for what will be drawn
	fill(this.color);
	stroke(this.color);
	strokeWeight(this.weight);

	//draws the data points and the connecting lines
	for(var i = 0;i<this.data.length;i++){

		if(i<this.data.length-1
			&& this.data[i+1].x <= graph.tr_pix.x
			&& this.data[i+1].y >= graph.tr_pix.y
			&& this.data[i].x >= graph.bl_pix.x
			&& this.data[i].y <= graph.bl_pix.y){
		//if(i<this.data.length-1){
			//draws the connecting lines, scaling the data so that
			//it corresponds to our coordinate space
			line(this.data[i].x, this.data[i].y,
					this.data[i+1].x, this.data[i+1].y);
		}
		//draws the data points, with scaling and offset.
		if(this.data[i].x <= graph.tr_pix.x
			&& this.data[i].y >= graph.tr_pix.y
			&& this.data[i].x >= graph.bl_pix.x
			&& this.data[i].y <= graph.bl_pix.y){
			ellipse(this.data[i].x, this.data[i].y, this.pointSize, this.pointSize);
		}

	}
};

//updates data to user input
Plot.prototype.getUser = function(){
	var d = new Point(0,0);
	var pmouse = new Point(mouseX,mouseY);
	var closest = 100000;
	var closestIndex = 0;
	for(var i=0;i<this.data.length;i++){

		d = Point.getDist(pmouse,this.data[i]);
		if(closest > d.x){
			closest = d.x;
			closestIndex = i;
		}
	}
	//TODO: add boundary limits to this funtion.
	this.data[closestIndex].y = mouseY;
};
Plot.prototype.tpRecord = function(variable, graph){
	this.timeplot = true;
	var bl = graph.bl_pix.x;
	var tr = graph.x_max;
	//var timescale = (tr-bl)/100;
	//var timescale = (graph.x_max-graph.x_min)/graph;
	var timescale = FR;
	for(var i=0;i<this.data.length;i++){
		this.data[i].x -= graph.xunit/timescale; //shift the x-axis
	}
	if(this.data[0] && this.data[0].x < bl){
		this.data.splice(0,1);
	}

	var p = new Point(tr, variable);
	this.data.push(p);

};
//gets distance between two points of data
Plot.prototype.getPointDist = function(num1, num2){
	return dist(this.data[num1].x, this.data[num1].y,
				this.data[num2].x, this.data[num2].y);
};
Plot.prototype.fixChoord = function(scalex, scaley, origin){
	var p = Point.getPoint(origin);
	for(var i = 0;i<this.data.length;i++){
		//this.data[i].fixChoord();
		p.add(this.data[i].x*scalex, this.data[i].y*scaley);
		this.data[i].x = p.x;
		this.data[i].y = p.y;
		p = Point.getPoint(origin);
	}
};

function Graph(w, h, x_min, x_max, y_min, y_max, resolution){

	// initial variables

	this.width = w;
	this.height = h;
	this.resolution = resolution;
	this.x_min = x_min;
	this.x_max = x_max;
	this.y_min = y_min;
	this.y_max = y_max;


	//styling variables
	this.title = "Title"
	this.xlabel = "X-Axis";
	this.ylabel = "Y-Axis";
	this.showLabels = true;
	this.showTitle = true;
	this.showBorder = true;
	this.borderWidth = 2;
	this.showLegend = false;

	this.x_offset = 0;
	this.y_offset = 0;

	//functional variables
	this.plots = [];

	/* bl = bottom left. coordinates of where the actual graph begins.
	 * tr = top right.
	 * this is used for determining correct positioning of graph coordinates.
	 * */
	this.bl_pix = new Point(this.x_offset+this.width*0.15,
								this.y_offset+this.height*0.95);
	this.tr_pix = new Point(this.x_offset+this.width*0.95,
								this.y_offset+this.height*0.15);
	this.bl_val = new Point(this.x_min, this.y_min);
	this.bl_val.invert();

	//xpix and ypix are the number of pixels between each labeled coord.
	this.xpix = dist(this.bl_pix.x, this.bl_pix.y,
					this.tr_pix.x, this.bl_pix.y)/this.resolution; //xScale in pixels
	this.ypix = dist(this.bl_pix.x, this.bl_pix.y,
					this.bl_pix.x, this.tr_pix.y)/this.resolution; //yScale in pixels

	//xunit and yunit are the number of pixels one unit on the graph is.
	this.xunit = dist(this.bl_pix.x, this.bl_pix.y,
					this.tr_pix.x, this.bl_pix.y)/(this.x_max-this.x_min); //xUnitScale
	this.yunit = dist(this.bl_pix.x, this.bl_pix.y,
					this.bl_pix.x, this.tr_pix.y)/(this.y_max-this.y_min); //yUnitScale

	this.origin = Point.getPoint(this.bl_pix);
	this.origin.add(this.bl_val.x*this.xunit, this.bl_val.y*this.yunit);
	//console.log(this.origin.x, this.origin.y);


}
//redraw background and plots
Graph.prototype.update = function(){

};
//draw the axis, labels, etc... (the graph without the curves)
Graph.prototype.set_offset = function(xoff,yoff){
	this.x_offset = xoff;
	this.y_offset = yoff;

	this.bl_pix = new Point(this.x_offset+this.width*0.15,
								this.y_offset+this.height*0.85);
	this.tr_pix = new Point(this.x_offset+this.width*0.90,
								this.y_offset+this.height*0.15);
	this.bl_val = new Point(this.x_min, this.y_min);
	this.bl_val.invert();


	this.xpix = dist(this.bl_pix.x, this.bl_pix.y,
					this.tr_pix.x, this.bl_pix.y)/this.resolution; //xScale in pixels
	this.ypix = dist(this.bl_pix.x, this.bl_pix.y,
					this.bl_pix.x, this.tr_pix.y)/this.resolution; //yScale in pixels

	this.xunit = dist(this.bl_pix.x, this.bl_pix.y,
					this.tr_pix.x, this.bl_pix.y)/(this.x_max-this.x_min); //xUnitScale
	this.yunit = dist(this.bl_pix.x, this.bl_pix.y,
					this.bl_pix.x, this.tr_pix.y)/(this.y_max-this.y_min); //yUnitScale

	this.origin = Point.getPoint(this.bl_pix);
	this.origin.add(this.bl_val.x*this.xunit, this.bl_val.y*this.yunit);
	console.log(this.origin.x, this.origin.y);
}
Graph.prototype.drawBg = function(bg, border){
	if (bg === undefined){
		bg = color(255);
	}
	if (border === undefined){
		border = color(0);
	}
	//border
	if(this.showBorder == false){
		noStroke();
	}else{
		stroke(border);
		strokeWeight(this.borderWidth);
	}

	//set background color of graph
	fill(bg);

	//draw base layer of graph
	rect(this.x_offset,
		 this.y_offset,
		 this.width,
		 this.height);

	strokeWeight(1);

	//draw axis
	stroke(0);
	line(this.bl_pix.x, this.bl_pix.y,
			this.tr_pix.x, this.bl_pix.y) //x border
	line(this.bl_pix.x, this.bl_pix.y,
			this.bl_pix.x, this.tr_pix.y) //y border

	//compute resolution values (numeric values)
	var xdiff = (this.x_max - this.x_min)/this.resolution;
	var ydiff = (this.y_max - this.y_min)/this.resolution;

	//draw x values and vertical lines
	fill(0);
	stroke(180);
	var count = this.x_min; //for counting intermediary values
	var pixCount = 0;
		//draw min label
		text(this.x_min, this.bl_pix.x, this.bl_pix.y+20);
	for(var i = 0; i < this.resolution; i++){
		count += xdiff;
		pixCount += this.xpix;
		line(this.bl_pix.x + pixCount, this.bl_pix.y+5,
				this.bl_pix.x + pixCount, this.tr_pix.y);
		text((Math.round(10*count)/10).toString(), this.bl_pix.x + pixCount, this.bl_pix.y+20);
	}

	//draw y values and horizontal lines
	var count = this.y_min; //for counting intermediary values
	var pixCount = 0;
		//draw min label
		text(this.y_min, this.bl_pix.x - 20, this.bl_pix.y);
	for(var i = 0; i < this.resolution; i++){
		count += ydiff;
		pixCount += this.ypix;
		line(this.bl_pix.x - 5, this.bl_pix.y-pixCount,
				this.tr_pix.x, this.bl_pix.y-pixCount);
		text((Math.round(10*count)/10).toString(), this.bl_pix.x - 20, this.bl_pix.y-pixCount);
	}


	//draw title AND axis labels AND legend
	if(this.showTitle == true){
		textSize(16);
		textAlign(CENTER,CENTER);
		if(this.showLegend != true){
			text(this.title, this.x_offset + this.width/2,
					this.y_offset + this.height*.07);
		}
		else{
			//shift the title out of the way of the legend
			//eg--split the space above the graph between them.
			textSize(16);
			textAlign(CENTER,CENTER);
			text(this.title, this.x_offset + this.width/4,
					this.y_offset + this.height*.07);

			//draw frame
			fill(bg);
			stroke(border);

			var top = this.y_offset+this.height*0.02;
			var left= this.x_offset+this.width/2;
			rect(left, top, this.width/2.2, this.height*.1);
			//code for drawing legend.
			textSize(12);
			textAlign(LEFT,TOP);
			noStroke();
			fill(0);
			for(var i=0;i<this.plots.length;i++){
				text(this.plots[i].plotTitle, left+8,top+2);
				top+=12; //shift the count down a line.
			}
		}
	}

	if(this.showLabels == true){
		//xlabel
		textAlign(CENTER,CENTER);
		textSize(12);
		text(this.xlabel,this.x_offset + this.width/2,this.y_offset + this.height*.97);
		//ylabel
		push();
		translate(this.x_offset+this.width*.03-10,this.y_offset + this.height/2)
		rotate(-Math.PI/2);
		text(this.ylabel,0,0)
		pop();
	}

};
//plots all plots on this graph
Graph.prototype.plotAll = function(){
	for(var i = 0; i<this.plots.length;i++){
		if(this.plots[i].timeplot == true){
			var temp = new Plot();
			temp.data = [];
			temp.data[0] = this.plots[i].data[this.plots[i].data.length-1];
			temp.fixChoord(this.xunit,
							this.yunit,
							this.origin);
			this.plots[i].data[this.plots[i].data.length-1] = temp.data[0];
		}
		this.plots[i].plot(this);
	}
};
//add a new plot to the graph.
Graph.prototype.addPlot = function(aplot){
	//var temp = this.getZeroes();
	aplot.fixChoord(this.xunit,
					this.yunit,
					this.origin);
	this.plots.push(aplot);
};
//get the x and y zero.
Graph.prototype.getZeroes = function(){

};
//get pixel x and y values for a point in the graph.
Graph.prototype.getChoord = function(x, y){

};

//this might make more sense to put as part of the Plot object
Graph.makeData = function(xarray, yarray){
	var finalArray = [];
	if(xarray.length == yarray.length){
		for(var i = 0; i<xarray.length;i++){
			var p = new Point(xarray[i],yarray[i]);
			finalArray.push(p);
		}
	}
	else{
		console.log("xarray and yarray lengths differ.");
	}
	return finalArray;
};

//this might make more sense to put as part of the Plot object
Graph.makeUserPlot = function(x1, x2, resolution, lineColor, weight, psize){
	var finalArray = [];
	var templot = new Plot();
	if (lineColor === undefined) {
        lineColor = color(0);
    }
	if (weight === undefined){
		weight = 1;
	}
	if (psize === undefined){
		psize = 3;
	}

	templot.lineColor = lineColor;
	templot.data = [];
	templot.weight = weight;
	templot.pointSize = psize;

	var scale = (x2-x1)/resolution;
	for(var i = x1;i<x2;i += scale){
		var p = new Point(i,0);
		templot.data.push(p);
	}

	return templot;
};