<div class="canvas" name="GLA">
<canvas id="GLA" width="200" height="200"></canvas>
<script id="GLA.js">
(function() {
	var canvas = document.getElementById('GLA');
	var ctx    = canvas.getContext('2d');

	var loop = 0, finish = false, id = 0;
	canvas.onclick = function(){
		init();
		loop = 0; finish = false;
		clearInterval(id);
		id = setInterval(function(){
			algorithm();
			draw();
			if (++loop > 1000 || finish) clearInterval(id);
		}, 50);
	};

	// points
	const N = 22;
	var x = [];

	// constraints
	const B = 9;
	var boundary = [];
	for (let i=0; i<B; ++i) boundary.push(i);
	var known = new Int32Array(N).fill(0);

	// adjacency matrix A
	var adj = new Array(N);
	for (let i=0; i<N; i++)
		adj[i] = new Int32Array(N).fill(0);

	// boundary to boundary
	for (let i=0; i<B; i++)
		adj[i][(i+1)%B] = adj[(i+1)%B][i] = 1;
	// boundary to interior
	for (let i=B; i<N; i++) {
		adj[i][i%B] = adj[i%B][i] = 1;
		adj[i][(i-1)%B] = adj[(i-1)%B][i] = 1;
		adj[i][B+(i-1)%(N-B)] = adj[B+(i-1)%(N-B)][i] = 1;
	}

	// degree matrix D
	var wsum = new Float32Array(N).fill(0);
	for (let i=0; i<N; ++i)
		for (let j=0; j<N; ++j)
			if (adj[i][j])
				wsum[i] += adj[i][j];

	function init() {
		x = [[4,5  ],[16,4 ],[9,8  ],[3,16 ],[8,14 ],
			[17,11],[23,9 ],[8,19 ],[16,16],[23,14],
			[5,24 ],[13,25],[19,22],[26,21],[29,16],
			[13,30],[22,31],[28,27],[32,23],[38,19],
			[30,32],[37,28]];

		for (let i=0; i<x.length; ++i)
			for (let j=0; j<x[j].length; ++j)
				x[i][j] /= 10;

		centering(x);

		for (let i=0; i<N; ++i)
			known[i] = false;

		for (let i=0; i<B; ++i) {
			const r = 1.5;
			var t = 2.0 * Math.PI * i / B;
			x[boundary[i]][0] = Math.cos(t) * r;
			x[boundary[i]][1] = Math.sin(t) * r;
			known[boundary[i]] = true;
		}
	}

	function algorithm() {
		const ε = 1e-6;
		const step = 3e-2;
		var error = 0;
		for (let i=0; i<N; ++i) {
			if (known[i]) continue;
			var xsum = 0, ysum = 0;
			for (let j=0; j<N; ++j)
				if (adj[i][j]) {
					xsum += adj[i][j] * x[j][0];
					ysum += adj[i][j] * x[j][1];
				}

			var xnew = x[i][0] + (xsum / wsum[i] - x[i][0]) * step;
			var ynew = x[i][1] + (ysum / wsum[i] - x[i][1]) * step;
			error += Math.abs(xnew - x[i][0]);
			error += Math.abs(ynew - x[i][1]);
			x[i][0] = xnew;
			x[i][1] = ynew;
		}
		if (error < ε) finish = true;
	}

	function centering(x) {
		var n = x.length;
		var d = x[0].length;
		var xm = x[0].slice().fill(0);
		for (var i=0; i<n; ++i) for (var j=0; j<d; ++j) xm[j] += x[i][j];
		for (var i=0; i<d; ++i) xm[i] /= n;
		for (var i=0; i<n; ++i) for (var j=0; j<d; ++j) x[i][j] -= xm[j];
	}

	function DrawPointArray(ctx, p, rgb) {
		const scale = 50;
		ctx.fillStyle = rgb;
		for (var i = 0; i < p.length; ++i) {
			ctx.beginPath();
			ctx.arc(p[i][0] * scale, p[i][1] * scale, 4, 0, Math.PI * 2, true);
			ctx.fill();
		}
	}

	function DrawAdjacencyMatrix(ctx, p, adj, rgb) {
		const scale = 50;
		ctx.strokeStyle = rgb;
		ctx.lineWidth = 2.25;
		ctx.beginPath();
		for (var i=0; i<p.length; ++i)
			for (var j=i+1; j<p.length; ++j)
				if (adj[i][j]) {
					ctx.moveTo(p[i][0] * scale, p[i][1] * scale);
					ctx.lineTo(p[j][0] * scale, p[j][1] * scale);
				}
		ctx.stroke();
	}

	function DrawBanner() {
		ctx.font = "32pt Arial";
		ctx.textBaseline = "middle";
		ctx.textAlign = "center";
		ctx.strokeStyle = "rgb(0,127,0)";
		ctx.strokeText("Click Me !", canvas.width/2, canvas.height/2);
	}

	function draw() {
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.save();
		ctx.translate(0, canvas.height);
		ctx.scale(1, -1);
		ctx.translate(canvas.width/2, canvas.height/2);

		DrawAdjacencyMatrix(ctx, x, adj, "rgb(255,192,0)");
		DrawPointArray(ctx, x, "rgb(0,0,0)");
		DrawPointArray(ctx, x.slice(0,B), "rgb(198,0,0)");
		ctx.restore();
	}

	init();
	draw();
	DrawBanner();
})();
</script>
</div><div class="canvas" name="PCA2D">
<canvas id="PCA2D" width="200" height="200"></canvas>
<script src="jsfeat-min.js"></script>
<script id="PCA2D.js">
function DrawPointArray(ctx, p, rgb) {
	ctx.fillStyle = rgb;
	ctx.lineWidth = 1.5;
	for (var i = 0; i < p.length; ++i) {
		ctx.beginPath();
		ctx.arc(p[i][0], p[i][1], 3, 0, Math.PI * 2, true);
		ctx.fill();
	}
}

