
// Data holder for tasukeGraphManager. 
var tasukeGraphPoint = function(dataArr) {
	this.id = null;	// Point ID.
	this.grp = null;	// Group identifer.

	this.fid = null;
	this.otherInfo = {};	

	this.x = null;
	this.y = null;

	this.l = null;
	this.t = null;

	if(dataArr) {
		this.x = dataArr[0];
		this.y = dataArr[1];
		this.id = dataArr[2];
		this.fid = dataArr[3];
		this.grp = dataArr[3];
	}
}

var tasukeGraphPointSymbol = function(grp) {
	this.grp = grp;
	this.size = 4;	// radius.
	this.fillStyle = null;
}

// Static method (not inherited by "new" instance)
tasukeGraphPointSymbol.pathArc = function(ctx, x, y) {
	ctx.fillStyle = this.fillStyle;
	ctx.beginPath();
	ctx.arc(x, y, this.size, 0, 2 * Math.PI);
	ctx.closePath();
}
tasukeGraphPointSymbol.pathUTriangle = function(ctx, x, y) {
	ctx.fillStyle = this.fillStyle;
	ctx.beginPath();
	ctx.moveTo(x, y-this.size);
	ctx.lineTo(x+this.size, y+this.size);
	ctx.lineTo(x-this.size, y+this.size);
	ctx.closePath();
}
tasukeGraphPointSymbol.pathSquare = function(ctx, x, y) {
	ctx.fillStyle = this.fillStyle;
	ctx.beginPath();
	ctx.moveTo(x-this.size, y-this.size);
	ctx.lineTo(x+this.size, y-this.size);
	ctx.lineTo(x+this.size, y+this.size);
	ctx.lineTo(x-this.size, y+this.size);
	ctx.lineTo(x-this.size, y-this.size);
	ctx.closePath();
}
tasukeGraphPointSymbol.pathDiamond = function(ctx, x, y) {
	ctx.fillStyle = this.fillStyle;
	ctx.beginPath();
	ctx.moveTo(x, y-this.size);
	ctx.lineTo(x+this.size, y);
	ctx.lineTo(x, y+this.size);
	ctx.lineTo(x-this.size, y);
	ctx.lineTo(x, y-this.size);
	ctx.closePath();
}

// Instance method.
tasukeGraphPointSymbol.prototype = {
	makePath: function(ctx, x, y) {
		// Abstruct mthod.
	}
}

/**
 * Core object.
 */
var tasukeGraphManager = function(cvs) {
	this.DEBUG_MODE = false;

	// canvas & layout
	this.cvs = cvs;

	this.defCvsW = null;
	this.defCvsH = null;
	this.ctx = null;
	if(this.cvs)  {
		this.ctx = this.cvs.getContext('2d'); 
		this.defCvsW = cvs.width;
		this.defCvsH = cvs.height;
	}
	this.tOffset = 10;
	this.bOffset = 20;
	this.lOffset = 40;
	this.rOffset = 10;
	this.pointRadius = 3;	// Radius to expand click point.
	this.pointExpands = [];	// For popup. Points closer to the click point are given priority in being hit. => [0,1,-1,2,-2,3,-3]
	for(var i=0; i<=this.pointRadius; i++) {
		this.pointExpands.push(i);
		if(i!==0) this.pointExpands.push(i * -1);	
	}
	this.popupTimelag = 1400;
	this.mousePopupOffsetX = 0;
	this.mousePopupOffsetY = -10;

	// Axis.
	this.xAxis = new tasukeGraphAxis();
	this.yAxis = new tasukeGraphAxis();
	this.yAxis.reverse = true;	// On y-axis, pixel coordinates and real values are flipped upside down.

	// Text
	this.drawPointText = '';	// id/grp

	// For processing.
	this.data = [];	// {id, grp, x, y, l, t}. x,y:realValue l,t:left/top(without offset)
	this.id2Point = {};
	this.selectedIds = {};		// Currently selected ID. Since the history is kept, direct changes from outside are prohibited.
//	this.lastChangeIds = {};	// id->true/false. true=add false=remove
	this.selectRegs = [];		// as realValue(not pos, for zoomin/out). Currently, element count is always 1. 
	this.scrTimer = null;		// For setTimeout(). Prevents heavy processing from being executed multiple times due to successive event firings.
	this.pointGrp2Symbol = {};
	this.id2OtherGrps = null;
	this.pos2pointMap = {};
//	this.mouseOnCvs = false;
	this.XY = {x1:null, y1:null, x2:null, y2:null, scrollTop:null, scrollLeft:null, scrollX1:null, scrollY1:null};

	// Undo/Redo
	this.maxUndoBuff = 20;
	this.undoBuff = [];
	this.redoBuff = [];

	// Image buffer.
	this.bufGraphImage = null;

	// Associated DOM elements.
	this.buttonIdUndo = null;
	this.buttonIdRedo = null;
	this.iptIdZoom = null;
	this.selIdChangeGrp = null;
	this.cvsIdLegend = null;
//	this.spanIdTotalCount = null;
//	this.spanIdSelectedCount = null;
	this.spanIdCursorInfo = null;
	this.divIdPopup = null;

	// css etc.
	var defFont = "Helvetica";	// <- Courier
	this.style = {
		defFont: defFont,
		plotLabelFont: "10px '" + defFont + "'",
		backgroundColor: "white",
		graphBackgroundColor: "#fdf9fb",
		frameColor: "#333333",
		gridColor: "#e3e3e3",
		selectStrokeColor: "rgba(0,180,0,0.8)",
		selectFillColor: "rgba(0,255,0,0.1)",
//		selectStrokeColor: "red",
//		selectFillColor: "rgba(255,0,0,0.05)",
		highlightStrokeColor: "red",
		pointText: "7px '" + defFont + "'",
		pointTextFillColor: "rgba(0,0,0,0.5)",
		scaleTextFillColor: "black"
	}

	// For event.
	this.keyState = {};
	this.hitPoint = null;
	this.addEvents();
	this.addHistory();	// Add empty history({}).
	this.popupDetail = true;
}

// Static methods
tasukeGraphManager.getMapClone = function(map) {
	var keys = Object.keys(map);
	var newMap = {};
	for(var i=0,len=keys.length; i<len; i++) newMap[keys[i]] = map[keys[i]];
	return newMap;
}

