This is the second in a series of 3D scatter plots rendered using webGL. The first example used static data stored on the server whereas this example allows users to upload csv files. The files need to be in the following format:
Var 1a | Var 2a | Var 3a | Var 1b | Var 2b | Var 3b | Var 1c | Var 2c | Var 3c |
---|---|---|---|---|---|---|---|---|
x1 | y1 | z1 | x2 | y2 | z2 | x3 | y3 | z3 |
235 | 597 | 487 | 232 | 593 | 156 | 325 | 945 | 332 |
458 | 702 | 724 | 735 | 152 | 717 | 273 | 350 | 598 |
You can change view by rotating cube using the mouse, move it left and right to rotate the cube around the y-axis and up and down to rotate it about the x adn z axis's. At present there's no relationship between the colours of the particles.
The visualisation uses the fantastic threejs library for the 3D and hooks into webGL. The example presented here is heavily based on the threejs scatter plot example. I've also used d3.js for some of convenience functions to import the data, scale the data and set up the ranges for the axis's.
xxxxxxxxxx
<html>
<head>
<script src="three.min.js"></script>
<script src="d3.v3.min.js" charset="utf-8"></script>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style type="text/css">
body {
margin: 0px;
padding: 0px;
}
#container {
width:960px;
height:500px;
}
</style>
</head>
<div id="wrapper">
<p>
<input id="uploader" name="uploader" type="file" />
</p>
<div id="container"></div>
</div>
<body>
<!--div id="container"></div-->
<script>
// <!--
function createTextCanvas(text, color, font, size) {
size = size || 16;
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var fontStr = (size + 'px ') + (font || 'Arial');
ctx.font = fontStr;
var w = ctx.measureText(text).width;
var h = Math.ceil(size);
canvas.width = w;
canvas.height = h;
ctx.font = fontStr;
ctx.fillStyle = color || 'black';
ctx.fillText(text, 0, Math.ceil(size * 0.8));
return canvas;
}
function createText2D(text, color, font, size, segW, segH) {
var canvas = createTextCanvas(text, color, font, size);
var plane = new THREE.PlaneGeometry(canvas.width, canvas.height, segW, segH);
var tex = new THREE.Texture(canvas);
tex.needsUpdate = true;
var planeMat = new THREE.MeshBasicMaterial({
map: tex,
color: 0xffffff,
transparent: true
});
var mesh = new THREE.Mesh(plane, planeMat);
mesh.scale.set(0.5, 0.5, 0.5);
mesh.doubleSided = true;
return mesh;
}
// from https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
function hexToRgb(hex) { //TODO rewrite with vector output
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function sq(x) {
var s = Math.pow(x, 2);
return s;
}
function getPts(x) {
//console.log(x)
var unfiltered = [],
lowPass = [],
highPass = [];
x.forEach(function(d, i) {
var line = d.split(",");
unfiltered[i] = {
x: +line[0],
y: +line[1],
z: +line[2]
};
lowPass[i] = {
x: +line[4],
y: +line[5],
z: +line[6]
};
highPass[i] = {
x: +line[7],
y: +line[8],
z: +line[9]
};
})
var xyzData = {
unfiltered: unfiltered,
lowPass: lowPass,
highPass: highPass
}
return xyzData;
}
var uploader = document.getElementById("uploader");
var reader = new FileReader();
var data;
reader.onload = function(e) {
var contents = e.target.result;
var rawData = contents.split(/\n/);
var tempData = rawData.slice(2, rawData.length);
data = getPts(tempData);
scatter(data);
// remove button after loading file
uploader.parentNode.removeChild(uploader);
};
uploader.addEventListener("change", handleFiles, false);
function handleFiles() {
var file = this.files[0];
reader.readAsText(file);
};
var renderer = new THREE.WebGLRenderer({
antialias: true
});
var w = 960;
var h = 500;
renderer.setSize(w, h);
document.getElementById("container").appendChild(renderer.domElement);
renderer.setClearColorHex(0xEEEEEE, 1.0);
var camera = new THREE.PerspectiveCamera(45, w / h, 1, 10000);
camera.position.z = 200;
camera.position.x = -100;
camera.position.y = 100;
var scene = new THREE.Scene();
var scatterPlot = new THREE.Object3D();
scene.add(scatterPlot);
scatterPlot.rotation.y = 0;
function v(x, y, z) {
return new THREE.Vector3(x, y, z);
}
var format = d3.format("+.3f");
function scatter(data) {
var temp = data.unfiltered;
var xExent = d3.extent(temp, function(d) {
return d.x;
}),
yExent = d3.extent(data.unfiltered, function(d) {
return d.y;
}),
zExent = d3.extent(data.unfiltered, function(d) {
return d.z;
});
var vpts = {
xMax: xExent[1],
xCen: (xExent[1] + xExent[0]) / 2,
xMin: xExent[0],
yMax: yExent[1],
yCen: (yExent[1] + yExent[0]) / 2,
yMin: yExent[0],
zMax: zExent[1],
zCen: (zExent[1] + zExent[0]) / 2,
zMin: zExent[0]
}
var colour = d3.scale.category20c();
var xScale = d3.scale.linear()
.domain(xExent)
.range([-50, 50]);
var yScale = d3.scale.linear()
.domain(yExent)
.range([-50, 50]);
var zScale = d3.scale.linear()
.domain(zExent)
.range([-50, 50]);
var lineGeo = new THREE.Geometry();
lineGeo.vertices.push(
v(xScale(vpts.xMin), yScale(vpts.yCen), zScale(vpts.zCen)), v(xScale(vpts.xMax), yScale(vpts.yCen), zScale(vpts.zCen)),
v(xScale(vpts.xCen), yScale(vpts.yMin), zScale(vpts.zCen)), v(xScale(vpts.xCen), yScale(vpts.yMax), zScale(vpts.zCen)),
v(xScale(vpts.xCen), yScale(vpts.yCen), zScale(vpts.zMax)), v(xScale(vpts.xCen), yScale(vpts.yCen), zScale(vpts.zMin)),
v(xScale(vpts.xMin), yScale(vpts.yMax), zScale(vpts.zMin)), v(xScale(vpts.xMax), yScale(vpts.yMax), zScale(vpts.zMin)),
v(xScale(vpts.xMin), yScale(vpts.yMin), zScale(vpts.zMin)), v(xScale(vpts.xMax), yScale(vpts.yMin), zScale(vpts.zMin)),
v(xScale(vpts.xMin), yScale(vpts.yMax), zScale(vpts.zMax)), v(xScale(vpts.xMax), yScale(vpts.yMax), zScale(vpts.zMax)),
v(xScale(vpts.xMin), yScale(vpts.yMin), zScale(vpts.zMax)), v(xScale(vpts.xMax), yScale(vpts.yMin), zScale(vpts.zMax)),
v(xScale(vpts.xMin), yScale(vpts.yCen), zScale(vpts.zMax)), v(xScale(vpts.xMax), yScale(vpts.yCen), zScale(vpts.zMax)),
v(xScale(vpts.xMin), yScale(vpts.yCen), zScale(vpts.zMin)), v(xScale(vpts.xMax), yScale(vpts.yCen), zScale(vpts.zMin)),
v(xScale(vpts.xMin), yScale(vpts.yMax), zScale(vpts.zCen)), v(xScale(vpts.xMax), yScale(vpts.yMax), zScale(vpts.zCen)),
v(xScale(vpts.xMin), yScale(vpts.yMin), zScale(vpts.zCen)), v(xScale(vpts.xMax), yScale(vpts.yMin), zScale(vpts.zCen)),
v(xScale(vpts.xMax), yScale(vpts.yMin), zScale(vpts.zMin)), v(xScale(vpts.xMax), yScale(vpts.yMax), zScale(vpts.zMin)),
v(xScale(vpts.xMin), yScale(vpts.yMin), zScale(vpts.zMin)), v(xScale(vpts.xMin), yScale(vpts.yMax), zScale(vpts.zMin)),
v(xScale(vpts.xMax), yScale(vpts.yMin), zScale(vpts.zMax)), v(xScale(vpts.xMax), yScale(vpts.yMax), zScale(vpts.zMax)),
v(xScale(vpts.xMin), yScale(vpts.yMin), zScale(vpts.zMax)), v(xScale(vpts.xMin), yScale(vpts.yMax), zScale(vpts.zMax)),
v(xScale(vpts.xCen), yScale(vpts.yMin), zScale(vpts.zMax)), v(xScale(vpts.xCen), yScale(vpts.yMax), zScale(vpts.zMax)),
v(xScale(vpts.xCen), yScale(vpts.yMin), zScale(vpts.zMin)), v(xScale(vpts.xCen), yScale(vpts.yMax), zScale(vpts.zMin)),
v(xScale(vpts.xMax), yScale(vpts.yMin), zScale(vpts.zCen)), v(xScale(vpts.xMax), yScale(vpts.yMax), zScale(vpts.zCen)),
v(xScale(vpts.xMin), yScale(vpts.yMin), zScale(vpts.zCen)), v(xScale(vpts.xMin), yScale(vpts.yMax), zScale(vpts.zCen)),
v(xScale(vpts.xMax), yScale(vpts.yMax), zScale(vpts.zMin)), v(xScale(vpts.xMax), yScale(vpts.yMax), zScale(vpts.zMax)),
v(xScale(vpts.xMax), yScale(vpts.yMin), zScale(vpts.zMin)), v(xScale(vpts.xMax), yScale(vpts.yMin), zScale(vpts.zMax)),
v(xScale(vpts.xMin), yScale(vpts.yMax), zScale(vpts.zMin)), v(xScale(vpts.xMin), yScale(vpts.yMax), zScale(vpts.zMax)),
v(xScale(vpts.xMin), yScale(vpts.yMin), zScale(vpts.zMin)), v(xScale(vpts.xMin), yScale(vpts.yMin), zScale(vpts.zMax)),
v(xScale(vpts.xMin), yScale(vpts.yCen), zScale(vpts.zMin)), v(xScale(vpts.xMin), yScale(vpts.yCen), zScale(vpts.zMax)),
v(xScale(vpts.xMax), yScale(vpts.yCen), zScale(vpts.zMin)), v(xScale(vpts.xMax), yScale(vpts.yCen), zScale(vpts.zMax)),
v(xScale(vpts.xCen), yScale(vpts.yMax), zScale(vpts.zMin)), v(xScale(vpts.xCen), yScale(vpts.yMax), zScale(vpts.zMin)),
v(xScale(vpts.xCen), yScale(vpts.yMin), zScale(vpts.zMin)), v(xScale(vpts.xCen), yScale(vpts.yMin), zScale(vpts.zMax))
);
var lineMat = new THREE.LineBasicMaterial({
color: 0x000000,
lineWidth: 1
});
var line = new THREE.Line(lineGeo, lineMat);
line.type = THREE.Lines;
scatterPlot.add(line);
var titleX = createText2D('-X');
titleX.position.x = xScale(vpts.xMin) - 12,
titleX.position.y = 5;
scatterPlot.add(titleX);
var valueX = createText2D(format(xExent[0]));
valueX.position.x = xScale(vpts.xMin) - 12,
valueX.position.y = -5;
scatterPlot.add(valueX);
var titleX = createText2D('X');
titleX.position.x = xScale(vpts.xMax) + 12;
titleX.position.y = 5;
scatterPlot.add(titleX);
var valueX = createText2D(format(xExent[1]));
valueX.position.x = xScale(vpts.xMax) + 12,
valueX.position.y = -5;
scatterPlot.add(valueX);
var titleY = createText2D('-Y');
titleY.position.y = yScale(vpts.yMin) - 5;
scatterPlot.add(titleY);
var valueY = createText2D(format(yExent[0]));
valueY.position.y = yScale(vpts.yMin) - 15,
scatterPlot.add(valueY);
var titleY = createText2D('Y');
titleY.position.y = yScale(vpts.yMax) + 15;
scatterPlot.add(titleY);
var valueY = createText2D(format(yExent[1]));
valueY.position.y = yScale(vpts.yMax) + 5,
scatterPlot.add(valueY);
var titleZ = createText2D('-Z ' + format(zExent[0]));
titleZ.position.z = zScale(vpts.zMin) + 2;
scatterPlot.add(titleZ);
var titleZ = createText2D('Z ' + format(zExent[1]));
titleZ.position.z = zScale(vpts.zMax) + 2;
scatterPlot.add(titleZ);
var mat = new THREE.ParticleBasicMaterial({
vertexColors: true,
size: 10
});
var pointCount = data.unfiltered.length;
var pointGeo = new THREE.Geometry();
for (var i = 0; i < pointCount; i++) {
var x = xScale(data.unfiltered[i].x);
var y = yScale(data.unfiltered[i].y);
var z = zScale(data.unfiltered[i].z);
pointGeo.vertices.push(new THREE.Vector3(x, y, z));
pointGeo.colors.push(new THREE.Color().setRGB(
hexToRgb(colour(i)).r / 255, hexToRgb(colour(i)).g / 255, hexToRgb(colour(i)).b / 255
));
}
var points = new THREE.ParticleSystem(pointGeo, mat);
scatterPlot.add(points);
renderer.render(scene, camera);
var paused = false;
var last = new Date().getTime();
var down = false;
var sx = 0,
sy = 0;
window.onmousedown = function(ev) {
down = true;
sx = ev.clientX;
sy = ev.clientY;
};
window.onmouseup = function() {
down = false;
};
window.onmousemove = function(ev) {
if (down) {
var dx = ev.clientX - sx;
var dy = ev.clientY - sy;
var dist = Math.sqrt(sq(camera.position.x) + sq(camera.position.y) + sq(camera.position.z));
scatterPlot.rotation.y += dx * 0.01;
scatterPlot.rotation.x += dy * 0.01;
sx += dx;
sy += dy;
}
}
var animating = false;
window.ondblclick = function() {
animating = !animating;
};
function animate(t) {
if (!paused) {
last = t;
renderer.clear();
camera.lookAt(scene.position);
renderer.render(scene, camera);
}
window.requestAnimationFrame(animate, renderer.domElement);
};
animate(new Date().getTime());
onmessage = function(ev) {
paused = (ev.data == 'pause');
};
}
</script>
</body>
</html>
Modified http://d3js.org/d3.v3.min.js to a secure url
https://d3js.org/d3.v3.min.js