function DrawArrow(ctx, x1, y1, x2, y2, len, angle, gap, rgb) {
	var slope = Math.atan2(y2 - y1, x2 - x1);
	x1 += gap * Math.cos(slope);
	y1 += gap * Math.sin(slope);
	x2 -= gap * Math.cos(slope);
	y2 -= gap * Math.sin(slope);
	ctx.save();
	ctx.strokeStyle = rgb;
	ctx.fillStyle = rgb;
	ctx.beginPath();
	ctx.moveTo(x1, y1);
	ctx.lineTo(x2, y2);
	ctx.stroke();
	ctx.beginPath();
	ctx.moveTo(x2, y2);
	ctx.lineTo(x2 - len * Math.cos(slope-angle), y2 - len * Math.sin(slope-angle));
	ctx.lineTo(x2 - len * Math.cos(slope+angle), y2 - len * Math.sin(slope+angle));
	ctx.fill();
	ctx.restore();
}

function DrawXY(ctx, scalarX, scalarY) {
	DrawArrow(ctx, -scalarX, 0, +scalarX, 0, 8, Math.PI / 5, 0, "black");
	DrawArrow(ctx, 0, -scalarY, 0, +scalarY, 8, Math.PI / 5, 0, "black");
	ctx.save();
	ctx.font = "16pt Arial";
	ctx.textAlign = "center";
	ctx.textBaseline = "middle";
	ctx.fillStyle = "black";
	ctx.fillText("x", +scalarX+8, 0);
	ctx.textBaseline = "bottom";
	ctx.scale(1, -1);
	ctx.fillText("y", 0, -scalarY-8);
	ctx.scale(1, -1);
	ctx.restore();
}

function pca(x) {
	var n = x.length;
	var d = x[0].length;

	// centering
	var xm = x[0].slice().fill(0);
	for (var i=0; i<n; ++i) for (var j=0; j<d; ++j) xm[j] += x[i][j];
	for (var i=0; i<d; ++i) xm[i] /= n;

	// covariance matrix
	var X = new jsfeat.matrix_t(n, d, jsfeat.F64_t | jsfeat.C1_t);
	for (var i=0; i<n; ++i) for (var j=0; j<d; ++j)
		X.data[j*n+i] = x[i][j] - xm[j];
	var C = new jsfeat.matrix_t(d, d, jsfeat.F64_t | jsfeat.C1_t);
	jsfeat.matmath.multiply_AAt(C, X);
	var evectors = new jsfeat.matrix_t(d, d, jsfeat.F64_t | jsfeat.C1_t);
	var evalues  = new jsfeat.matrix_t(1, d, jsfeat.F64_t | jsfeat.C1_t);
	jsfeat.linalg.eigenVV(C, evectors, evalues);

	// eigenvector is filled row by row
	return [xm, evectors.data.slice()];
}