// Instance methods
tasukeGraphManager.prototype = {
	refresh: function() {
		this.selectRegs = [];
		this.bufGraphImage = null;

//		this.changeCvsSizeDefault();
//		this.clearSelectedPoint();
	},
	setSelectedIds: function(ids) {
		if(!ids) ids = {};
/*
		var newIds = {}
		var idsArr = Object.keys(ids);
		for(var i=0,len=idsArr.length; i<len; i++) {
			if(idsArr[i] in  this.id2Point) newIds[idsArr[i]] = true;
		}
		this.selectedIds = newIds;
*/
		this.selectedIds = tasukeGraphManager.getMapClone(ids);
		this.addHistory();
	},
	getSelectedIds: function() {
		return this.selectedIds;
	},
	clearSelectedIds: function() {
		this.selectedIds = {};
	},
	clearAxis: function() {
		 this.xAxis.clear();
		 this.yAxis.clear();
	},
	setData: function(arr=[]) {
		// Array: [[x,y,"id","grp"]];
		for(var i=0,len=this.data.length; i<len; i++) {
			delete this.id2Point[this.data[i].id];
		}
		this.data = [];
		this.id2Point = {};
		for(var i=0,len=arr.length; i<len; i++) {
			var point = new tasukeGraphPoint(arr[i]);
			if(this.id2OtherGrps && point.id in this.id2OtherGrps) {
				point.otherInfo = this.id2OtherGrps[point.id];
			}
			this.id2Point[point.id] = point;
			this.data.push(point);
		}
		if(this.spanIdTotalCount) $("#" + this.spanIdTotalCount).html(this.data.length);
		this.setPointGrp2Symbol();
	},
	initLegend: function(selId, cvsId, validGrpsArr=[]) {
		this.selIdChangeGrp = selId;
		this.cvsIdLegend = cvsId;
		if(selId) {
			$sel = $("#" + selId);
			$sel.children().remove().end();
			$sel.append(new Option("FID", "", false, true));
			for(var i=0,len=validGrpsArr.length; i<len; i++) {
				$sel.append(new Option(validGrpsArr[i][1], validGrpsArr[i][0], false, false));
			}

			$sel.on("change", function() {
				this.changeGrp();
				this.drawLegend();
			}.bind(this));
		}
	},
/*
	initCountForm: function(spanIdTotalCount, spanIdSelectedCount) {
		this.spanIdTotalCount = spanIdTotalCount;
		this.spanIdSelectedCount = spanIdSelectedCount;
		$("#" + this.spanIdTotalCount).parent("span").css("display", "inline-block");
	},
*/
	initUndoRedo: function(btnIdUndo, btnIdRedo) {
		this.buttonIdUndo = btnIdUndo;
		this.buttonIdRedo = btnIdRedo;
		if(btnIdUndo) {
			$("#" + btnIdUndo).on("click", function(e) {
				this.undoHistory();
			}.bind(this));
		}
		if(this.buttonIdRedo) {
			$("#" + this.buttonIdRedo).on("click", function(e) {
				this.redoHistory();
			}.bind(this));
		}

	},
	changeGrp: function() {
		if(this.id2OtherGrps === null || !this.selIdChangeGrp) return;

		var grp = $("#" + this.selIdChangeGrp).val();
		for(var i=0,len=this.data.length; i<len; i++) {
			var id = this.data[i].id;
			if(grp) {
				 this.data[i]["grp"] = this.id2OtherGrps[id][grp];
			} else {
				 this.data[i]["grp"] =  this.data[i]["fid"];
			}
		}
		this.setPointGrp2Symbol();
		this.draw();
	},
	setPointGrp2Symbol: function() {
		this.pointGrp2Symbol = {};

		var uniGrp = {};
		for(var i=0,len=this.data.length; i<len; i++) {
			uniGrp[this.data[i].grp] = true;
		}
		var grps = Object.keys(uniGrp);
		grps.sort();

		var makePathFuncs = [
			tasukeGraphPointSymbol.pathSquare,
			tasukeGraphPointSymbol.pathArc,
			tasukeGraphPointSymbol.pathUTriangle,
			tasukeGraphPointSymbol.pathDiamond
		];
		var lenMakePathFuncs = makePathFuncs.length;

		var viridis = new viridisMap();
		var lenGrps = grps.length;
		for(var i=0; i<lenGrps; i++) {
			var symbol = new tasukeGraphPointSymbol(grps[i]);
			var funcIndex = i % lenMakePathFuncs;
			symbol.makePath = makePathFuncs[funcIndex];
			var rate = 0;
			if(lenGrps !== 1) rate = i / (lenGrps - 1);
			symbol.fillStyle = viridis.getRgb(rate);
			this.pointGrp2Symbol[grps[i]] = symbol;
		}
//console.log(this.pointGrp2Symbol);
	},
	adjustScaleAuto: function() {
		if(!this.data) return;

		var xArr = [];
		var yArr = [];
		for(var i=0,len=this.data.length; i<len; i++) {
			xArr.push(this.data[i].x);
			yArr.push(this.data[i].y);
		}
		this.xAxis.adjustScale(xArr);
		this.yAxis.adjustScale(yArr);
		if(this.DEBUG_MODE) console.log(this.xAxis);
		if(this.DEBUG_MODE) console.log(this.yAxis);
	},
	draw: function() {
		var graphW = this.cvs.width - this.lOffset - this.rOffset;
		var graphH = this.cvs.height - this.tOffset - this.bOffset;
		this.ctx.save();

		// fill background.
		this.ctx.clearRect(0, 0, this.cvs.width, this.cvs.height);
//		this.ctx.fillStyle = this.style.backgroundColor; 
//		this.ctx.fillRect(0, 0, this.cvs.width, this.cvs.height);
		this.ctx.fillStyle = this.style.graphBackgroundColor;
		this.ctx.fillRect(this.lOffset, this.tOffset, graphW, graphH);

		this.ctx.translate(this.lOffset, this.tOffset);

		var valRegexp = new RegExp(/^[0\.]*[^0][^0]/);

		// draw grid(with unit number).
		this.ctx.strokeStyle = this.style.gridColor;
		this.ctx.textAlign = "center";
		this.ctx.textBaseline = "top";
		for(var i=this.xAxis.unitMin; i<=this.xAxis.unitMax; i+=this.xAxis.scaleUnit) {
			if(i === null) break;	 // Fixing unknown bugs.
			var x = this.xAxis.getPos(graphW, i);
			this.ctx.fillStyle = this.style.gridColor;
			this.ctx.beginPath();
			this.ctx.moveTo(x, 0);
			this.ctx.lineTo(x, graphH);
			this.ctx.stroke();

			var val = tasukeGraphAxis.getRoundNumber(i, 2);
			this.ctx.fillStyle = this.style.scaleTextFillColor;
			this.ctx.fillText(val, x, graphH + 2);
		}
//		this.ctx.textAlign = "left";
		this.ctx.textAlign = "right";
		this.ctx.textBaseline = "middle";
		for(var i=this.yAxis.unitMin; i<=this.yAxis.unitMax; i+=this.yAxis.scaleUnit) {
			if(i === null) break;	// Fixing unknown bugs.
			var y = this.yAxis.getPos(graphH, i);
			this.ctx.fillStyle = this.style.gridColor;
			this.ctx.beginPath();
			this.ctx.moveTo(0, y);
			this.ctx.lineTo(graphW, y);
			this.ctx.stroke();

			var val = tasukeGraphAxis.getRoundNumber(i, 2);
			this.ctx.fillStyle = "black";
			this.ctx.fillText(val, -5, y);
//			this.ctx.fillText(val, this.lOffset * -1 + 10, y);
		}

		// draw graph frame.
		this.ctx.strokeStyle = this.style.frameColor;
		this.ctx.strokeRect(0, 0, graphW, graphH);

		// draw label.
		var rad = 3;	// Point symbol radius.
		if(this.drawPointText) {
			this.ctx.fillStyle = this.style.pointTextFillColor;
			this.ctx.textAlign = "left";
			this.ctx.textBaseline = "middle";
			for(var i=0,len=this.data.length; i<len; i++) {
				var labelTxt = this.data[i]["otherInfo"][this.drawPointText];
				if(this.drawPointText === "fid") labelTxt = this.data[i][this.drawPointText];
				if(!labelTxt) continue;
				var l = this.xAxis.getPos(graphW, this.data[i].x);
				var t = this.yAxis.getPos(graphH, this.data[i].y);
				this.ctx.fillText(labelTxt, l+rad+2, t);
			}
		}	

		// Release memory and plot.
		this.pos2pointMap = {};
/* It is not a cross reference, so it is GCed.
		var ls = Object.keys(this.pos2pointMap);
		for(var i=0,iLen=ls.length; i<iLen; i++) {
			var ts = Object.keys(this.pos2pointMap[ls[i]]);
			for(var j=0,jLen=ts.length; j<jLen; j++) {
				this.pos2pointMap[ls[i]][ts[j]] = [];
				delete this.pos2pointMap[ls[i]][ts[j]];
			}
			delete this.pos2pointMap[ls[i]];
		}
*/
		for(var i=0,len=this.data.length; i<len; i++) {
			var l = this.xAxis.getPos(graphW, this.data[i].x);
			var t = this.yAxis.getPos(graphH, this.data[i].y);
			this.data[i].l = l;
			this.data[i].t = t;
			this.ctx.fillStyle = this.pointGrp2Symbol[this.data[i].grp].fillStyle;
			this.pointGrp2Symbol[this.data[i].grp].makePath(this.ctx, l, t);
			this.ctx.fill();

			// memory pos2point.
			if( !(l in this.pos2pointMap) ) this.pos2pointMap[l] = {};
			if( !(t in this.pos2pointMap[l]) ) this.pos2pointMap[l][t] = [];
			this.pos2pointMap[l][t].push(this.data[i]);
		}

		// Highlight selected points
		this.ctx.strokeStyle = this.style.highlightStrokeColor;
		this.ctx.lineWidth = 2;
		var selIds = Object.keys(this.selectedIds);
		for(var i=0,len=selIds.length; i<len; i++) {
			if( !(selIds[i] in this.id2Point) ) continue;
			var p = this.id2Point[selIds[i]];
			this.pointGrp2Symbol[p.grp].makePath(this.ctx, p.l, p.t);
			this.ctx.stroke();
		}
/*
		var selIds = Object.keys(this.selectedPoint);
		for(var i=0,len=selIds.length; i<len; i++) {
			var p = this.selectedPoint[selIds[i]];
			this.pointGrp2Symbol[p.grp].makePath(this.ctx, p.l, p.t);
			this.ctx.stroke();
		}
*/
		this.ctx.restore();

/*
		this.ctx.strokeStyle = this.style.selectStrokeColor;
		this.ctx.fillStyle = this.style.selectFillColor
		for(var i=0,len=this.selectRegs.length; i<len; i++) {
			var sel = this.selectRegs[i];
			var l = this.xAxis.getPos(graphW, sel.lv) + this.lOffset;
			var r = this.xAxis.getPos(graphW, sel.rv) + this.lOffset;
			var t = this.yAxis.getPos(graphH, sel.tv) + this.tOffset;
			var b = this.yAxis.getPos(graphH, sel.bv) + this.tOffset;
			this.ctx.fillRect(l, t, r - l + 1, b - t + 1);
			this.ctx.strokeRect(l, t, r - l + 1, b - t + 1);
		}
*/
		this.bufGraphImage = this.ctx.getImageData(0, 0, this.cvs.width, this.cvs.height);
	},
	drawLegend: function() {
		if(this.cvsIdLegend === null) return;

		var cvs = document.getElementById(this.cvsIdLegend);
		var ctx = cvs.getContext("2d");
		ctx.clearRect(0, 0, cvs.width, cvs.height);
//		ctx.fillStyle = "white";
//		ctx.fillRect(0, 0, cvs.width, cvs.height);

		var grps = Object.keys(this.pointGrp2Symbol);
		grps.sort();
		var textW = cvs.width - 20;
		ctx.textBaseline = "middle";
		var rowH = 11;
		var minRowH = 8;
		if(cvs.height / rowH < grps.length) {
			rowH = Math.round(cvs.height / grps.length);
			if(rowH < minRowH) rowH = minRowH;
		}
		var fontSize = rowH + 1;
		var maxFontSize = 11;
		if(fontSize > maxFontSize) fontSize = maxFontSize;
		ctx.font = fontSize +"px '" + this.style.defFont + "'";
		for(var i=0,len=grps.length; i<len; i++) {
			var y = 10+i*rowH;
			if(y > cvs.height) {
				console.log("tasukeGraphManager.drawLegend Skip after: " + grps[i]);
				break;
			}

			ctx.fillStyle = this.pointGrp2Symbol[grps[i]].fillStyle;
			this.pointGrp2Symbol[grps[i]].makePath(ctx, 10, y);
			ctx.fill();

			ctx.fillStyle = "black";
			ctx.fillText(grps[i], 20, y, textW);
		}
	},
	initCvsSize: function(w, h) {
		this.defCvsW = w;
		this.defCvsH = h;
		this.changeCvsSize(w, h);
	},
	changeCvsSize: function(w, h) {
		this.cvs.width = w;
		this.cvs.height = h;
		this.cvs.setAttribute("width", w);
		this.cvs.setAttribute("height", h);
	},
	changeCvsSizeDefault: function() {
		this.changeCvsSize(this.defCvsW, this.defCvsH);
	},
	initZoomForm: function(id) {
		var $form = $("#" + id);

		$form.attr("min", -2);
		$form.attr("max", 20);
		$form.attr("step", 1);
		$form.val(0);

		var manager = this;
//		$form.on('change', function(e) {
		$form.on('input', function(e) {
			var val = parseInt($(this).val());
			if(manager.scrTimer) {
				if(manager.DEBUG_MODE) console.log("Cancel tasukeGraphManager.draw()!!");
				clearTimeout(manager.scrTimer);
			}
			manager.scrTimer = setTimeout(function() {
				var $parentDiv =  $(manager.cvs).parent("div");
				var lastScrollTopRate = $parentDiv.scrollTop() /  manager.cvs.height;
				var lastScrollLeftRate = $parentDiv.scrollLeft() / manager.cvs.width;

				var unitW = manager.defCvsW / 5;
				var unitH = manager.defCvsH / 5;
				var newCvsW = manager.defCvsW + unitW * val;
				var newCvsH =  manager.defCvsH + unitH * val;
				manager.changeCvsSize(newCvsW, newCvsH);
				manager.draw();
				$parentDiv.scrollLeft(newCvsW * lastScrollLeftRate);
				$parentDiv.scrollTop(newCvsH * lastScrollTopRate);
				manager.scrTimer = null;
			}, 20);	// 20 <= 50. If the delay is too long, the event may not fire.
		});
		this.iptIdZoom = id;
	},

	addEvents: function() {
		window.addEventListener("keydown", this.winEvent_keydown.bind(this), false);
		window.addEventListener("keyup", this.winEvent_keyup.bind(this), false);

		this.cvs.addEventListener("mousedown", this.cvsEvent_mousedown.bind(this),  false);
		this.cvs.addEventListener("mousemove", this.cvsEvent_mousemove.bind(this),  false);
		this.cvs.addEventListener("mouseup", this.cvsEvent_mouseup.bind(this),  false);
//		this.cvs.addEventListener("mouseover", this.cvsEvent_mouseover.bind(this),  false);
		this.cvs.addEventListener("mouseout", this.cvsEvent_mouseout.bind(this),  false);
		this.cvs.addEventListener("wheel", this.cvsEvent_wheel.bind(this),  false);
	},

	// Funcs for Canvas evens.
	// "this" will be Canvas-element, so use ".bind(manager)".

	winEvent_keydown: function(e) {
		if(e.keyCode === 32)  e.preventDefault();	// space
		if(e.repeat) return

		// Without this, pressing Esc in modes other than" PCA" will cause the Axis value to become null, resulting in an infinite loop.
		if(!this.cvs.checkVisibility()) return;	// Cancel event if graph is not visible. 

		this.keyState[e.keyCode] = true;
		if(this.DEBUG_MODE) console.log("tasukeGraphManager.keyDown:" + e.keyCode);
		if(e.keyCode === 27) this.disableDrug();
	},
	winEvent_keyup: function(e) {
		if (e.repeat) return;
		if(!this.cvs.checkVisibility()) return;

		delete this.keyState[e.keyCode];
		if(this.DEBUG_MODE) console.log("tasukeGraphManager.keyUp:" + e.keyCode);
	},
	popupMorePointDetail: function(accId, x, y) {
		var $popup = $("#" + this.divIdPopup);
		var point = this.id2Point[accId];
		var html = accId + "&nbsp;&nbsp;&nbsp;" + point["grp"] + "<br>" + 
			"<span style=\"color:gray;\">" + this.yAxis["label"] + ":</span> " + tasukeGraphAxis.getRoundNumber(point["y"],6) + " <br>" + 
			"<span style=\"color:gray;\">" + this.xAxis["label"] + ":</span> " + tasukeGraphAxis.getRoundNumber(point["x"],6); 
		$popup.html(html);
		$popup.css("left", (x + this.mousePopupOffsetX) + "px");
		$popup.css("top", (y + this.mousePopupOffsetY) + "px");
		$popup.css("width", "auto");
		$popup.css("height", "48px");
		$popup.css("display", "inline-block");
	},
	cvsEvent_mousedown: function(e) {
		e.preventDefault();

		var rect = e.target.getBoundingClientRect();
		var x = parseInt(e.clientX)-parseInt(rect.left);
		var y = parseInt(e.clientY)-parseInt(rect.top);
		var rGraph = this.cvs.width - this.rOffset - 1;
		var bGraph = this.cvs.height - this.bOffset - 1;
		if(x < this.lOffset || x > rGraph || y < this.tOffset || y > bGraph) return;

		//TODO isSmartPhone()
		var rightclick;
		if(e.which) {
			rightclick = (e.which==3);
		} else if(e.button) {
			rightclick = (e.button==2);
		}

		if(!rightclick) {
			if("32" in this.keyState) {
				this.XY.scrollLeft = $(this.cvs).parent().scrollLeft();
				this.XY.scrollTop = $(this.cvs).parent().scrollTop();
				this.XY.scrollX1 = parseInt(e.clientX);
				this.XY.scrollY1 = parseInt(e.clientY);
			}
			this.XY.x1 = x;
			this.XY.y1 = y;
			this.XY.x2 = this.XY.x1;
			this.XY.y2 = this.XY.y1;
			if(this.DEBUG_MODE) console.log(this.XY);
		}
	},
	cvsEvent_mousemove: function(e) {
		e.preventDefault();

		var rect = e.target.getBoundingClientRect();
		var x =  parseInt(e.clientX)-parseInt(rect.left);
		var y =  parseInt(e.clientY)-parseInt(rect.top);

		if(this.XY.x1 === null) {
			// Popup information dialog.
			if(this.divIdPopup) {
				if(this.scrTimer) clearTimeout(this.scrTimer);
				this.scrTimer = setTimeout(function() {
					this.hitPoint = null;
					var $popup = $("#" + this.divIdPopup);
					var pointX = x - this.lOffset;
					var pointY = y - this.tOffset;
					var hit = false;
					var range = this.pointExpands;
//					var range = [0,1,-1,2,-2,3,-3];
					for(var i=0,iLen=range.length; i<iLen; i++) {
						var l = pointX + range[i];
						if( !(l in this.pos2pointMap) ) continue;
						for(var j=0,jLen=range.length; j<jLen; j++) {
							var t = pointY + range[j];
							if( !(t in  this.pos2pointMap[l]) ) continue;

							var point = this.pos2pointMap[l][t][0];
							$popup.html(point["id"]);
							$popup.css("left", (e.clientX + 20) + "px");
							$popup.css("top", (e.clientY - 6) + "px");
							$popup.css("min-width", "120px");
							$popup.css("height", "14px");
							$popup.css("display", "inline-block");
							this.hitPoint = point;
							hit = true;
							break;
						}
						if(hit) break;
					}
					if(!hit) $popup.css("display", "none");
					this.scrTimer = null;

					// Display more details with time lag.
					if(hit && this.popupDetail) this.scrTimer = setTimeout(function() {
						this.popupMorePointDetail(this.hitPoint["id"], e.clientX, e.clientY);
						this.scrTimer = null;
					}.bind(this), this.popupTimelag);
				}.bind(this), 30);
			}
//			return;
		}

		// Report cursor info.
		if(this.spanIdCursorInfo) {
			var graphW = this.cvs.width - this.lOffset - this.rOffset;
			var graphH = this.cvs.height - this.tOffset - this.bOffset;
			var graphXpre = this.xAxis.getVal(graphW, x - this.lOffset);
			var graphYpre = this.yAxis.getVal(graphH, y - this.tOffset);
			if(graphXpre !== null && graphYpre !== null) { 
				var graphX = tasukeGraphAxis.getRoundNumber(graphXpre, 4);
				var graphY = tasukeGraphAxis.getRoundNumber(graphYpre, 4);
				var text = graphY + " / " + graphX;
				if(this.hitPoint) {
					var pointX = tasukeGraphAxis.getRoundNumber(this.hitPoint["x"], 4);
					var pointY = tasukeGraphAxis.getRoundNumber(this.hitPoint["y"], 4);
					text += "  [" + this.hitPoint["id"] + ": " + pointY + " / " + pointX + "]";
				}
				$("#" + this.spanIdCursorInfo).text(text); 
			} else {
				$("#" + this.spanIdCursorInfo).text("");
			}
		}

		if(this.XY.x1 === null) return;

		// Scroll
		this.XY.x2 = x;
		this.XY.y2 = y;
		if(this.XY.scrollLeft !== null) {
			$(this.cvs).parent().scrollLeft(this.XY.scrollLeft - (parseInt(e.clientX) - this.XY.scrollX1));
			$(this.cvs).parent().scrollTop(this.XY.scrollTop - (parseInt(e.clientY) - this.XY.scrollY1));
		} else {
			if(this.bufGraphImage) this.ctx.putImageData(this.bufGraphImage, 0, 0);

			var sel = this.getSelectReg.call(this);
//			if(this.DEBUG_MODE) console.log(sel);
			this.ctx.fillStyle = this.style.selectFillColor;
			this.ctx.strokeStyle = this.style.selectStrokeColor;
			this.ctx.fillRect(sel.l + this.lOffset, sel.t + this.tOffset, sel.w, sel.h);
			this.ctx.strokeRect(sel.l + this.lOffset, sel.t + this.tOffset, sel.w, sel.h);
		}
	},
	cvsEvent_mouseup:  function(e) {
		e.preventDefault();

		if(this.XY.x1 !== null && this.XY.scrollLeft === null) {
			this.selectRegs = [];
			var sel = this.getSelectReg.call(this);
			if(sel.tv) this.selectRegs.push(sel);

			this.selectPoints(sel);
			this.draw();
		}
		this.XY.x1 = null;
		this.XY.y1 = null;
		this.XY.scrollLeft = null;
		this.XY.scrollTop = null;
		this.XY.scrollX1 = null;
		this.XY.scrollY1 = null;
	},
/*
	cvsEvent_mouseover: function(e) {
		this.mouseOnCvs = true;
	},
*/
	cvsEvent_mouseout:  function(e) {
		this.cvsEvent_mouseup(e);
//		this.mouseOnCvs = false;
		if(this.bufGraphImage) this.ctx.putImageData(this.bufGraphImage, 0, 0);
	},
	cvsEvent_wheel: function(e) {
		if("17" in this.keyState && this.iptIdZoom) {
			e.preventDefault();
			console.log("deltaY:" + e.deltaY);
			var $form = $("#" + this.iptIdZoom);
			var srcVal = parseInt($form.val());
			var dstVal = 0;
			var step =  parseInt($form.attr("step"));
			if(!step) step = 1;
			if(e.deltaY > 0) {
				dstVal = srcVal - step;
			} else if(e.deltaY < 0) {
				dstVal = srcVal + step;
			}
			var max = parseInt($form.attr("max"));
			var min = parseInt($form.attr("min"));
			if(dstVal > max) {
				dstVal = max;
			} else if(dstVal < min) {
				dstVal = min;
			}
			if(this.DEBUG_MODE) console.log("Zoom:" + srcVal + "=>" + dstVal + " min:" + min + ",max:" + max);
			if(srcVal !== dstVal) $form.val(dstVal).trigger("input");
		}
	},
	disableDrug: function() {
		if(this.XY.x1) this.draw();
		this.XY.x1 = null;
		this.XY.y1 = null;
	},
	getSelectReg: function() {
		var xy = this.XY;
		var l = xy.x1 - this.lOffset;
		var r = xy.x2 - this.lOffset;
		if(l > r) { var tmp = l; l = r; r = tmp; }
//		var rGraph = this.cvs.width - this.rOffset - 1;
		var graphW =  this.cvs.width -   this.lOffset - this.rOffset;
		if(l < 0) {
			l = 0;
		} else if(l > graphW-1) {
			l = graphW-1;
		}
		if(r < 0) {
			r = 0;
		} else if(r > graphW-1) {
			r = graphW-1;
		}

		var t = xy.y1 - this.tOffset;
		var b = xy.y2 - this.tOffset;
		if(t > b) {var tmp = t; t = b; b = tmp; }
//		var bGraph =  this.cvs.height -  this.bOffset - 1;
		var graphH = this.cvs.height - this.tOffset - this.bOffset;
		if(t < 0) {
			t = 0;
		} else if(t > graphH-1) {
			t = graphH-1;
		}
		if(b < 0) {
			b = 0;
		} else if(b > graphH-1) {
			b = graphH-1;
		}

		var w = r - l + 1;
		var h = b - t + 1;

		var tv = null; var bv = null; var lv = null; var rv = null;
		if(true) {	//TODO
			tv =  this.yAxis.getVal(graphH, t);
			bv =  this.yAxis.getVal(graphH, b);
			lv = this.xAxis.getVal(graphW, l);
			rv = this.xAxis.getVal(graphW, r);
		}
		return {t:t, b:b, l:l, r:r, w:w, h:h, tv:tv, bv:bv, lv:lv, rv:rv};
	},

	selectPoints: function(selectReg) {
		// Collision detection is done using real values, not pixel positions.
		var graphW = this.cvs.width - this.lOffset - this.rOffset;
		var pRad = this.xAxis.getVal(graphW, this.pointRadius) - this.xAxis.getVal(graphW, 0);	// Extend the selection(to make it easier to select points)
		var act = "";	// Clear -> reselect
		if("16" in this.keyState && "18" in this.keyState) {
			act = "common";
		} else if("18" in this.keyState) {
			act = "remove";
		} else if("16" in this.keyState || "17" in this.keyState) {
			act = "add";
		}

		var bufAccs = this.selectedIds;
		if(!act || act === "common") this.selectedIds = {};
		for(var i=0,len=this.data.length; i<len; i++) {
			var p = this.data[i];
			if(p.x >= selectReg.lv - pRad &&  p.x <= selectReg.rv + pRad &&
				p.y >= selectReg.bv - pRad && p.y <= selectReg.tv + pRad){
				if(act === "common") {
					if(bufAccs[p.id]) this.selectedIds[p.id] = true;
				} else if(act === "remove" || (selectReg.w === 1 && selectReg.h === 1 && p.id in this.selectedIds && act === "add")) {
					// Remove or Deselect
					delete this.selectedIds[p.id];
				} else {
					this.selectedIds[p.id] = true;
				}
			}
		}
		bufAccs = null;
		this.addHistory();
		if(this.onselect) this.onselect();
	},
/*
	selectPoints: function(selectReg) {
		// Collision detection is done using real values, not pixel positions.
		var graphW = this.cvs.width - this.lOffset - this.rOffset;
		var pRad = this.xAxis.getVal(graphW, this.pointRadius) - this.xAxis.getVal(graphW, 0);	// Extend the selection(to make it easier to select points)
		if( !("16" in this.keyState || "17" in this.keyState || "18" in this.keyState) ) this.selectedIds = {};
		for(var i=0,len=this.data.length; i<len; i++) {
			var p = this.data[i];
			if(p.x >= selectReg.lv - pRad &&  p.x <= selectReg.rv + pRad &&
				p.y >= selectReg.bv - pRad && p.y <= selectReg.tv + pRad){
				if("18" in this.keyState || (selectReg.w === 1 && selectReg.h === 1 && p.id in this.selectedIds && ("16" in this.keyState || "17" in this.keyState))) {
					delete this.selectedIds[p.id];
				} else {
					this.selectedIds[p.id] = true;
				}
			}
		}
		this.addHistory();
		if(this.onselect) this.onselect();
	},
*/
	// For Undo/Redo
	addHistory: function() {
		var his = this.selectedIds;
		if(!his) his = {};
		var lenUndoBuff = this.undoBuff.length;

		// If it's the same as last time, don't add history.
		if(lenUndoBuff > 0) {
			var lastHis =  this.undoBuff[lenUndoBuff-1];
			var lastIds = Object.keys(lastHis);
			var newIds = Object.keys(his);
			var lenLastIds = lastIds.length;
			var lenNewIds = newIds.length;
			var diff = false;
			if(lenLastIds === lenNewIds) {
				if(lenLastIds === 0) {
					// Contiguous empty selection. skip.
				} else {
					for(var i=0; i<lenLastIds; i++) {
						if( !(lastIds[i] in his) ) { diff = true; break; }
					}
				}
			} else {
				diff = true;
			}
			if(!diff) return;
		}
		if(this.DEBUG_MODE) console.log("Exec tasukeGraphManager.addHistory()");

		if(lenUndoBuff >= this.maxUndoBuff) this.undoBuff.shift();
		this.undoBuff.push(tasukeGraphManager.getMapClone(his));

		this.redoBuff.splice(0);	// Empty redo history.
		this.syncUndoButton();
	},
	undoHistory: function() {
		var redoBuf = this.undoBuff.pop();
		if(!redoBuf) return;
		var buf = this.undoBuff[this.undoBuff.length-1];

		if(this.DEBUG_MODE) console.log("Exec tasukeGraphManager.undoHistory()");
		this.redoBuff.push(tasukeGraphManager.getMapClone(redoBuf));
		this.applyHistory(tasukeGraphManager.getMapClone(buf));
//		this.applyHistory(buf);
	},
	redoHistory: function() {
		var buf = this.redoBuff.pop();
		if(!buf) return;

		if(this.DEBUG_MODE) console.log("Exec tasukeGraphManager.redoHistory()");
		this.undoBuff.push(tasukeGraphManager.getMapClone(buf));
		this.applyHistory(tasukeGraphManager.getMapClone(buf));
//		this.applyHistory(buf);
	},
	applyHistory: function(his) {
		this.selectedIds = his;
		this.draw();
		this.syncUndoButton();
		if(this.onselect) this.onselect();
	},
	clearHistory: function() {
		this.undoBuff.splice(0);
		this.redoBuff.splice(0);
		this.lastUndoBuff = null;
	},
	syncUndoButton: function() {
		if(this.buttonIdUndo) {
			var $btnUndo = $('#' + this.buttonIdUndo);
			if(this.undoBuff.length <= 1) {
				$btnUndo.prop("disabled", true);
			} else {
				$btnUndo.prop("disabled", false);
			}
		}
		if(this.buttonIdRedo) {
			var $btnRedo = $('#' + this.buttonIdRedo);
			if(this.redoBuff.length < 1) {
				$btnRedo.prop("disabled", true);
			} else {
				$btnRedo.prop("disabled", false);
			}
		}
	},
	exportAsCanvas: function(xScaleName, yScaleName) {
		var offsetScalename = 18;
		var cvsLgend = null;
			if(this.cvsIdLegend) cvsLgend = document.getElementById(this.cvsIdLegend);
		var w = offsetScalename + this.cvs.width;
			if(cvsLgend) w += parseInt(cvsLgend.width);
		var h = this.cvs.height + offsetScalename;

		var cvsExp = document.createElement("canvas");
		cvsExp.width = w;
		cvsExp.height = h;
		cvsExp.setAttribute("width", w);
		cvsExp.setAttribute("height", h);

		var ctxExp = cvsExp.getContext("2d");
		ctxExp.save();
		ctxExp.putImageData(this.ctx.getImageData(0, 0, this.cvs.width, this.cvs.height), offsetScalename, 0);
		if(cvsLgend) ctxExp.putImageData(cvsLgend.getContext("2d").getImageData(0, 0, cvsLgend.width, cvsLgend.height), offsetScalename + this.cvs.width, 0);

		ctxExp.font = "15px '" + this.style.defFont + "'";
		ctxExp.fillStyle = this.style.scaleTextFillColor;

		// Draw X-ScaleName.
		ctxExp.textAlign = "center";
		ctxExp.textBaseline = "top";
		ctxExp.fillText(xScaleName, offsetScalename + this.cvs.width / 2, this.cvs.height + 2);

		// Draw Y-ScaleName.
		ctxExp.translate(offsetScalename / 2, this.cvs.height / 2);
		ctxExp.rotate(Math.PI / -2);
		ctxExp.fillText(yScaleName, 0, 0);

		ctxExp.restore();

		return cvsExp; 
	},

	// Callback events (abstract method).
	onselect: null
/*
	onselect: function() {
		if(this.DEBUG_MODE) console.log("Call onselect.")
	}
*/
};


/**
 * Axis Object
 */
var tasukeGraphAxis = function() {
	this.clear();
/*
	this.max = Number.NEGATIVE_INFINITY;
	this.min = Number.POSITIVE_INFINITY;
	this.scaleUnit = null;
	this.scaleUnitSub = null;
	this.unitMax = null;
	this.unitMin = null;
*/
	this.reverse = false;	// if yAxis, set true;
}

// Static methods
tasukeGraphAxis.getScaleUnitList = function() {
	var tempArr = [1, 2, 2.5, 3, 4, 5];
	var lenTempArr = tempArr.length;
	var arr = [];
	for(var i=-6; i<=5; i++) {
		var m = 10 ** i;
		for(var j=0; j<lenTempArr; j++) {
			var val = tempArr[j] * m;
			if(i<0) val = parseFloat(val.toFixed(Math.abs(i)));
			arr.push(val);
		}
	}
	return arr;
}
tasukeGraphAxis.getRoundNumber = function(f, validDigit=2) {
	var val = f;
//	var asZeroBorder = parseFloat("0." + "0".repeat(validDigit) + "1");
//	if(val > (-1 * asZeroBorder) &&  val < asZeroBorder) {
	if(val > -0.0000001 && val < 0.0000001) {
		val = 0;
	} else if(f > -1 && f < 1) {
		var retArr = /^[\-0]+\.(0*)[^0]/.exec(f.toString());
		if(retArr) {
			// validDigit=4: 0.0001234567 -> 0.0001235
			var lenMz = retArr[1].length;
			val = f.toFixed(lenMz + validDigit);
		}
	} else {
		var retArr = /^\-?(\w+)\.(\w+)$/.exec(f.toString());
		if(retArr) {
			var lenIntPart = retArr[1].length;
			if(lenIntPart >= validDigit) {
				// validDigit=4: 12345.67 -> 12346
				val = Math.round(f);
			} else {
				// validDigit=6: 1234.567 -> 1234.57
				val = f.toFixed(validDigit - lenIntPart);
			}
		}
	}
	val = parseFloat(val);
	if(val == 0) val = 0;
	return val;
}