(function() {
	var canvas = document.getElementById('PCA2D');
	var ctx    = canvas.getContext('2d');

	var x = [[4,5  ],[16,4 ],[9,8  ],[3,16 ],[8,14 ],
			 [17,11],[23,9 ],[8,19 ],[16,16],[23,14],
			 [5,24 ],[13,25],[19,22],[26,21],[29,16],
			 [13,30],[22,31],[28,27],[32,23],[38,19],
			 [30,32],[37,28]];

	for (var i=0; i<x.length; ++i)
		for (var j=0; j<x[j].length; ++j)
			x[i][j] *= 4.5;

	var [c, ev] = pca(x);

	// coordinate system;
	ctx.lineWidth = 2;
	ctx.translate(0, canvas.height);
	ctx.scale(1, -1);
	ctx.translate(canvas.width/2 - c[0], canvas.height/2 - c[1]);
	ctx.save();

	// mouse control
	var theta = 0;
	canvas.onmousemove = function(event) {
		var dx = event.offsetX - canvas.width/2;
		var dy = event.offsetY - canvas.height/2;
		theta = Math.atan2(-dy, dx);
		draw();
	}

	draw();
	function draw() {
		ctx.save();
		ctx.setTransform(1, 0, 0, 1, 0, 0);
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.restore();

		ctx.save();
		ctx.translate(+c[0], +c[1]);
		ctx.rotate(theta);
		ctx.translate(-c[0], -c[1]);
		const t = 50;
		DrawXY(ctx, 170, 150);
		DrawPointArray(ctx, x, "black");
		DrawArrow(ctx, c[0], c[1], c[0] + ev[0] * t, c[1] + ev[1] * t, 8, Math.PI / 5, 0, "#CC5500");
		DrawArrow(ctx, c[0], c[1], c[0] + ev[2] * t, c[1] + ev[3] * t, 8, Math.PI / 5, 0, "#00CC55");
		ctx.restore();
	}
})();
</script>
</div><div class="canvas" name="PCA3D">
<canvas id="PCA3D" width="200" height="200"></canvas>
<script src="three.min.js"></script>
<script id="PCA3D.js">
(function(){
	var canvas = document.getElementById('PCA3D');
	var renderer = new THREE.WebGLRenderer({canvas: canvas, alpha: true, preserveDrawingBuffer: true});
	var scene = new THREE.Scene();

	var light = new THREE.PointLight( 0xffffff, 1, 100 );
	light.position.set( 50, 50, 50 );
	scene.add( light );

	var radius = 75;
	var n = 50;
	var p = new Array(n);
	for (var i = 0; i < n; ++i) {
		var x = radius * Math.random() * 0.3;
		var y = radius * Math.random() * 0.9;
		var z = radius * Math.random() * 0.6;
		p[i] = [x,y,z];
	}
	var [c, ev] = pca(p);

	var geometry = new THREE.SphereGeometry( 2 );
	var material = new THREE.MeshPhongMaterial( {
		color : 'gray',
		emissive : 0x222222,
		reflectivity : 0.8,
	} );
	for (var i = 0; i < n; ++i) {
		var sphere = new THREE.Mesh( geometry, material );
		sphere.position.fromArray( p[i] );
		scene.add( sphere );
	}

	var dir1 = new THREE.Vector3( ev[0], ev[1], ev[2] ).normalize();
	var dir2 = new THREE.Vector3( ev[3], ev[4], ev[5] ).normalize();
	var dir3 = new THREE.Vector3( ev[6], ev[7], ev[8] ).normalize();
	var center = new THREE.Vector3( c[0], c[1], c[2] );
	var arrowHelper1 = new THREE.ArrowHelper( dir1, center, 20, 0xCC5500, 4, 4 );
	var arrowHelper2 = new THREE.ArrowHelper( dir2, center, 20, 0x00CC55, 4, 4 );
	var arrowHelper3 = new THREE.ArrowHelper( dir3, center, 20, 0x5500CC, 4, 4 );
	arrowHelper1.line.material.linewidth = 3;	// bug
	arrowHelper2.line.material.linewidth = 3;	// bug
	arrowHelper3.line.material.linewidth = 3;	// bug
	scene.add( arrowHelper1 );
	scene.add( arrowHelper2 );
	scene.add( arrowHelper3 );
	var axesHelper = new THREE.AxesHelper( radius * 2 );
	scene.add( axesHelper );

	var camera = new THREE.PerspectiveCamera();
	var depth = radius * 1.2;
	var angle_x = 0.5;
	var angle_y = 0.5;
	function updateCamera() {
		camera.position.x = center.x + depth * Math.cos(angle_y) * Math.cos(angle_x);
		camera.position.y = center.y + depth * Math.sin(-angle_x);
		camera.position.z = center.z + depth * Math.sin(angle_y);
		camera.up.set( 0, 0, 1 );
		camera.lookAt( center );
	}
	updateCamera();
	renderer.render(scene, camera);

	canvas.onmousemove = function(event) {
		angle_x = (event.offsetX-canvas.width/2)/(canvas.width/2);
		angle_y = (event.offsetY-canvas.height/2)/(canvas.height/2);
		updateCamera();
		renderer.render(scene, camera);
	}
})();
</script>
</div>