// Instance methods
tasukeGraphAxis.prototype = {
	clear: function() {
		this.label = "";
		this.max = Number.NEGATIVE_INFINITY;
		this.min = Number.POSITIVE_INFINITY;
		this.scaleUnit = null;
		this.scaleUnitSub = null;
		this.unitMax = null;
		this.unitMin = null;
	},
	adjustScale: function(data, divNum=5) {
		for(var i=0,len=data.length; i<len; i++) {
			var val = data[i];
			if(this.min > val) {
				this.min = val;
			}
			if(this.max < val) {
				this.max = val;
			}
		}
		var preUnit =  (this.max - this.min) / divNum;
			if(this.max === this.min) preUnit = Math.abs(tasukeGraphAxis.getRoundNumber(data[0], 1) / 4);	// data.length = 1;
		var unitList = tasukeGraphAxis.getScaleUnitList();
		var lastUnit = unitList[0];
		for(var i=0,len=unitList.length; i<len; i++) {
			if(preUnit < unitList[i]) {
				this.scaleUnit = lastUnit;
				this.scaleUnitSub = lastUnit / 2;
				break;
			}
			lastUnit = unitList[i];
		}
//console.log("scaleUnit:" + this.scaleUnit);

		this.unitMax = tasukeGraphAxis.getRoundNumber(Math.ceil(this.max / this.scaleUnit) * this.scaleUnit, 2);
		this.unitMin = tasukeGraphAxis.getRoundNumber(Math.floor(this.min / this.scaleUnit) * this.scaleUnit, 2);
		if(this.unitMax === this.unitMin) {
			this.unitMax += this.scaleUnit;
			this.unitMin -= this.scaleUnit;
		}
//console.log("Test:" + this.unitMax + "/" + this.unitMin);
//console.log(data);
	},
	getPos: function(graphW, val) {
		if(val < this.unitMin || val > this.unitMax) return null;

		var pos;
		if(this.reverse) {
			pos = Math.round(graphW / (this.unitMax - this.unitMin) * (this.unitMax - val));
		} else {
			pos = Math.round(graphW / (this.unitMax - this.unitMin) * (val - this.unitMin));
		}
//		console.log("tasukeGraphAxis.getPos:" + graphW + "/(" + this.unitMax + "-" + this.unitMin + ")*(" + val + "-" + this.unitMax + ")=" + pos);
		return pos;
	},
	getVal: function(graphW, pos) {
		var val;
		if(this.reverse) {
			val = this.unitMax - pos * (this.unitMax - this.unitMin) / graphW
		} else {
			val = this.unitMin + pos * (this.unitMax - this.unitMin) / graphW; 
		}
		if(val < this.unitMin || val > this.unitMax) val = null;
		return val;
	}
};

var viridisMap = function() {
	this.map = [
[0.26700401,  0.00487433,  0.32941519],
[0.26851048,  0.00960483,  0.33542652],
[0.26994384,  0.01462494,  0.34137895],
[0.27130489,  0.01994186,  0.34726862],
[0.27259384,  0.02556309,  0.35309303],
[0.27380934,  0.03149748,  0.35885256],
[0.27495242,  0.03775181,  0.36454323],
[0.27602238,  0.04416723,  0.37016418],
[0.2770184 ,  0.05034437,  0.37571452],
[0.27794143,  0.05632444,  0.38119074],
[0.27879067,  0.06214536,  0.38659204],
[0.2795655 ,  0.06783587,  0.39191723],
[0.28026658,  0.07341724,  0.39716349],
[0.28089358,  0.07890703,  0.40232944],
[0.28144581,  0.0843197 ,  0.40741404],
[0.28192358,  0.08966622,  0.41241521],
[0.28232739,  0.09495545,  0.41733086],
[0.28265633,  0.10019576,  0.42216032],
[0.28291049,  0.10539345,  0.42690202],
[0.28309095,  0.11055307,  0.43155375],
[0.28319704,  0.11567966,  0.43611482],
[0.28322882,  0.12077701,  0.44058404],
[0.28318684,  0.12584799,  0.44496   ],
[0.283072  ,  0.13089477,  0.44924127],
[0.28288389,  0.13592005,  0.45342734],
[0.28262297,  0.14092556,  0.45751726],
[0.28229037,  0.14591233,  0.46150995],
[0.28188676,  0.15088147,  0.46540474],
[0.28141228,  0.15583425,  0.46920128],
[0.28086773,  0.16077132,  0.47289909],
[0.28025468,  0.16569272,  0.47649762],
[0.27957399,  0.17059884,  0.47999675],
[0.27882618,  0.1754902 ,  0.48339654],
[0.27801236,  0.18036684,  0.48669702],
[0.27713437,  0.18522836,  0.48989831],
[0.27619376,  0.19007447,  0.49300074],
[0.27519116,  0.1949054 ,  0.49600488],
[0.27412802,  0.19972086,  0.49891131],
[0.27300596,  0.20452049,  0.50172076],
[0.27182812,  0.20930306,  0.50443413],
[0.27059473,  0.21406899,  0.50705243],
[0.26930756,  0.21881782,  0.50957678],
[0.26796846,  0.22354911,  0.5120084 ],
[0.26657984,  0.2282621 ,  0.5143487 ],
[0.2651445 ,  0.23295593,  0.5165993 ],
[0.2636632 ,  0.23763078,  0.51876163],
[0.26213801,  0.24228619,  0.52083736],
[0.26057103,  0.2469217 ,  0.52282822],
[0.25896451,  0.25153685,  0.52473609],
[0.25732244,  0.2561304 ,  0.52656332],
[0.25564519,  0.26070284,  0.52831152],
[0.25393498,  0.26525384,  0.52998273],
[0.25219404,  0.26978306,  0.53157905],
[0.25042462,  0.27429024,  0.53310261],
[0.24862899,  0.27877509,  0.53455561],
[0.2468114 ,  0.28323662,  0.53594093],
[0.24497208,  0.28767547,  0.53726018],
[0.24311324,  0.29209154,  0.53851561],
[0.24123708,  0.29648471,  0.53970946],
[0.23934575,  0.30085494,  0.54084398],
[0.23744138,  0.30520222,  0.5419214 ],
[0.23552606,  0.30952657,  0.54294396],
[0.23360277,  0.31382773,  0.54391424],
[0.2316735 ,  0.3181058 ,  0.54483444],
[0.22973926,  0.32236127,  0.54570633],
[0.22780192,  0.32659432,  0.546532  ],
[0.2258633 ,  0.33080515,  0.54731353],
[0.22392515,  0.334994  ,  0.54805291],
[0.22198915,  0.33916114,  0.54875211],
[0.22005691,  0.34330688,  0.54941304],
[0.21812995,  0.34743154,  0.55003755],
[0.21620971,  0.35153548,  0.55062743],
[0.21429757,  0.35561907,  0.5511844 ],
[0.21239477,  0.35968273,  0.55171011],
[0.2105031 ,  0.36372671,  0.55220646],
[0.20862342,  0.36775151,  0.55267486],
[0.20675628,  0.37175775,  0.55311653],
[0.20490257,  0.37574589,  0.55353282],
[0.20306309,  0.37971644,  0.55392505],
[0.20123854,  0.38366989,  0.55429441],
[0.1994295 ,  0.38760678,  0.55464205],
[0.1976365 ,  0.39152762,  0.55496905],
[0.19585993,  0.39543297,  0.55527637],
[0.19410009,  0.39932336,  0.55556494],
[0.19235719,  0.40319934,  0.55583559],
[0.19063135,  0.40706148,  0.55608907],
[0.18892259,  0.41091033,  0.55632606],
[0.18723083,  0.41474645,  0.55654717],
[0.18555593,  0.4185704 ,  0.55675292],
[0.18389763,  0.42238275,  0.55694377],
[0.18225561,  0.42618405,  0.5571201 ],
[0.18062949,  0.42997486,  0.55728221],
[0.17901879,  0.43375572,  0.55743035],
[0.17742298,  0.4375272 ,  0.55756466],
[0.17584148,  0.44128981,  0.55768526],
[0.17427363,  0.4450441 ,  0.55779216],
[0.17271876,  0.4487906 ,  0.55788532],
[0.17117615,  0.4525298 ,  0.55796464],
[0.16964573,  0.45626209,  0.55803034],
[0.16812641,  0.45998802,  0.55808199],
[0.1666171 ,  0.46370813,  0.55811913],
[0.16511703,  0.4674229 ,  0.55814141],
[0.16362543,  0.47113278,  0.55814842],
[0.16214155,  0.47483821,  0.55813967],
[0.16066467,  0.47853961,  0.55811466],
[0.15919413,  0.4822374 ,  0.5580728 ],
[0.15772933,  0.48593197,  0.55801347],
[0.15626973,  0.4896237 ,  0.557936  ],
[0.15481488,  0.49331293,  0.55783967],
[0.15336445,  0.49700003,  0.55772371],
[0.1519182 ,  0.50068529,  0.55758733],
[0.15047605,  0.50436904,  0.55742968],
[0.14903918,  0.50805136,  0.5572505 ],
[0.14760731,  0.51173263,  0.55704861],
[0.14618026,  0.51541316,  0.55682271],
[0.14475863,  0.51909319,  0.55657181],
[0.14334327,  0.52277292,  0.55629491],
[0.14193527,  0.52645254,  0.55599097],
[0.14053599,  0.53013219,  0.55565893],
[0.13914708,  0.53381201,  0.55529773],
[0.13777048,  0.53749213,  0.55490625],
[0.1364085 ,  0.54117264,  0.55448339],
[0.13506561,  0.54485335,  0.55402906],
[0.13374299,  0.54853458,  0.55354108],
[0.13244401,  0.55221637,  0.55301828],
[0.13117249,  0.55589872,  0.55245948],
[0.1299327 ,  0.55958162,  0.55186354],
[0.12872938,  0.56326503,  0.55122927],
[0.12756771,  0.56694891,  0.55055551],
[0.12645338,  0.57063316,  0.5498411 ],
[0.12539383,  0.57431754,  0.54908564],
[0.12439474,  0.57800205,  0.5482874 ],
[0.12346281,  0.58168661,  0.54744498],
[0.12260562,  0.58537105,  0.54655722],
[0.12183122,  0.58905521,  0.54562298],
[0.12114807,  0.59273889,  0.54464114],
[0.12056501,  0.59642187,  0.54361058],
[0.12009154,  0.60010387,  0.54253043],
[0.11973756,  0.60378459,  0.54139999],
[0.11951163,  0.60746388,  0.54021751],
[0.11942341,  0.61114146,  0.53898192],
[0.11948255,  0.61481702,  0.53769219],
[0.11969858,  0.61849025,  0.53634733],
[0.12008079,  0.62216081,  0.53494633],
[0.12063824,  0.62582833,  0.53348834],
[0.12137972,  0.62949242,  0.53197275],
[0.12231244,  0.63315277,  0.53039808],
[0.12344358,  0.63680899,  0.52876343],
[0.12477953,  0.64046069,  0.52706792],
[0.12632581,  0.64410744,  0.52531069],
[0.12808703,  0.64774881,  0.52349092],
[0.13006688,  0.65138436,  0.52160791],
[0.13226797,  0.65501363,  0.51966086],
[0.13469183,  0.65863619,  0.5176488 ],
[0.13733921,  0.66225157,  0.51557101],
[0.14020991,  0.66585927,  0.5134268 ],
[0.14330291,  0.66945881,  0.51121549],
[0.1466164 ,  0.67304968,  0.50893644],
[0.15014782,  0.67663139,  0.5065889 ],
[0.15389405,  0.68020343,  0.50417217],
[0.15785146,  0.68376525,  0.50168574],
[0.16201598,  0.68731632,  0.49912906],
[0.1663832 ,  0.69085611,  0.49650163],
[0.1709484 ,  0.69438405,  0.49380294],
[0.17570671,  0.6978996 ,  0.49103252],
[0.18065314,  0.70140222,  0.48818938],
[0.18578266,  0.70489133,  0.48527326],
[0.19109018,  0.70836635,  0.48228395],
[0.19657063,  0.71182668,  0.47922108],
[0.20221902,  0.71527175,  0.47608431],
[0.20803045,  0.71870095,  0.4728733 ],
[0.21400015,  0.72211371,  0.46958774],
[0.22012381,  0.72550945,  0.46622638],
[0.2263969 ,  0.72888753,  0.46278934],
[0.23281498,  0.73224735,  0.45927675],
[0.2393739 ,  0.73558828,  0.45568838],
[0.24606968,  0.73890972,  0.45202405],
[0.25289851,  0.74221104,  0.44828355],
[0.25985676,  0.74549162,  0.44446673],
[0.26694127,  0.74875084,  0.44057284],
[0.27414922,  0.75198807,  0.4366009 ],
[0.28147681,  0.75520266,  0.43255207],
[0.28892102,  0.75839399,  0.42842626],
[0.29647899,  0.76156142,  0.42422341],
[0.30414796,  0.76470433,  0.41994346],
[0.31192534,  0.76782207,  0.41558638],
[0.3198086 ,  0.77091403,  0.41115215],
[0.3277958 ,  0.77397953,  0.40664011],
[0.33588539,  0.7770179 ,  0.40204917],
[0.34407411,  0.78002855,  0.39738103],
[0.35235985,  0.78301086,  0.39263579],
[0.36074053,  0.78596419,  0.38781353],
[0.3692142 ,  0.78888793,  0.38291438],
[0.37777892,  0.79178146,  0.3779385 ],
[0.38643282,  0.79464415,  0.37288606],
[0.39517408,  0.79747541,  0.36775726],
[0.40400101,  0.80027461,  0.36255223],
[0.4129135 ,  0.80304099,  0.35726893],
[0.42190813,  0.80577412,  0.35191009],
[0.43098317,  0.80847343,  0.34647607],
[0.44013691,  0.81113836,  0.3409673 ],
[0.44936763,  0.81376835,  0.33538426],
[0.45867362,  0.81636288,  0.32972749],
[0.46805314,  0.81892143,  0.32399761],
[0.47750446,  0.82144351,  0.31819529],
[0.4870258 ,  0.82392862,  0.31232133],
[0.49661536,  0.82637633,  0.30637661],
[0.5062713 ,  0.82878621,  0.30036211],
[0.51599182,  0.83115784,  0.29427888],
[0.52577622,  0.83349064,  0.2881265 ],
[0.5356211 ,  0.83578452,  0.28190832],
[0.5455244 ,  0.83803918,  0.27562602],
[0.55548397,  0.84025437,  0.26928147],
[0.5654976 ,  0.8424299 ,  0.26287683],
[0.57556297,  0.84456561,  0.25641457],
[0.58567772,  0.84666139,  0.24989748],
[0.59583934,  0.84871722,  0.24332878],
[0.60604528,  0.8507331 ,  0.23671214],
[0.61629283,  0.85270912,  0.23005179],
[0.62657923,  0.85464543,  0.22335258],
[0.63690157,  0.85654226,  0.21662012],
[0.64725685,  0.85839991,  0.20986086],
[0.65764197,  0.86021878,  0.20308229],
[0.66805369,  0.86199932,  0.19629307],
[0.67848868,  0.86374211,  0.18950326],
[0.68894351,  0.86544779,  0.18272455],
[0.69941463,  0.86711711,  0.17597055],
[0.70989842,  0.86875092,  0.16925712],
[0.72039115,  0.87035015,  0.16260273],
[0.73088902,  0.87191584,  0.15602894],
[0.74138803,  0.87344918,  0.14956101],
[0.75188414,  0.87495143,  0.14322828],
[0.76237342,  0.87642392,  0.13706449],
[0.77285183,  0.87786808,  0.13110864],
[0.78331535,  0.87928545,  0.12540538],
[0.79375994,  0.88067763,  0.12000532],
[0.80418159,  0.88204632,  0.11496505],
[0.81457634,  0.88339329,  0.11034678],
[0.82494028,  0.88472036,  0.10621724],
[0.83526959,  0.88602943,  0.1026459 ],
[0.84556056,  0.88732243,  0.09970219],
[0.8558096 ,  0.88860134,  0.09745186],
[0.86601325,  0.88986815,  0.09595277],
[0.87616824,  0.89112487,  0.09525046],
[0.88627146,  0.89237353,  0.09537439],
[0.89632002,  0.89361614,  0.09633538],
[0.90631121,  0.89485467,  0.09812496],
[0.91624212,  0.89609127,  0.1007168 ],
[0.92610579,  0.89732977,  0.10407067],
[0.93590444,  0.8985704 ,  0.10813094],
[0.94563626,  0.899815  ,  0.11283773],
[0.95529972,  0.90106534,  0.11812832],
[0.96489353,  0.90232311,  0.12394051],
[0.97441665,  0.90358991,  0.13021494],
[0.98386829,  0.90486726,  0.13689671],
[0.99324789,  0.90615657,  0.1439362]
	];
};
viridisMap.prototype = {
	getRgb: function(rate) {
		var idx = Math.ceil((this.map.length - 1) * rate);
		var target = this.map[idx];
		return "rgb(" + parseInt(target[0]*255) + "," + parseInt(target[1]*255) + "," + parseInt(target[2]*255) + ")";
	}
};
